Using transactions in CakePHP TreeBehavior? - php

We have been using TreeBehavior with CakePHP 3.6 for a while now. Lately, with a big tree (some 90000 nodes) we are running into problems.
When deleting a node, the beforeDelete() function is called in cakephp/src/ORM/Behavior/TreeBehavior:
/**
* Also deletes the nodes in the subtree of the entity to be delete
*
* #param \Cake\Event\Event $event The beforeDelete event that was fired
* #param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
* #return void
*/
public function beforeDelete(Event $event, EntityInterface $entity)
{
$config = $this->getConfig();
$this->_ensureFields($entity);
$left = $entity->get($config['left']);
$right = $entity->get($config['right']);
$diff = $right - $left + 1;
if ($diff > 2) {
$query = $this->_scope($this->_table->query())
->delete()
->where(function ($exp) use ($config, $left, $right) {
/* #var \Cake\Database\Expression\QueryExpression $exp */
return $exp
->gte($config['leftField'], $left + 1)
->lte($config['leftField'], $right - 1);
});
$statement = $query->execute();
$statement->closeCursor();
}
$this->_sync($diff, '-', "> {$right}");
}
This function calls the _sync() function to update the left and right values of the tree:
/**
* Auxiliary function used to automatically alter the value of both the left and
* right columns by a certain amount that match the passed conditions
*
* #param int $shift the value to use for operating the left and right columns
* #param string $dir The operator to use for shifting the value (+/-)
* #param string $conditions a SQL snipped to be used for comparing left or right
* against it.
* #param bool $mark whether to mark the updated values so that they can not be
* modified by future calls to this function.
* #return void
*/
protected function _sync($shift, $dir, $conditions, $mark = false)
{
$config = $this->_config;
foreach ([$config['leftField'], $config['rightField']] as $field) {
$query = $this->_scope($this->_table->query());
$exp = $query->newExpr();
$movement = clone $exp;
$movement->add($field)->add((string)$shift)->setConjunction($dir);
$inverse = clone $exp;
$movement = $mark ?
$inverse->add($movement)->setConjunction('*')->add('-1') :
$movement;
$where = clone $exp;
$where->add($field)->add($conditions)->setConjunction('');
$query->update()
->set($exp->eq($field, $movement))
->where($where);
$query->execute()->closeCursor();
}
}
However, it seems that the update queries performed by this _sync() function are not wrapped in a transaction, and look like this:
UPDATE `leafs` SET `lft` = ((`lft` - 12)) WHERE ((`lft` > 52044))
UPDATE `leafs` SET `rght` = ((`rght` - 12)) WHERE ((`rght` > 52044))
Should these not be wrapped in a transaction, like this?
START TRANSACTION
UPDATE `leafs` SET `lft` = ((`lft` - 12)) WHERE ((`lft` > 52044))
UPDATE `leafs` SET `rght` = ((`rght` - 12)) WHERE ((`rght` > 52044))
COMMIT
... especially since the values that are updated (lft and rght) also occur in the WHERE clause. We are having some issues with the tree becoming corrupted and are wondering whether this might be a cause... especially when multiple operations are performed in quick succession on a big tree.

Related

What's the best annotation to describe a simulation overload function?

I am using Visual Studio Code(VS Code), it can display the annotation of functios.
I am try to improve write a PHPDoc standard.
Can any suggestions to me? Thank you.
Here have 3 functions, but they are not the focal point .
I just want to know how to write a PHPDoc standard describe the function 2 and simulation overload function 3.
Ref:
https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#54-examples
<?php
// Function 1:
/**
* Check if A is equal to B.(Just a sample note, not important)
*
* #param string A
* #param string B
*
* #return bool
*/
function checkAandB_1($a, $b){
if($a === $b)
return true;
return false;
}
// Function 2:
/**
* Check if A is equal to B.(Just a sample note, not important)
*
* #param string Input x,y (<=here, I need to known how to write a right note to describe this function can support input a string combin x and y with a comma in one line)
*
* #return bool
*/
function checkAandB_2($str){
if( substr_count($str, ",") === 1 ) {
$strArr = explode(",",$str);
if($strArr[0] === $strArr[1])
return true;
}
return false;
}
// Function 3: Simulation a overload function
/**
* Check if A is equal to B.(Just a sample note, not important)
*
* #param string (What's the best annotation to describe this function?)
*
* #return bool
*/
function checkAandB_overload($str, $b = null){
if(isset($b) && $str === $b){
return true;
}elseif( substr_count($str, ",") === 1 ) {
$strArr = explode(",",$str);
if($strArr[0] === $strArr[1])
return true;
}
return false;
}
var_dump(checkAandB_1("1","2")); // false
var_dump(checkAandB_1("2","2")); // true
var_dump(checkAandB_2("1,2")); // false
var_dump(checkAandB_2("2,2")); // true
var_dump(checkAandB_overload("1","2")); // false
var_dump(checkAandB_overload("2","2")); // true
var_dump(checkAandB_overload("1,2")); // false
var_dump(checkAandB_overload("2,2")); // true

CakePHP3: How to change an association strategy on-the-fly?

I would like to change an association strategy (hasMany) on the fly to "in" (default) to "select". Because this will correct the result for this situation:
"Get all publishers and only the first five books":
$publishersTable = TableRegistry::getTableLocator()->get('Publishers');
$publishersTable->getAssociation('Books')->setStrategy('select');
$query = $publishersTable->find()
->contain(['Books'=> function(Query $q){
return $q->limit(5);
}]);
Unfortunately, Cake still using "in" to run the query and not "separated queries" and the result is only 5 publishers (and not all publishers with the first 5 books).
Is it possible to change the strategy on-the-fly?
Thanks in advance !
A hasMany association will always use a single separate query, never multiple separate queries. The difference between the select and subquery strategies is that one will directly compare against an array of primary keys, and the other against a joined subquery that will match the selected parent records.
What you are trying is to select the greatest-n-per-group, that's not possible with the built in association loaders, and it can be a little tricky depending on the DBMS that you are using, check for example How to limit contained associations per record/group? for an example for MySQL < 8.x using a custom association and loader.
For DBMS that do support it, look into window functions. Here's an example of a loader that uses native window functions, it should be possible to simply replace the one in the linked example with it, but keep in mind that it's not really tested or anything, I just had it laying around from some experiments:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* #var int
*/
protected $limit;
/**
* The target table.
*
* #var \Cake\ORM\Table
*/
protected $target;
/**
* {#inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {#inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {#inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* #var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}
Thanks #ndm but I found another shorter solution:
$publishersTable->find()
->formatResults(function ($results) use ($publishersTable) {
return $results->map(function ($row) use ($publishersTable) {
$row['books'] = $publishersTable->Books->find()
->where(['publisher_id'=>$row['id']])
->limit(5)
->toArray();
return $row;
});
});

Unit test: using the proper terminology for mocking/stubbing

After fundamental changes on my project system architecture, I find myself in a situation where I would need to create "fake" implementation in order to test some functionality that used to be public like the following:
/**
* Display the template linked to the page.
*
* #param $newSmarty Smarty object to use to display the template.
*
* #param $parameters associative Array containing the values to pass to the template.
* The key is the name of the variable in the template and the value is the value of the variable.
*
* #param $account child class in the AccountManager hierarchy
*
* #param $partialview String name of the partial view we are working on
*/
protected function displayPageTemplateSmarty(Smarty &$newSmarty, array $parameters = array(), AccountManager $account = NULL, string $partialview = "")
{
$this->smarty = $newSmarty;
if (is_file(
realpath(dirname(__FILE__)) . "/../../" .
Session::getInstance()->getCurrentDomain() . "/view/" . (
!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
)
)) {
$this->smarty->assign(
'activeLanguage',
Session::getInstance()->getActiveLanguage()
);
$this->smarty->assign('domain', Session::getInstance()->getCurrentDomain());
$this->smarty->assign(
'languages',
Languagecontroller::$supportedLanguages
);
$this->smarty->assign(
'title',
Languagecontroller::getFieldTranslation('PAGE_TITLE', '')
);
$this->smarty->assign_by_ref('PageController', $this);
$htmlTagBuilder = HTMLTagBuilder::getInstance();
$languageController = LanguageController::getInstance();
$this->smarty->assign_by_ref('htmlTagBuilder', $htmlTagBuilder);
$this->smarty->assign_by_ref('languageController', $languageController);
if (!is_null($account)) {
$this->smarty->assign_by_ref('userAccount', $account);
}
if (!is_null($this->menuGenerator)) {
$this->smarty->assign_by_ref('menuGenerator', $this->menuGenerator);
}
foreach ($parameters as $key => $value) {
$this->smarty->assign($key, $value);
}
$this->smarty->display((!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
));
}
}
In this case, the PageController class used to be called directly in controllers, but is now an abstract class extended by the controllers and my unit tests can no longer access the method.
I also have methods like this one in my new session wrapper class that can only be used in very specific context and for which I really need to create fake page implementation to test them.
/**
* Add or update an entry to the page session array.
*
* Note: can only be updated by the PageController.
*
* #param $key String Key in the session array.
* Will not be added if the key is not a string.
*
* #param $value The value to be added to the session array.
*
* #return Boolean
*/
public function updatePageSession(string $key, $value)
{
$trace = debug_backtrace();
$updated = false;
if (isset($trace[1]) and
isset($trace[1]['class']) and
$trace[1]['class'] === 'PageController'
) {
$this->pageSession[$key] = $value;
$updated = true;
}
return $updated;
}
Even though I read a few article, it is still quite unclear in my mind if those fake classes should be considered as "stub" or a "mock" (or even "fake", "dummy" and so on).
I really need to use the proper terminology since my boss is expecting me (in a close future) to delegate most of my workload with oversea developers.
How would you call those fake class implementation created solely for testing purpose in order to be self-explanatory?
Gerard Meszaros explains the terminology of dummies, stubs, spies, mocks, and fakes here.
You can find examples from the PHP world here.

Laravel 5 Cache/Paginate Issue

So I decided to go from Laravel 4 to 5 which took me around 1-2 days because I barely knew how to do the transition. While doing the Upgrade for my app i came across a small problem with Json Pagination.
This code is what allows a PageQuery to be Paginated Via KnockoutJS
/**
* Builds paginate query with given parameters.
*
* #param array $params
* #param integer $page
* #param integer $perPage
*
* #return array
*/
public function buildPaginateQuery(array $params, $page = 1, $perPage = 15)
{
$query = $this->model;
$query = $this->appendParams($params, $query);
$count = (new Cache)->remember('count', '2000', function() use ($query){
return $query->count();
});
$totalPages = $count / $perPage;
$query = $query->skip($perPage * ($page - 1))->take($perPage);
$query = $query->order(isset($params['order']) && $params['order'] ? $params['order'] : null);
//$query = $query->cacheTags(array($this->model->table, 'pagination'))->remember(2000);
$query = (new Cache)->remember(array($this->model->table, 'pagination'), '2000', function() use ($query){
return $query;
});
return array('query' => $query, 'totalPages' => $totalPages, 'totalItems' => $count);
}
which eventually lead to this error in this screenshot
The Error directs to the code above and this code specifically
/**
* Get the full path for the given cache key.
*
* #param string $key
* #return string
*/
protected function path($key)
{
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
$path = $this->directory() . '/'.join('/', $parts).'/'.$hash;
//unset the tags so we use the base cache folder if no
//tags are passed with subsequent call to the same instance
//of this class
//$this->tags = array();
return $path;
}
Im using a custom Cache Driver called TaggedFile. This worked fine in L4 but came across errors because There were some files removed within the Cache Alias. Like the StoreInterface. Can I receive some help for this? If you need me to post anything I will.
More Stuff:
Before I used this to Register the taggedFile Driver in global.php:
Cache::extend('taggedFile', function($app)
{
return new Illuminate\Cache\Repository(new Lib\Extensions\TaggedFileCache);
});
I do not know where exactly to put this. Does anyone know the equivalent of that? I tried putting it in AppServiceProvider but an error came up saying:
Call to undefined method Illuminate\Support\Facades\Cache::extend()
This used to work in L4 so i decided to go into the vendor folder manually find what the problem was....
This only had: getFacadeAccessor (Which L4 also only had but extend worked)
So i decided to use getFacadeAccessor and it worked, but i don't know if that was the solution or not.
As you noticed you are passing an array as a $key value, the safest way would be to replace the code
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
With
$parts = array_slice(str_split($hash = md5(json_encode($key)), 2), 0, 2);
NB: I am not sure what version of php you are running, but json_encode( ... ) is normally faster then serialize( ... )

How to use both fetch() and fetchAll() in a PDO wrapper class?

How can I alter my PDO wrapper class, so that if I expect a single row result with my query it uses fetch() and if it expects multiple results it uses fetchAll().
Right now, if I have only one result I still have to loop through the result array and that seem pretty unpracticable to me.
Query in the model:
public function doccEdit() {
$id = mysql_real_escape_string($_GET['id']);
$this->result = $GLOBALS['db']->select("creditcards", "id = ?", $id);
print_r($this->result);
}
In the wrapper class:
public function run($sql, $bind="") {
$this->sql = trim($sql);
$this->bind = $this->cleanup($bind);
$this->error = "";
try {
$pdostmt = $this->prepare($this->sql);
if($pdostmt->execute($this->bind) !== false) {
if(preg_match("/^(" . implode("|", array("select", "describe", "pragma")) . ") /i", $this->sql))
return $pdostmt->fetchall(PDO::FETCH_OBJ);
elseif(preg_match("/^(" . implode("|", array("delete", "insert", "update")) . ") /i", $this->sql))
return $pdostmt->rowCount();
}
} catch (PDOException $e) {
$this->error = $e->getMessage();
$this->debug();
return false;
}
}
DON'T TRY to automate everything
The less magic in your code, the easier support and less painful troubles.
Don't try to stuff all the logic into one single method. It's a class! You can create as many methods as you need.
When you need rowCount() - select it explicitly! It's not that hard.
But when you stumble upon this code after couple months, you will know what does this value mean.
When you need single row - use a method to get a single row.
When you need many rows - use a method to get many rows.
It is simple and extremely unambiguous!
When you turn back to your code after 2 months, you will have absolutely no idea, what did you expected. So - always write it explicitly.
Here is an excerpt from my mysqli wrapper class to give you an idea:
public function query()
{
return $this->rawQuery($this->prepareQuery(func_get_args()));
}
/**
* Helper function to get scalar value right out of query and optional arguments
*
* Examples:
* $name = $db->getOne("SELECT name FROM table WHERE id=1");
* $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return string|FALSE either first column of the first row of resultset or FALSE if none found
*/
public function getOne()
{
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query))
{
$row = $this->fetch($res);
if (is_array($row)) {
return reset($row);
}
$this->free($res);
}
return FALSE;
}
/**
* Helper function to get single row right out of query and optional arguments
*
* Examples:
* $data = $db->getRow("SELECT * FROM table WHERE id=1");
* $data = $db->getOne("SELECT * FROM table WHERE id=?i", $id);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array|FALSE either associative array contains first row of resultset or FALSE if none found
*/
public function getRow()
{
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query)) {
$ret = $this->fetch($res);
$this->free($res);
return $ret;
}
return FALSE;
}
/**
* Helper function to get single column right out of query and optional arguments
*
* Examples:
* $ids = $db->getCol("SELECT id FROM table WHERE cat=1");
* $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array|FALSE either enumerated array of first fields of all rows of resultset or FALSE if none found
*/
public function getCol()
{
$ret = array();
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
{
while($row = $this->fetch($res))
{
$ret[] = reset($row);
}
$this->free($res);
}
return $ret;
}
/**
* Helper function to get all the rows of resultset right out of query and optional arguments
*
* Examples:
* $data = $db->getAll("SELECT * FROM table");
* $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows);
*
* #param string $query - an SQL query with placeholders
* #param mixed $arg,... unlimited number of arguments to match placeholders in the query
* #return array enumerated 2d array contains the resultset. Empty if no rows found.
*/
public function getAll()
{
$ret = array();
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
{
while($row = $this->fetch($res))
{
$ret[] = $row;
}
$this->free($res);
}
return $ret;
}
Look - from the function name you can always tell which result to expect:
$name = $db->getOne('SELECT name FROM table WHERE id = ?i',$_GET['id']);
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
Don't be fooled by such a pitfall like number of returned rows.
There could be honest one row in the resultset which you intend to populate with fetchAll. So, it will return single-dimensional array instead of multi-dimensional and you will have plenty of video effects on your page
Since you didn't mark an answer as accepted. I thought I'd answer you question. I found this while looking for the answer myself. I agree with "Your Common Sense" in that they should be two separate functions. However, in direct answer to your question, this is what I had (PDO example rather than mysqli):
function select($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($qry->rowCount() > 1){
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetchAll(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetchAll(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetchAll(PDO::FETCH_ASSOC);
}
}
else{
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetch(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetch(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetch(PDO::FETCH_ASSOC);
}
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}
However I found that if I queried all rows in a table but there was only one row in the table it would return a 1-d array instead of a 2-d array. My code to handle the result wouldn't work for both types of arrays. I could handle that each time but I found it just easier to, as stated above, separate them into different functions so if I knew there would be only one answer I could call the appropriate function. This is what I have now:
function select($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetch(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetch(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetch(PDO::FETCH_ASSOC);
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}
function selectAll($sql,$params=NULL,$fetchType=NULL){
try{
$qry = $this->db->prepare($sql);
$qry->execute($params);
if($fetchType == 'OBJ'){//returns object
$results = $qry->fetchAll(PDO::FETCH_OBJ);
}elseif($fetchType == 'NUM'){//-numerical array
$results = $qry->fetchAll(PDO::FETCH_NUM);
}else{//default - associative array
$results = $qry->fetchAll(PDO::FETCH_ASSOC);
}
if($results){
return $results;
}else{
return NULL;
}
}
catch(PDOException $err){
$this->logError($err);
}
}

Categories