diff --git a/src/Storm/Event/Abstract.php b/src/Storm/Event/Abstract.php index d25da741915d580396e1045db3238df0d2ba504d..2c9947cd1fb2395d7afa0916c384e39351cd9412 100644 --- a/src/Storm/Event/Abstract.php +++ b/src/Storm/Event/Abstract.php @@ -26,24 +26,23 @@ THE SOFTWARE. abstract class Storm_Event_Abstract { - protected $_model; - public function __construct($model) { - $this->_model = $model; + public function isSaveEvent() { + return false; } - public function getModel() { - return $this->_model; + public function isDeleteEvent() { + return false; } - public function isSaveEvent() { + public function isQueryEvent() { return false; } - public function isDeleteEvent() { - return false; + public function getModel() { + return null; } } diff --git a/src/Storm/Event/Delete.php b/src/Storm/Event/Delete.php index 50e7a606849a6c57ec83152102695db49481ba78..b7cd54fb7198000ff031a0a5570470aceb7a0b57 100644 --- a/src/Storm/Event/Delete.php +++ b/src/Storm/Event/Delete.php @@ -25,7 +25,8 @@ THE SOFTWARE. */ -class Storm_Event_Delete extends Storm_Event_Abstract { +class Storm_Event_Delete extends Storm_Event_Model { + public function isDeleteEvent() { return true; } diff --git a/src/Storm/Event/Model.php b/src/Storm/Event/Model.php new file mode 100644 index 0000000000000000000000000000000000000000..65a3df0997dad99367dc5758c2687942acb6a052 --- /dev/null +++ b/src/Storm/Event/Model.php @@ -0,0 +1,40 @@ +<?php +/* +STORM is under the MIT License (MIT) + +Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + + +class Storm_Event_Model extends Storm_Event_Abstract { + + protected $_model; + + public function __construct($model) { + $this->_model = $model; + } + + + public function getModel() { + return $this->_model; + } +} diff --git a/src/Storm/Event/Save.php b/src/Storm/Event/Save.php index 87a0f413f23184c0452c4dd1ad1023cf0c8b16b7..66b5463878e768c893c1f84bfa41360d374f5e11 100644 --- a/src/Storm/Event/Save.php +++ b/src/Storm/Event/Save.php @@ -25,8 +25,9 @@ THE SOFTWARE. */ -class Storm_Event_Save extends Storm_Event_Abstract { +class Storm_Event_Save extends Storm_Event_Model { + public function isSaveEvent() { return true; } -} \ No newline at end of file +} diff --git a/src/Storm/Events.php b/src/Storm/Events.php index f497336bfc72e62536dc12a2cb6a88011b851be4..f37ba57b84c4d21b1fbe7f9885d1a0834d726676 100644 --- a/src/Storm/Events.php +++ b/src/Storm/Events.php @@ -84,10 +84,7 @@ class Storm_Events { } - /** - * @param $event Storm_Event - */ - public function notify($event) { + public function notify(Storm_Event_Abstract $event) : self { $this->_observers->eachDo(function($observer) use($event) { $observer($event); diff --git a/src/Storm/Join.php b/src/Storm/Join.php new file mode 100644 index 0000000000000000000000000000000000000000..f814d8b704b18a8bca1b3010508bf5426a1cdfa9 --- /dev/null +++ b/src/Storm/Join.php @@ -0,0 +1,75 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Join extends Storm_Join_Abstract { + use Storm_Query_Trait; + + public function __construct(Storm_Model_Loader $loader, ?string $alias = null) { + $this->_helper = new Storm_Query_Helper($loader, $alias); + $this->_criteria = (new Storm_Query_Criteria) + ->setAlias($this->_helper->getIdentifier()); + } + + + protected function _prepareHelper() : Storm_Query_Helper { + $helper = $this->getHelper(); + $count = $this->_countSelect(); + if (0 === $count) + $helper->allSelect(); + + if ($count > 1) + $this->_selectWithAlias(); + + $helper->beRow(); + + return $helper; + } + + + /** DB management */ + + public function getAssembleSelect() : Zend_Db_Table_Select { + $select = parent::getAssembleSelect(); + $this->_assembleGroupBy($select); + $this->_assembleOrders($select); + + if ($this->_distinct) + $select->distinct(); + + return $select; + } + + + /** Volatile management */ + + public function computeErrors() : array { + foreach ($this->getJoins() as $join) + $this->_errors = [...$this->_errors, ...$join->computeErrors()]; + + return parent::computeErrors(); + } +} diff --git a/src/Storm/Join/Abstract.php b/src/Storm/Join/Abstract.php new file mode 100644 index 0000000000000000000000000000000000000000..7ba9ca43c9972ebfd361936f663621e1bcee5c3c --- /dev/null +++ b/src/Storm/Join/Abstract.php @@ -0,0 +1,132 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +abstract class Storm_Join_Abstract extends Storm_Query_Abstract { + + protected array $_joins = []; + + public function getJoins() : array { + return $this->_joins; + } + + + public function inner(Storm_Join_Inner $inner) : self { + $this->_joins [] = $inner->setParentHelper($this->getHelper()); + + return $this; + } + + + public function left(Storm_Join_Left $left) : self { + $this->_joins [] = $left->setParentHelper($this->getHelper()); + + return $this; + } + + + public function right(Storm_Join_Right $right) : self { + $this->_joins [] = $right->setParentHelper($this->getHelper()); + + return $this; + } + + + protected function _countSelect() : int { + $count = 0; + + if ($this->getHelper()->hasSelect()) + $count++; + + foreach ($this->getJoins() as $join) + $count += $join->_countSelect(); + + return $count; + } + + + protected function _selectWithAlias() : self { + $this->getHelper()->beWithAlias(); + + foreach ($this->getJoins() as $join) + $join->_selectWithAlias(); + + return $this; + } + + + /** DB management */ + + public function getAssembleSelect() : Zend_Db_Table_Select { + $select = parent::getAssembleSelect(); + + $this->assembleJoins($select); + + return $select; + } + + + protected function _assembleFrom(Zend_Db_Table_Select $select) : self { + $select->setIntegrityCheck(false); + + return parent::_assembleFrom($select); + } + + + public function assembleJoins(Zend_Db_Table_Select $select) : self { + $this->_assembleJoin($select); + + foreach ($this->getJoins() as $join) + $join->assembleJoins($select); + + return $this; + } + + + protected function _assembleJoin(Zend_Db_Table_Select $select) : self { + return $this; + } + + + /** Volatile management */ + + public function instances(?array $instances = []) : array { + $instances = parent::instances($instances); + + foreach ($this->getJoins() as $join) + $instances = $join->instances($instances); + + return $instances; + } + + + public function filterWith(Storm_Query_Instances $query_instances) : self { + foreach ($this->getJoins() as $join) + $join->filterWith($query_instances); + + return parent::filterWith($query_instances); + } +} diff --git a/src/Storm/Join/Inner.php b/src/Storm/Join/Inner.php new file mode 100644 index 0000000000000000000000000000000000000000..a5daf6065e6045093fdae81f9ca62135d10c773d --- /dev/null +++ b/src/Storm/Join/Inner.php @@ -0,0 +1,47 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Join_Inner extends Storm_Join_Sub { + + public function computeErrors() : array { + $errors = []; + + if ( ! $this->_condition) + $errors [] = 'Error: For Inner condition "on" is mandatory'; + + return [...$errors, ...parent::computeErrors()]; + } + + + protected function _assembleJoin(Zend_Db_Table_Select $select) : self { + $select->join($this->getHelper()->table(), + $this->_joinConditions(), + $this->getHelper()->select()); + + return $this; + } +} diff --git a/src/Storm/Join/Left.php b/src/Storm/Join/Left.php new file mode 100644 index 0000000000000000000000000000000000000000..39edebc15838084821b619665b896209b759708d --- /dev/null +++ b/src/Storm/Join/Left.php @@ -0,0 +1,47 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Join_Left extends Storm_Join_Sub { + + public function computeErrors() : array { + $errors = []; + + if ( ! $this->_condition) + $errors [] = 'Error: For Left condition "on" is mandatory'; + + return [...$errors, ...parent::computeErrors()]; + } + + + protected function _assembleJoin(Zend_Db_Table_Select $select) : self { + $select->joinLeft($this->getHelper()->table(), + $this->_joinConditions(), + $this->getHelper()->select()); + + return $this; + } +} diff --git a/src/Storm/Join/Right.php b/src/Storm/Join/Right.php new file mode 100644 index 0000000000000000000000000000000000000000..25bb4515ba932d370a43d2d06d01cba76b19a171 --- /dev/null +++ b/src/Storm/Join/Right.php @@ -0,0 +1,47 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Join_Right extends Storm_Join_Sub { + + public function computeErrors() : array { + $errors = []; + + if ( ! $this->_condition) + $errors [] = 'Error: For Right condition "on" is mandatory'; + + return [...$errors, ...parent::computeErrors()]; + } + + + protected function _assembleJoin(Zend_Db_Table_Select $select) : self { + $select->joinRight($this->getHelper()->table(), + $this->_joinConditions(), + $this->getHelper()->select()); + + return $this; + } +} diff --git a/src/Storm/Join/Sub.php b/src/Storm/Join/Sub.php new file mode 100644 index 0000000000000000000000000000000000000000..5d00a9c220ed041896a1430fc6d2737956cf6e0d --- /dev/null +++ b/src/Storm/Join/Sub.php @@ -0,0 +1,142 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Join_Sub extends Storm_Join_Abstract implements Storm_Query_SubInterface { + + protected ?Storm_Query_Helper $_parent_helper = null; + protected ?Storm_Query_Condition $_condition = null; + + public function __construct(Storm_Model_Loader $loader, ?string $alias = null) { + $this->_helper = new Storm_Query_Helper($loader, $alias); + $this->_criteria = (new Storm_Query_Criteria) + ->setAlias($this->_helper->getIdentifier()); + } + + + public function setParentHelper(Storm_Query_Helper $parent_helper) : self { + $this->_parent_helper = $parent_helper; + + return $this; + } + + + public function getParentHelper() : ?Storm_Query_Helper { + return $this->_parent_helper; + } + + + public function on_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)); + + return $this; + } + + + public function on_not_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beNotEqual()); + + return $this; + } + + + public function on_gt(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beGreater()); + + return $this; + } + + + public function on_gt_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beGreaterEqual()); + + return $this; + } + + + public function on_lt(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beLesser()); + + return $this; + } + + + public function on_lt_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beLesserEqual()); + + return $this; + } + + + public function filterWith(Storm_Query_Instances $query_instances) : self { + if ($this->_condition) + $this->_condition->filterWith($query_instances); + + return parent::filterWith($query_instances); + } + + + public function computeErrors() : array { + $errors = []; + + foreach ($this->getJoins() as $join) + $errors = [...$errors, ...$join->computeErrors()]; + + foreach ($this->getSubQueries() as $sub) + $errors = [...$errors, ...$sub->computeErrors()]; + + return $errors; + } + + + protected function _joinConditions() : string { + $join_conditions = $this->_condition + ? $this->_condition->sql() + : []; + + $join_conditions [] = $this->_sqlWhere(); + + return implode($this->_criteria->separator(), array_filter($join_conditions)); + } + + + protected function _addCondition(Storm_Query_Condition $condition) : self { + if ($this->_condition) { + $this->_condition->add($condition); + + return $this; + } + + $this->_condition = $condition; + + return $this; + } +} diff --git a/src/Storm/Model/Abstract.php b/src/Storm/Model/Abstract.php index c742e97b1eba7c3fe34669fe56d94be4efaf3a92..f1fac3fdd55d91a3515ac362eec7fb6facfc5ea8 100644 --- a/src/Storm/Model/Abstract.php +++ b/src/Storm/Model/Abstract.php @@ -1,30 +1,31 @@ <?php /* -STORM is under the MIT License (MIT) + STORM is under the MIT License (MIT) -Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr + Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. */ -abstract class Storm_Model_Abstract { +abstract class Storm_Model_Abstract implements Storm_Model_Interface { + /** * @var array */ @@ -130,7 +131,6 @@ abstract class Storm_Model_Abstract { */ protected $_errors = array(); - /** * Default values for attributes of a new instance * Should be defined in subclasses like: @@ -139,7 +139,6 @@ abstract class Storm_Model_Abstract { */ protected $_default_attribute_values = array(); - /** * By default, use autoincrement ids generated by the SGBD. When $_fixed_id = true, * does not take id from SGBD. @@ -147,7 +146,6 @@ abstract class Storm_Model_Abstract { */ protected $_fixed_id = false; - /** * Generic mean to handle associations (in order to extent and replace has_many and belongs_to) * Collection of Storm_Model_Association_Abstract subclasses @@ -155,35 +153,29 @@ abstract class Storm_Model_Abstract { */ protected $_associations; - - /** - * @param string $class - * @return Storm_Model_Loader - */ - protected static function _buildLoaderFor($class) { + protected static function _buildLoaderFor(string $class) : Storm_Model_Loader { $reflection = new ReflectionClass($class); $class_vars = $reflection->getdefaultProperties(); if (isset($class_vars['_loader_class'])) { $loader_class = $class_vars['_loader_class']; - return new $loader_class($class); + return new $loader_class($class); } return new Storm_Model_Loader($class); } - public static function getClassVar($var) { + + public static function getClassVar(string $var) { $reflection = new ReflectionClass(get_called_class()); $class_vars = $reflection->getdefaultProperties(); return $class_vars[$var]; } - /** - * @return Storm_Model_Loader - */ + public static function getLoader() { return self::getLoaderFor(get_called_class()); } @@ -203,22 +195,14 @@ abstract class Storm_Model_Abstract { } - /** - * @param string $class - * @return Storm_Model_Loader - */ - public static function getLoaderFor($class) { + public static function getLoaderFor(string $class) { return isset(self::$_loaders[$class]) ? self::$_loaders[$class] : self::setLoaderFor($class, self::_buildLoaderFor($class)); } - /** - * @param string $class - * @param Storm_Model_Loader $loader - */ - public static function setLoaderFor($class, $loader) { + public static function setLoaderFor(string $class, $loader) { return self::$_loaders[$class] = $loader; } @@ -228,7 +212,7 @@ abstract class Storm_Model_Abstract { } - public static function getLoaders() { + public static function getLoaders() : array { return self::$_loaders; } @@ -254,9 +238,9 @@ abstract class Storm_Model_Abstract { * Put the instance in its Loader's cache * @return Storm_Model_Abstract */ - public function cache() { static::getLoader()->cacheInstance($this); + return $this; } @@ -267,6 +251,7 @@ abstract class Storm_Model_Abstract { */ public function save($force_primary_key = false) { $this->_updateNullBelongsToIdFieldsFromDependents(); + if ($valid = $this->isValid()) { $this->saveWithoutValidation($force_primary_key); $this->_getEvents()->notify(new Storm_Event_Save($this)); @@ -285,10 +270,12 @@ abstract class Storm_Model_Abstract { public function assertSave($force_primary_key = false) { if (!$this->save($force_primary_key)) throw new Storm_Model_Exception('Can\'t save '.get_class($this).': '.implode(',',$this->getErrors())); + return true; } - public function getClassName() { + + public function getClassName() : string { return get_class($this); } @@ -313,23 +300,30 @@ abstract class Storm_Model_Abstract { protected function _updateNullBelongsToIdFieldsFromDependents() { $this->_belongs_to_attributes = array_filter($this->_belongs_to_attributes); + foreach ($this->_belongs_to_attributes as $field => $dependent) { $id_field = $this->getIdFieldForDependent($field); + if (null === $this->_get($id_field)) $this->_set($id_field, $dependent->getId()); } + return $this; } public function beforeSave() {} + public function afterSave() {} + public function beforeDelete() {} + public function afterDelete() {} + /** * Is this model valid for saving * @@ -357,7 +351,6 @@ abstract class Storm_Model_Abstract { $this->validate(); return !$this->hasErrors(); - } @@ -379,6 +372,7 @@ abstract class Storm_Model_Abstract { } + /** * @param string $attribute * @param string $error @@ -401,9 +395,9 @@ abstract class Storm_Model_Abstract { * @param string $error */ public function check($condition, $error) { - if (!$condition) { + if (!$condition) $this->addError($error); - } + return $this; } @@ -414,9 +408,9 @@ abstract class Storm_Model_Abstract { * @param string $error */ public function checkAttribute($attribute, $condition, $error) { - if (!$condition) { + if (!$condition) $this->addAttributeError($attribute, $error); - } + return $this; } @@ -448,10 +442,8 @@ abstract class Storm_Model_Abstract { * * Used by Loader while saving in order to build the * SQL query. - * - * @return array */ - public function attributesToArray() { + public function attributesToArray() : array { $attributes = array(); $all_attributes = array_merge($this->_default_attribute_values, @@ -462,35 +454,22 @@ abstract class Storm_Model_Abstract { if (method_exists($this, $method)) { $attributes[$name] = $this->$method(); - } else { $attributes[$name] = $value; } } - - return $attributes; } - /** - * Return field $_attributes - * - * @return array - */ - public function getRawAttributes() { + public function getRawAttributes() : array { return array_merge($this->_default_attribute_values, $this->_attributes); } - /** - * Return field $_attributes_in_db - * - * @return array - */ - public function getRawDbAttributes() { + public function getRawDbAttributes() : array { return $this->_attributes_in_db; } @@ -501,10 +480,8 @@ abstract class Storm_Model_Abstract { * * This method may be redefined in subclasses * in order to provide some coupling with Zend_Form::populate() - * - * @return array */ - public function toArray() { + public function toArray() : array { return $this->attributesToArray(); } @@ -536,10 +513,8 @@ abstract class Storm_Model_Abstract { * @return Storm_Model_Abstract */ public function setId($id) { - if ($this->_table_primary != null) { + if ($this->_table_primary != null) $this->_set(strtolower($this->_table_primary), $id); - } - //return $this->_set('id', $id); //perfs $this->_attributes['id'] = $id; @@ -552,29 +527,18 @@ abstract class Storm_Model_Abstract { * @return string */ public function getIdFieldForDependent($field) { - if ( - array_key_exists($field, $this->_belongs_to) - && (array_key_exists('referenced_in', $this->_belongs_to[$field])) - ) { + if (array_key_exists($field, $this->_belongs_to) + && (array_key_exists('referenced_in', $this->_belongs_to[$field]))) return $this->_belongs_to[$field]['referenced_in']; - } - return $field . '_id'; - } - /** - * @param string $attribute - * @return bool - */ - public function isAttributeEmpty($attribute) { - if (!array_key_exists($attribute, $this->_attributes)) { + public function isAttributeEmpty(string $attribute) : bool { + if (!array_key_exists($attribute, $this->_attributes)) return true; - } - $value = $this->_get($attribute); return empty($value); } @@ -582,13 +546,11 @@ abstract class Storm_Model_Abstract { protected function _deleteDependents() { foreach ($this->hasManyRelationships() as $field => $relation) { - if (!array_key_exists('dependents', $relation)) { + if (!array_key_exists('dependents', $relation)) continue; - } - if ($relation['dependents'] != 'delete') { + if ($relation['dependents'] != 'delete') continue; - } $dependents = $this->_getDependents($field); @@ -605,6 +567,7 @@ abstract class Storm_Model_Abstract { protected function _saveDependencies() { foreach (array_keys($this->_has_many_attributes) as $field) $this->_saveDependents($field); + $this->_associations->save($this); } @@ -614,28 +577,22 @@ abstract class Storm_Model_Abstract { * @return Storm_Model_Abstract */ protected function _saveDependents($field) { - if (array_key_exists('through', $this->descriptionOf($field))) { + if (array_key_exists('through', $this->descriptionOf($field))) return $this; - } - if (!array_key_exists($field, $this->_has_many_attributes_in_db)) { + if (!array_key_exists($field, $this->_has_many_attributes_in_db)) $this->_has_many_attributes_in_db[$field] = $this->_getDependentsFromLoader($field); - } $has_many_attributes = (array)$this->_has_many_attributes[$field]; $has_many_attributes_in_db = (array)$this->_has_many_attributes_in_db[$field]; - $dependents_to_delete = array_diff( - $has_many_attributes_in_db, - $has_many_attributes - ); + $dependents_to_delete = array_diff($has_many_attributes_in_db, + $has_many_attributes); - foreach ($dependents_to_delete as $dependent) { + foreach ($dependents_to_delete as $dependent) $dependent->delete(); - } - foreach($this->_has_many_attributes[$field] as $dependent) { + foreach($this->_has_many_attributes[$field] as $dependent) $dependent->save(); - } $this->_has_many_attributes_in_db[$field] = $this->_has_many_attributes[$field]; return $this; @@ -664,12 +621,12 @@ abstract class Storm_Model_Abstract { } - protected function _methodMatch($method) { - foreach(static::$_method_prefixes as $prefix => $length) { + protected function _methodMatch(string $method) : ?array { + foreach(static::$_method_prefixes as $prefix => $length) if (substr($method, 0, $length) == $prefix) return [1 => $prefix, 2 => substr($method, $length)]; - } + return null; } @@ -680,16 +637,11 @@ abstract class Storm_Model_Abstract { * ex: * $car->perform('getColor') * $car->perform('setColor', ['red']) - * - * @param string $method - * @param array $args - * @return Storm_Model_Abstract - * @throws Exception */ - public function perform($method, $args = []) { - if (!$matches = $this->_methodMatch($method)) { - throw new Storm_Model_Exception('Tried to call unknown method '.get_class($this).'::'.$method); - } + public function perform(string $method, array $args = []) { + if (!$matches = $this->_methodMatch($method)) + throw new Storm_Model_Exception('Tried to call unknown method ' + . get_class($this) . '::' . $method); $attribute = $this->_accessorToAttributeName($matches[2]); @@ -716,15 +668,13 @@ abstract class Storm_Model_Abstract { } return $this; - } /** - * @param string $field - * @return array + * @return mixed */ - public function descriptionOf($field) { + public function descriptionOf(string $field) { if (method_exists($this, $method = 'descriptionOf'.$field)) return $this->$method(); @@ -740,8 +690,10 @@ abstract class Storm_Model_Abstract { public function hasManyRelationships() { $relations = $this->_has_many; + foreach (array_keys($this->_has_many) as $field) $relations[$field] = $this->descriptionOf($field); + return $relations; } @@ -754,6 +706,7 @@ abstract class Storm_Model_Abstract { */ public function _has($attribute) { $dependent = $this->callGetterByAttributeName($attribute); + return !empty($dependent); } @@ -802,21 +755,18 @@ abstract class Storm_Model_Abstract { return $dependent->_numberOf($this->_getDependentFieldNameForInstance($field, $dependent)); }, $dependents)); - } /** * Update with a [name] => value formatted array - * - * @param Array $datas - * @return Storm_Model_Abstract */ - public function updateAttributes(Array $datas) { + public function updateAttributes(array $datas) : self { foreach($datas as $name => $value) { $method = 'set'.$this->attributeNameToAccessor($name); $this->$method($value); } + return $this; } @@ -828,13 +778,11 @@ abstract class Storm_Model_Abstract { * setAttributeName(...) magic. * * So we are sure that attributes are exactly those from db. - *n - * @param Array $datas - * @return Storm_Model_Abstract */ - public function initializeAttributes($datas) { + public function initializeAttributes(array $datas) : self { $this->_attributes_in_db = $this->_attributes = array_merge($this->_attributes, - array_change_key_case($datas)); + array_change_key_case($datas)); + return $this; } @@ -844,9 +792,9 @@ abstract class Storm_Model_Abstract { * @return boolean true when attribute has changed from initial value in db */ public function hasChangedAttribute($name) { - if ($this->isNew() || - null === $name || - !isset($this->_attributes[$name])) + if ($this->isNew() + || null === $name + || !isset($this->_attributes[$name])) return false; if (!isset($this->_attributes_in_db[$name])) @@ -857,45 +805,36 @@ abstract class Storm_Model_Abstract { public function hasChange() { - foreach(array_keys($this->toArray()) as $name) { - if($this->hasChangedAttribute($name)) - return true; - } + foreach(array_keys($this->toArray()) as $name) + if ($this->hasChangedAttribute($name)) + return true; + return false; } /** * UserId -> user_id - * - * @param string $accessor - * @return string */ - protected function _accessorToAttributeName($accessor) { + protected function _accessorToAttributeName(string $accessor) : string { return Storm_Inflector::underscorize($accessor); } + /** * user_id -> UserId - * - * @param string $accessor - * @return string */ - public function attributeNameToAccessor($name) { + public function attributeNameToAccessor(string $name) : string { return Storm_Inflector::camelize($name); - } - /** - * @return boolean true if attribute exists - */ - public function isAttributeExists($field) { + public function isAttributeExists(string $field) : bool { return - array_key_exists($field, $this->_attributes) or - array_key_exists($field, $this->_has_many) or - array_key_exists($field, $this->_belongs_to) or - $this->hasDefaultValueForAttribute($field); + array_key_exists($field, $this->_attributes) + || array_key_exists($field, $this->_has_many) + || array_key_exists($field, $this->_belongs_to) + || $this->hasDefaultValueForAttribute($field); } @@ -906,31 +845,25 @@ abstract class Storm_Model_Abstract { /** * Return the value of attribute $field. If cannot find it, raise Exception. - * @param string $field * @return mixed * @throws Storm_Model_Exception */ - protected function _get($field) { - if (isset($this->_attributes[$field]) || array_key_exists($field, $this->_attributes)) { + protected function _get(string $field) { + if (isset($this->_attributes[$field]) || array_key_exists($field, $this->_attributes)) return $this->_attributes[$field]; - } - if (isset($this->_has_many[$field]) || array_key_exists($field, $this->_has_many)) { + if (isset($this->_has_many[$field]) || array_key_exists($field, $this->_has_many)) return $this->_getDependents($field); - } - if (isset($this->_belongs_to[$field]) || array_key_exists($field, $this->_belongs_to)) { + if (isset($this->_belongs_to[$field]) || array_key_exists($field, $this->_belongs_to)) return $this->_getDependent($field); - } - if ($this->hasDefaultValueForAttribute($field)) { - return $this->getDefaultValueForAttribute($field); - } + if ($this->hasDefaultValueForAttribute($field)) + return $this->getDefaultValueForAttribute($field); - throw new Storm_Model_Exception( - sprintf('Tried to call unknown method %s::get%s', - get_class($this), - $this->attributeNameToAccessor($field))); + throw new Storm_Model_Exception(sprintf('Tried to call unknown method %s::get%s', + get_class($this), + $this->attributeNameToAccessor($field))); } @@ -951,8 +884,8 @@ abstract class Storm_Model_Abstract { * @return mixed the value of attribute or null if does not exist. */ public function __get($field) { - if (array_key_exists($key = strtolower($field), $this->_attributes) || - array_key_exists($key = strtoupper($field), $this->_attributes)) + if (array_key_exists($key = strtolower($field), $this->_attributes) + || array_key_exists($key = strtoupper($field), $this->_attributes)) return $this->_attributes[$key]; return null; @@ -986,12 +919,11 @@ abstract class Storm_Model_Abstract { */ public function __set($field, $value) { $method = 'set'.strtolower($field); - return $this->$method($value); + return $this->$method($value); } - /** * If has_many relationship specifies unique => true and $value in $dependents, * then the constraint is violated. @@ -1002,17 +934,17 @@ abstract class Storm_Model_Abstract { * @return boolean */ protected function _isUniqueConstraintViolated($relationship, $dependents, $value) { - $is_unique = (array_key_exists('unique', $relationship) and (true === $relationship['unique'])); + $is_unique = (array_key_exists('unique', $relationship) + && (true === $relationship['unique'])); if (!$is_unique) return false; if ($value->isNew()) return false; - foreach($dependents as $dependent) { + foreach($dependents as $dependent) if ($dependent->getId() == $value->getId()) return true; - } return false; } @@ -1024,16 +956,14 @@ abstract class Storm_Model_Abstract { * @return Storm_Model_Abstract */ protected function _addDependent($field, $value) { - if (!$value) { + if (!$value) return $this; - } if (!$map = $this->descriptionOf($field)) throw new Storm_Model_Exception( - sprintf('Tried to call unknown method %s::add%s', - get_class($this), - $this->attributeNameToAccessor($this->_singularize($field)))); - + sprintf('Tried to call unknown method %s::add%s', + get_class($this), + $this->attributeNameToAccessor($this->_singularize($field)))); $dependents = $this->_get($field); if ($this->_isUniqueConstraintViolated($map, $dependents, $value)) @@ -1091,6 +1021,7 @@ abstract class Storm_Model_Abstract { if ($value->getId() == $other_value->getId()) return $dependent; } + return null; } @@ -1130,11 +1061,10 @@ abstract class Storm_Model_Abstract { $dependents = $this->_get($field); $dependents_without_value = array(); - foreach ($dependents as $dependent) { - if ( (!$value->isNew() && ($dependent->getId() != $value->getId())) || ($value !== $dependent)) { + foreach ($dependents as $dependent) + if ( (!$value->isNew() && ($dependent->getId() != $value->getId())) + || ($value !== $dependent)) $dependents_without_value []= $dependent; - } - } $this->_set($field, $dependents_without_value); return $this; @@ -1165,11 +1095,9 @@ abstract class Storm_Model_Abstract { */ public function _storeDependentsForField($dependents, $field) { // if not already done, set current data in db - if (!array_key_exists($field, $this->_has_many_attributes_in_db)) { + if (!array_key_exists($field, $this->_has_many_attributes_in_db)) $this->_has_many_attributes_in_db[$field] = $dependents; - } - $this->_has_many_attributes[$field] = $dependents; return $this; } @@ -1205,7 +1133,6 @@ abstract class Storm_Model_Abstract { $getManyMethod = 'get' . ucfirst($field); $getOneMethod = 'get' . ucfirst($singularized_field); - foreach ($instances as $instance) { if ($instance->hasBelongsToRelashionshipWith($singularized_field)) $dependents []= $instance->$getOneMethod(); @@ -1231,11 +1158,7 @@ abstract class Storm_Model_Abstract { } - /** - * @param Storm_Model_Abstract $model - * @return Storm_Model_Loader - */ - protected function _getLoaderForModel($model) { + protected function _getLoaderForModel(string $model) { return call_user_func(array($model, 'getLoader')); } @@ -1260,10 +1183,9 @@ abstract class Storm_Model_Abstract { if (isset($map['limit'])) $find_params['limit'] = $map['limit']; - if (isset($map['scope'])) { + if (isset($map['scope'])) foreach($map['scope'] as $scope_field => $scope_value) $find_params[$scope_field] = $scope_value; - } $dependents = $this ->_getLoaderForModel($map['model']) @@ -1291,20 +1213,19 @@ abstract class Storm_Model_Abstract { * @return Storm_Model_Abstract */ protected function _setDependents($field, $value) { - if (!$value) $value = []; + if (!$value) + $value = []; $map = $this->descriptionOf($field); if (isset($map['through'])) { $through_field = $map['through']; - if (isset($this->_belongs_to[$through_field])) { if ($dependent = $this->_getDependent($through_field)) return $dependent->callSetterByAttributeName($field, $value); return $this; } - $this->_setDependents($through_field, array()); foreach ($value as $item) @@ -1313,10 +1234,10 @@ abstract class Storm_Model_Abstract { return $this; } - $this->_has_many_attributes[$field] = $this->_wrapDependentsWithDefinedClass($field, $value); foreach($value as $item) $item->_set($map['role'], $this); + return $this; } @@ -1338,28 +1259,21 @@ abstract class Storm_Model_Abstract { } - /** - * @param string $attribute - */ - public function callGetterByAttributeName($attribute) { - return call_user_func(array($this, 'get'.$this->attributeNameToAccessor($attribute))); + public function callGetterByAttributeName(string $attribute) { + return call_user_func([$this, 'get' . $this->attributeNameToAccessor($attribute)]); } /** - * @param string $attribute * @param mixed $value */ - public function callSetterByAttributeName($attribute, $value) { - return call_user_func([$this, 'set'.$this->attributeNameToAccessor($attribute)], $value); + public function callSetterByAttributeName(string $attribute, $value) { + return call_user_func([$this, 'set' . $this->attributeNameToAccessor($attribute)], $value); } - /** - * @param string $attribute - */ - public function callAdderByAttributeName($attribute, $value) { - return call_user_func([$this, 'add'.$this->attributeNameToAccessor($this->_singularize($attribute))], $value); + public function callAdderByAttributeName(string $attribute, $value) { + return call_user_func([$this, 'add' . $this->attributeNameToAccessor($this->_singularize($attribute))], $value); } @@ -1380,7 +1294,6 @@ abstract class Storm_Model_Abstract { return null; } - $id = $this->_getDependentIdForField($field); // if the instance is in cache, returns it @@ -1391,24 +1304,16 @@ abstract class Storm_Model_Abstract { return $dependent; } - - if (null == $id) { + if (null == $id) return null; - } - // in runtime cache ? if (array_key_exists($field, $this->_belongs_to_attributes)) { - if (null == $dependent = $this->_belongs_to_attributes[$field]) { + if (null == $dependent = $this->_belongs_to_attributes[$field]) return null; - } - - if ($id == $dependent->getId()) { + if ($id == $dependent->getId()) return $dependent; - - } - } // otherwise in database ? @@ -1422,33 +1327,25 @@ abstract class Storm_Model_Abstract { } - /** - * @param string $str - * @return string - */ - protected function _singularize($str) { + protected function _singularize(string $str) : string { return Storm_Inflector::singularize($str); } - /** - * @param string $str - * @return string - */ - protected function _pluralize($str) { + + protected function _pluralize(string $str) : string { return Storm_Inflector::pluralize($str); } + /** * @param string $field * @param mixed $value * @return Storm_Model_Abstract */ protected function _set($field, $value) { - if (array_key_exists($field, $this->_has_many)) { + if (array_key_exists($field, $this->_has_many)) return $this->_setDependents($field, $value); - } - if (array_key_exists($field, $this->_belongs_to)) { $id_field = $this->getIdFieldForDependent($field); $this->_attributes[$id_field] = (null == $value) ? null : $value->getId(); @@ -1467,23 +1364,25 @@ abstract class Storm_Model_Abstract { public function acceptHierarchyVisitor($visitor) { - $my_name = get_class($this); + $my_name = get_class($this); foreach ([$this->_belongs_to, $this->_has_many] as $links) array_walk( - $links, - function($link, $link_name) use ($my_name, $visitor) { - if (!isset($link['model'])) - return; - $visitor->visit(self::getLoaderFor($link['model'])->newInstance(), - $my_name, $link_name); - }); + $links, + function($link, $link_name) use ($my_name, $visitor) { + if (!isset($link['model'])) + return; + $visitor->visit(self::getLoaderFor($link['model'])->newInstance(), + $my_name, $link_name); + }); + return $this; } public function dotGraph() { - $visitor = new Storm_Model_HierarchyVisitor(); + $visitor = new Storm_Model_HierarchyVisitor(); $this->acceptHierarchyVisitor($visitor); + return $visitor->asDot(); } diff --git a/src/Storm/Model/BaseLoader.php b/src/Storm/Model/BaseLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..10472d5e3d4274ec9d2728ecbd96043e6a33cb21 --- /dev/null +++ b/src/Storm/Model/BaseLoader.php @@ -0,0 +1,146 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Model_BaseLoader { + + protected static $_default_strategy = 'Db'; + protected string $_model; + protected $_persistence_strategy; + + public function __construct(string $class) { + $this->_model = $class; + } + + + public function newFromRow(array $row) : Storm_Model_Interface { + $row = array_change_key_case($row, CASE_LOWER); + + return $this + ->newInstance() + ->initializeAttributes($row); + } + + + public function newInstance(?array $attributes = null) : Storm_Model_Interface { + $class = $this->_model; + $instance = new $class(); + + if ( ! empty($attributes)) + $instance->updateAttributes($attributes); + + return $instance; + } + + + public static function setDefaultStrategy(string $strategy) { + static::$_default_strategy = $strategy; + } + + + public static function defaultToVolatile() { + static::setDefaultStrategy('Volatile'); + } + + + public static function defaultToDb() { + static::setDefaultStrategy('Db'); + } + + + /** + * @param $tbl : Storm_Model_Table | Storm_Test_ObjectWrapper + */ + public function setTable($tbl) : self { + $this->getPersistenceStrategy()->setTable($tbl); + + return $this; + } + + + /** + * @return Storm_Model_Table | Storm_Test_ObjectWrapper + */ + public function getTable() { + return $this->getPersistenceStrategy()->getTable(); + } + + + public function getModel() : string { + return $this->_model; + } + + + /** + * @return Storm_Model_PersistenceStrategy_Db | Storm_Model_PersistenceStrategy_Volatile + */ + public function getPersistenceStrategy() { + if (isset($this->_persistence_strategy)) + return $this->_persistence_strategy; + + $class_name = 'Storm_Model_PersistenceStrategy_' . static::$_default_strategy; + return $this->_persistence_strategy = new $class_name($this); + } + + + public function beVolatile() : self { + if ( ! $this->getPersistenceStrategy()->isVolatile()) + $this->_persistence_strategy = new Storm_Model_PersistenceStrategy_Volatile($this); + + return $this; + } + + + public function isVolatile() : bool { + return $this->getPersistenceStrategy()->isVolatile(); + } + + + public function fetchForQuery(Storm_Query_Abstract $query) : array { + return $this->getPersistenceStrategy()->fetchForQuery($query); + } + + + public function countForQuery(Storm_Query_Abstract $query) : int { + return $this->getPersistenceStrategy()->countForQuery($query); + } + + + public function fetchFirstForQuery(Storm_Query_Abstract $query) : ?Storm_Model_Abstract { + return $this->_findFirstFrom($this->fetchForQuery($query->limit(1))); + } + + + protected function _findFirstFrom(array $instances) : ?Storm_Model_Abstract { + if (0 === count($instances)) + return null; + + $first_instance = reset($instances); + $this->cacheInstance($first_instance); + + return $first_instance; + } +} diff --git a/src/Storm/Model/Interface.php b/src/Storm/Model/Interface.php new file mode 100644 index 0000000000000000000000000000000000000000..4a3115fd97c8144a989cf7e0f735916663980a81 --- /dev/null +++ b/src/Storm/Model/Interface.php @@ -0,0 +1,130 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +interface Storm_Model_Interface { + + /** + * Create compatibility with accessor like $miles->instrument. + * ex: + * $miles->setInstrument("trumpet"); + * assert("trumpet" === $miles->getTrumpet()) + * assert("trumpet" === $miles->trumpet) + * + * If attribute not defined, returns null for compatibility purpose + * with legacy code - Remember STORM is intended to get + * easily into messy crappy code. Sorry. + * + * assert(null === $miles->zork) + * + * @param string $field name of the attribute / field + * @return mixed the value of attribute or null if does not exist. + */ + public function __get($field); + + + public static function getClassVar(string $var); + + + public static function getLoader(); + + + public static function __callStatic($name, $args); + + + public static function getLoaderFor(string $class); + + + public static function setLoaderFor(string $class, $loader); + + + public static function unsetLoaders(); + + + public static function getLoaders() : array; + + + public function getClassName() : string; + + + /** + * Return an associative array with attribute + * name as $key and its value. + * + * Used by Loader while saving in order to build the + * SQL query. + */ + public function attributesToArray() : array; + + + public function getRawAttributes() : array; + + + /** + * Return an associative array with attribute + * name as $key and its value. + * + * This method may be redefined in subclasses + * in order to provide some coupling with Zend_Form::populate() + */ + public function toArray() : array; + + + public function isAttributeEmpty(string $attribute) : bool; + + + /** + * See __call + * + * ex: + * $car->perform('getColor') + * $car->perform('setColor', ['red']) + */ + public function perform(string $method, array $args = []); + + + /** + * Set initial attribute values with a [name] => value formatted array + * Used by Loader::newFromRow + * The difference with updateAttributes is that it doesn't call + * setAttributeName(...) magic. + * + * So we are sure that attributes are exactly those from db. + */ + public function initializeAttributes(array $datas) : self; + + + public function attributeNameToAccessor(string $name) : string; + + + public function isAttributeExists(string $field) : bool; + + + public function callGetterByAttributeName(string $attribute); + + + public function __toString(); +} diff --git a/src/Storm/Model/Loader.php b/src/Storm/Model/Loader.php index afda9e42e1dc8512a08045849f4e01deb8c3f882..899bc7fbc78ec91b3c384cb9d651d8b2bec051d9 100644 --- a/src/Storm/Model/Loader.php +++ b/src/Storm/Model/Loader.php @@ -1,26 +1,26 @@ <?php /* -STORM is under the MIT License (MIT) + STORM is under the MIT License (MIT) -Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr + Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. */ @@ -42,19 +42,13 @@ THE SOFTWARE. * } * } */ -class Storm_Model_Loader { +class Storm_Model_Loader extends Storm_Model_BaseLoader { + /** * @var Storm_Model_Loader_Cache */ protected static $_loader_cache; - protected static $_default_strategy = 'Db'; - - /** - * @var string - */ - protected $_model; - /** * @var Storm_Model_Table */ @@ -65,8 +59,6 @@ class Storm_Model_Loader { */ protected $_loaded_instances = array(); - protected $_persistence_strategy; - /** * @param $cache Zend_Cache */ @@ -75,21 +67,6 @@ class Storm_Model_Loader { } - public static function setDefaultStrategy($strategy) { - static::$_default_strategy = $strategy; - } - - - public static function defaultToVolatile() { - static::setDefaultStrategy('Volatile'); - } - - - public static function defaultToDb() { - static::setDefaultStrategy('Db'); - } - - public static function resetCache() { static::$_loader_cache = null; } @@ -102,32 +79,8 @@ class Storm_Model_Loader { $this->_model = $class; $this->_id_field = strtolower($class::getClassVar('_table_primary')); - if ($this->_id_field == null) { + if ($this->_id_field == null) $this->_id_field = 'id'; - } - - } - - /** - * @param Storm_Model_Table $tbl - * @return Storm_Model_Loader - */ - public function setTable($tbl) { - $this->getPersistenceStrategy()->setTable($tbl); - return $this; - - } - - /** - * @return Storm_Model_Table - */ - public function getTable() { - return $this->getPersistenceStrategy()->getTable(); - } - - - public function getModel() { - return $this->_model; } @@ -151,47 +104,25 @@ class Storm_Model_Loader { public function getLoaderCache() { if (!static::$_loader_cache) static::$_loader_cache = new Storm_Model_Loader_NullCache(); - return static::$_loader_cache; - } - - public function getPersistenceStrategy() { - if (isset($this->_persistence_strategy)) - return $this->_persistence_strategy; - - // Storm_Model_PersistenceStrategy_Db | Storm_Model_PersistenceStrategy_Volatile - $class_name = 'Storm_Model_PersistenceStrategy_' . static::$_default_strategy; - return $this->_persistence_strategy = new $class_name($this); + return static::$_loader_cache; } - public function beVolatile() { - if (!$this->getPersistenceStrategy()->isVolatile()) - $this->_persistence_strategy = new Storm_Model_PersistenceStrategy_Volatile($this); - return $this; - } - - public function isVolatile() { - return $this->getPersistenceStrategy()->isVolatile(); - } - /** * @param int $id * @return Storm_Model_Abstract */ public function find($id) { - if(null===$id){ + if (null === $id) return null; - } - if (isset($this->_loaded_instances[$id])) { + if (isset($this->_loaded_instances[$id])) return $this->_loaded_instances[$id]; - } if ($instance = $this->getLoaderCache()->load($this->_model, $id)) return $instance; - if ($instance = $this->getPersistenceStrategy()->find($id)) { $this->cacheInstance($instance); return $instance; @@ -210,6 +141,7 @@ class Storm_Model_Loader { public function cacheInstance($instance) { $this->_loaded_instances[$instance->getId()] = $instance; $this->getLoaderCache()->save($instance); + return $this; } @@ -224,23 +156,9 @@ class Storm_Model_Loader { * @return Storm_Model_Loader */ public function clearCache() { - $this->_loaded_instances=[]; - return $this; - } - + $this->_loaded_instances = []; - /** - * @param array $attributes default attributes values - * @return a new instance of my model - */ - public function newInstance($attributes = null) { - $class = $this->_model; - $instance = new $class(); - - if (!empty($attributes)) - $instance->updateAttributes($attributes); - - return $instance; + return $this; } @@ -251,7 +169,7 @@ class Storm_Model_Loader { * @return Storm_Model_Abstract */ public function newInstanceWithId($id, $attributes = null) { - $instance = $this + $instance = $this ->newInstance() ->setId($id); //id must be set BEFORE other attributes (they may depend on the id's value) @@ -267,18 +185,17 @@ class Storm_Model_Loader { public function newInstanceWithIdAssertSave($id, $attributes = null) { $instance = $this->newInstanceWithId($id,$attributes); $instance->assertSave(true); + return $instance; } + /** * Create an instance and assigns its * attributes using an associative array * (name_of_attribute => value) - * - * @param array $row - * @return Storm_Model_Abstract */ - public function newFromRow($row) { + public function newFromRow(array $row) : Storm_Model_Interface { $row = array_change_key_case($row, CASE_LOWER); $id = $row[$this->getIdField()]; unset($row[$this->getIdField()]); @@ -291,6 +208,7 @@ class Storm_Model_Loader { ->initializeAttributes($row); } + /** * Insert (if new) or update the record in DB * @@ -330,7 +248,7 @@ class Storm_Model_Loader { /** * @param Array */ - public function deleteBy($clauses, $page_size=100) { + public function deleteBy($clauses, $page_size = 100) { $done = 0; do { $clauses['limit'] = $page_size; @@ -360,8 +278,8 @@ class Storm_Model_Loader { */ public function getIdFieldForDependent($field) { $model_instance = new $this->_model; - return $model_instance->getIdFieldForDependent($field); + return $model_instance->getIdFieldForDependent($field); } @@ -463,19 +381,9 @@ class Storm_Model_Loader { } - protected function _findFirstFrom(array $instances) : ?Storm_Model_Abstract { - if (0 === count($instances)) - return null; - - $first_instance = reset($instances); - $this->cacheInstance($first_instance); - - return $first_instance; - } - - protected function _prepareFirstBy($args) { $args['limit'] = 1; + return $args; } @@ -485,17 +393,47 @@ class Storm_Model_Loader { } - public function fetchForQuery(Storm_Query $query) : array { - return $this->getPersistenceStrategy()->fetchForQuery($query); + public function query(?string $alias = null): Storm_Query { + return new Storm_Query($this, $alias); + } + + + public function orQuery(?string $alias = null): Storm_Query { + return static::query()->beOr(); + } + + + public function join(string $alias): Storm_Join { + return new Storm_Join($this, $alias); + } + + + public function orJoin(string $alias): Storm_Join { + return static::query()->beOr(); + } + + + public function inner(string $alias): Storm_Join_Inner { + return new Storm_Join_Inner($this, $alias); + } + + + public function left(string $alias): Storm_Join_Left { + return new Storm_Join_Left($this, $alias); + } + + + public function right(string $alias): Storm_Join_Right { + return new Storm_Join_Right($this, $alias); } - public function countForQuery(Storm_Query $query) : int { - return $this->getPersistenceStrategy()->countForQuery($query); + public function subIn(?string $alias = null): Storm_Query_In { + return new Storm_Query_In($this, $alias); } - public function fetchFirstForQuery(Storm_Query $query) : ?Storm_Model_Abstract { - return $this->_findFirstFrom($this->fetchForQuery($query->limit(1))); + public function subExists(?string $alias = null): Storm_Query_Exists { + return new Storm_Query_Exists($this, $alias); } } diff --git a/src/Storm/Model/PersistenceStrategy/Abstract.php b/src/Storm/Model/PersistenceStrategy/Abstract.php index e0d0395f876d1bf51c6558e514a01e4e85ffa912..28d4383d46e9d1835d952da950fdbc4de6d82a50 100644 --- a/src/Storm/Model/PersistenceStrategy/Abstract.php +++ b/src/Storm/Model/PersistenceStrategy/Abstract.php @@ -27,9 +27,10 @@ THE SOFTWARE. class Storm_Model_PersistenceStrategy_Abstract { - protected Storm_Model_Loader $_loader; + protected Storm_Model_BaseLoader $_loader; + protected $_table; - public function __construct(Storm_Model_Loader $loader) { + public function __construct(Storm_Model_BaseLoader $loader) { $this->_loader = $loader; } @@ -47,4 +48,22 @@ class Storm_Model_PersistenceStrategy_Abstract { public function updateAll(array $where, array $set_values) : int { return 0; } + + + public function getTable() { + if (!isset($this->_table)) { + $table_name = call_user_func_array([$this->_loader->getModel(), + 'getClassVar'], + ['_table_name']); + $this->_table = new Storm_Model_Table(['name' => $table_name]); + } + + return $this->_table; + } + + + public function setTable($tbl) { + $this->_table = $tbl; + return $this; + } } diff --git a/src/Storm/Model/PersistenceStrategy/Db.php b/src/Storm/Model/PersistenceStrategy/Db.php index bbc88d469e18b5689b0740e41e7b160673eb9c80..5fbf125fc867fbd6846f801f1472b0b5bc62fe54 100644 --- a/src/Storm/Model/PersistenceStrategy/Db.php +++ b/src/Storm/Model/PersistenceStrategy/Db.php @@ -28,29 +28,11 @@ THE SOFTWARE. class Storm_Model_PersistenceStrategy_Db extends Storm_Model_PersistenceStrategy_Abstract { - protected $_table; - public function newFromRow($row) { return $this->_loader->newFromRow($row); } - public function getTable() { - if (!isset($this->_table)) { - $table_name = call_user_func_array([$this->_loader->getModel(),'getClassVar'],['_table_name']); - $this->_table = new Storm_Model_Table(array('name' => $table_name)); - } - - return $this->_table; - } - - - public function setTable($tbl) { - $this->_table = $tbl; - return $this; - } - - public function find($id) { $rowset = $this->getTable()->find($id)->toArray(); @@ -103,7 +85,7 @@ class Storm_Model_PersistenceStrategy_Db ? $value : Storm_Query_Clause::newWith($key, $value); - return $clause->getFormatDb($this->getTable()); + return $clause->getFormatDb(); } @@ -113,19 +95,19 @@ class Storm_Model_PersistenceStrategy_Db } - public function fetchForQuery(Storm_Query $query) : array { + public function fetchForQuery(Storm_Query_Abstract $query) : array { return $this->findAll($this->_assembleQuery($query)); } - public function countForQuery(Storm_Query $query) : int { + public function countForQuery(Storm_Query_Abstract $query) : int { return $this->_count($this->_assembleQuery($query)); } - protected function _assembleQuery(Storm_Query $query) { - $select = $this->getTable()->select(); - $query->assemble($select); + protected function _assembleQuery(Storm_Query_Abstract $query) : Zend_Db_Table_Select { + $select = $query->getAssembleSelect(); + $this->setTable($select->getTable()); return $select; } diff --git a/src/Storm/Model/PersistenceStrategy/Volatile.php b/src/Storm/Model/PersistenceStrategy/Volatile.php index 260c04e95272dfb8c0de6d140bf29428332f613e..680dfb93fa4f9e10db975efabd91450abb2fb5ab 100644 --- a/src/Storm/Model/PersistenceStrategy/Volatile.php +++ b/src/Storm/Model/PersistenceStrategy/Volatile.php @@ -27,18 +27,10 @@ class Storm_Model_PersistenceStrategy_Volatile extends Storm_Model_PersistenceStrategy_Abstract { - protected - $_instances = [], - $desc_order = false, - $_table; + protected array $_instances = []; - public function getTable() { - return $this->_table; - } - - - public function setTable($tbl) { - $this->_table = $tbl; + public function setInstances(array $instances) : self { + $this->_instances = $instances; return $this; } @@ -56,7 +48,7 @@ class Storm_Model_PersistenceStrategy_Volatile /** * @param $select null | string | array */ - protected function _findAll($select, ?Storm_Query $query) : array { + protected function _findAll($select, ?Storm_Query_Abstract $query) : array { $group_by = $this->_getGroupBy($select, $query); $order = $this->_getOrder($select); $limit = $this->_getLimit($select, $query); @@ -73,7 +65,7 @@ class Storm_Model_PersistenceStrategy_Volatile $this->_allMatchingInstancesDo($select, $query, function($model) use (&$values) { - $values []= $model; + $values [] = $model; }); $values = $this->groupBy($values, $group_by); @@ -85,6 +77,9 @@ class Storm_Model_PersistenceStrategy_Volatile if ($page_size > 0) $values = array_slice($values, $page * $page_size, $page_size); + if ($query) + $query->notify(); + return array_map([$this->_loader, 'newFromRow'], $values); } @@ -119,12 +114,17 @@ class Storm_Model_PersistenceStrategy_Volatile } - public function fetchForQuery(Storm_Query $query) : array { + public function getInstances() : array { + return $this->_instances; + } + + + public function fetchForQuery(Storm_Query_Abstract $query) : array { return $this->_findAll([], $query); } - public function countForQuery(Storm_Query $query) : int { + public function countForQuery(Storm_Query_Abstract $query) : int { return sizeof($this->fetchForQuery($query)); } @@ -306,31 +306,25 @@ class Storm_Model_PersistenceStrategy_Volatile null, function($model) { unset($this->_instances[$model['id']]); - }, - $this->getInstancesArray()); + }); } protected function _allMatchingInstancesDo(array $clauses, - ?Storm_Query $query, + ?Storm_Query_Abstract $query, callable $callback) : int { - $filter_callback = $query - ? fn($model) => $this->_modelMatchesQuery($model, $query) - : fn($model) => $this->_modelMatchesClauses($model, $clauses); - - $matching_instances = array_filter($this->getInstancesArray(), $filter_callback); + $matching_instances = $query + ? $query->getMatchingInstances() + : $this->_modelsFromClauses($clauses); return count(array_map($callback, $matching_instances)); } - protected function _modelMatchesClauses(array $model, array $clauses) : bool { - return $this->containsAllAttributes($model, $clauses); - } - - - protected function _modelMatchesQuery(array $model, Storm_Query $query) : bool { - return $query->containsAllAttributes($model); + protected function _modelsFromClauses(array $clauses) : array { + return + array_filter($this->getInstancesArray(), + fn($model) => $this->containsAllAttributes($model, $clauses)); } @@ -344,7 +338,7 @@ class Storm_Model_PersistenceStrategy_Volatile } - protected function _getLimit(array &$select, ?Storm_Query $query) : string { + protected function _getLimit(array &$select, ?Storm_Query_Abstract $query) : string { if ($query) return $query->getLimitValue(); @@ -355,7 +349,7 @@ class Storm_Model_PersistenceStrategy_Volatile } - protected function _getLimitPage(array &$select, ?Storm_Query $query) : array { + protected function _getLimitPage(array &$select, ?Storm_Query_Abstract $query) : array { if ($query) return $query->getLimitPageValue(); @@ -373,7 +367,7 @@ class Storm_Model_PersistenceStrategy_Volatile } - protected function _getGroupBy(array &$select, ?Storm_Query $query) : string { + protected function _getGroupBy(array &$select, ?Storm_Query_Abstract $query) : string { if ($query) return $query->getGroupByValue(); diff --git a/src/Storm/Model/Row.php b/src/Storm/Model/Row.php new file mode 100644 index 0000000000000000000000000000000000000000..70a479a15c5a6526fdf5e12f76d383baf50ac72e --- /dev/null +++ b/src/Storm/Model/Row.php @@ -0,0 +1,245 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Model_Row implements Storm_Model_Interface { + + const GETTER_METHOD = 'get'; + + protected static array $_loaders = []; + protected array $_attributes = []; + protected string $_table_primary = ''; + protected Storm_Model_Associations $_associations; + + public function __construct() { + $this->_associations = new Storm_Model_Associations; + } + + + /** + * Main purpose is to setup generic getters and setters: + * + * $car->getColor(); + * + * @param string $method + * @param array $args + * @return Storm_Model_Abstract + * @throws Exception + */ + public function __call($method, $args) { + return $this->_associations->handleCall($this, $method, $args); + } + + + /** + * Create compatibility with accessor like $miles->instrument. + * ex: + * $miles->setInstrument("trumpet"); + * assert("trumpet" === $miles->getTrumpet()) + * assert("trumpet" === $miles->trumpet) + * + * If attribute not defined, returns null for compatibility purpose + * with legacy code - Remember STORM is intended to get + * easily into messy crappy code. Sorry. + * + * assert(null === $miles->zork) + * + * @param string $field name of the attribute / field + * @return mixed the value of attribute or null if does not exist. + */ + public function __get($field) { + if (array_key_exists($key = strtolower($field), $this->_attributes) + || array_key_exists($key = strtoupper($field), $this->_attributes)) + return $this->_attributes[$key]; + + return null; + } + + + /** + * Return the value of attribute $field. If cannot find it, raise Exception. + * @return mixed + * @throws Storm_Model_Exception + */ + protected function _get(string $field) { + if (array_key_exists($field, $this->_attributes)) + return $this->_attributes[$field]; + + throw new Storm_Model_Exception(sprintf('Tried to call unknown method %s::get%s', + get_class($this), + $this->attributeNameToAccessor($field))); + } + + + public static function getClassVar(string $var) { + $reflection = new ReflectionClass(get_called_class()); + $class_vars = $reflection->getdefaultProperties(); + + return $class_vars[$var]; + } + + + public static function getLoader() { + return static::getLoaderFor(get_called_class()); + } + + + public static function __callStatic($name, $args) { + return call_user_func_array([static::getLoader(), $name], $args); + } + + + public static function getLoaderFor(string $class) { + return static::$_loaders[$class] + ?? static::setLoaderFor($class, (new Storm_Model_BaseLoader($class))); + } + + + public static function setLoaderFor(string $class, $loader) { + return static::$_loaders[$class] = $loader; + } + + + public static function unsetLoaders() { + static::$_loaders = []; + } + + + public static function getLoaders() : array { + return static::$_loaders; + } + + + public function getClassName() : string { + return get_class($this); + } + + + /** + * Return an associative array with attribute + * name as $key and its value. + * + * Used by Loader while saving in order to build the + * SQL query. + */ + public function attributesToArray() : array { + return $this->_attributes; + } + + + public function getRawAttributes() : array { + return $this->_attributes; + } + + + /** + * Return an associative array with attribute + * name as $key and its value. + * + * This method may be redefined in subclasses + * in order to provide some coupling with Zend_Form::populate() + */ + public function toArray() : array { + return $this->attributesToArray(); + } + + + public function isAttributeEmpty(string $attribute) : bool { + if ( ! array_key_exists($attribute, $this->_attributes)) + return true; + + return empty($this->_get($attribute)); + } + + + /** + * See __call + * + * ex: + * $car->perform('getColor') + */ + public function perform(string $method, array $args = []) { + if ( ! $matches = $this->_methodMatch($method)) + throw new Storm_Model_Exception('Tried to call unknown method ' + . get_class($this) . '::' . $method); + + return static::GETTER_METHOD === $matches[1] + ? $this->_get($this->_accessorToAttributeName($matches[2])) + : $this; + } + + + protected function _methodMatch(string $method) : ?array { + return static::GETTER_METHOD === substr($method, 0, 3) + ? [1 => static::GETTER_METHOD, + 2 => substr($method, 3)] + : null; + } + + + /** + * UserId -> user_id + */ + protected function _accessorToAttributeName(string $accessor) : string { + return Storm_Inflector::underscorize($accessor); + } + + + /** + * Set initial attribute values with a [name] => value formatted array + * Used by Loader::newFromRow + * The difference with updateAttributes is that it doesn't call + * setAttributeName(...) magic. + * + * So we are sure that attributes are exactly those from db. + */ + public function initializeAttributes(array $datas) : self { + $this->_attributes = array_merge($this->_attributes, array_change_key_case($datas)); + + return $this; + } + + + public function attributeNameToAccessor(string $name) : string { + return Storm_Inflector::camelize($name); + } + + + public function isAttributeExists(string $field) : bool { + return array_key_exists($field, $this->_attributes); + } + + + public function callGetterByAttributeName(string $attribute) { + return call_user_func([$this, static::GETTER_METHOD + . $this->attributeNameToAccessor($attribute)]); + } + + + public function __toString() { + return get_class($this); + } +} diff --git a/src/Storm/Model/Table.php b/src/Storm/Model/Table.php index 2778a62d1caeb014266a3e70f1786fa8140a5654..b07b1698cc580d865ac5010c69e3cc53195bc570 100644 --- a/src/Storm/Model/Table.php +++ b/src/Storm/Model/Table.php @@ -26,6 +26,7 @@ THE SOFTWARE. class Storm_Model_Table extends Zend_Db_Table_Abstract { + public function getName() : string { + return $this->_name; + } } - -?> diff --git a/src/Storm/Query.php b/src/Storm/Query.php index bac1b191e5df2745da62837fab5d5bc541a56ab7..b68e80e03b16bcf51d940b8d674797fdb1d36726 100644 --- a/src/Storm/Query.php +++ b/src/Storm/Query.php @@ -25,336 +25,38 @@ */ -class Storm_Query implements Storm_Query_CriteriaInterface { +class Storm_Query extends Storm_Query_Abstract { + use Storm_Query_Trait; - protected array $_orders; - protected Storm_Model_Loader $_loader; - protected Storm_Query_CriteriaInterface $_criteria; - protected ?Storm_Query_Clause $_clause_limit; - protected ?Storm_Query_Clause $_clause_limit_page; - protected ?Storm_Query_Clause $_clause_group_by; - - protected function __construct(Storm_Model_Loader $loader) { - $this->_loader = $loader; - $this->_orders = []; - $this->_clause_limit = null; - $this->_clause_limit_page = null; - $this->_clause_group_by = null; - $this->_criteria = new Storm_Query_Criteria; - } - - - public static function from(string $loader_class) : self { - return new static($loader_class::getLoader()); - } - - - /** - * @param $limit int | string - */ - public function limit($limit) : self { - $this->_clause_limit = Storm_Query_Clause::limit($limit); - return $this; - } - - - /** - * @param $range array | int | string - */ - public function limit_page($range) : self { - $this->_clause_limit_page = Storm_Query_Clause::limitPage($range); - return $this; - } - - - public function group(string $value) : self { - $this->_clause_group_by = Storm_Query_Clause::group($value); - return $this; - } - - - /** - * @param $key_or_clause string|Storm_Query_MatchRating - */ - public function order($key_or_clause) : self { - $this->_orders [] = $this->_order($key_or_clause) - ->setOrder(false); - return $this; + public function __construct(Storm_Model_Loader $loader, ?string $alias = null) { + $this->_helper = new Storm_Query_Helper($loader, $alias); + $this->_criteria = (new Storm_Query_Criteria) + ->setAlias($this->_helper->getIdentifier()); } - /** - * @param $key_or_clause string|Storm_Query_MatchRating - */ - public function order_desc($key_or_clause) : self { - $this->_orders [] = $this->_order($key_or_clause) - ->setOrder(true); - return $this; - } - - - /** - * @param $key_or_clause string|Storm_Query_MatchRating - */ - protected function _order($key_or_clause) : Storm_Query_Clause { - if ($key_or_clause instanceof Storm_Query_MatchRating) - return Storm_Query_Clause::order($key_or_clause->getKey(), - Storm_Query_Clause::match($key_or_clause)); - - return Storm_Query_Clause::order($key_or_clause); - } - - - public function fetchAll() : array { - return $this->_loader->fetchForQuery($this); - } + protected function _prepareHelper() : Storm_Query_Helper { + $helper = $this->getHelper(); + if ($helper->hasSelect()) + $helper->beRow(); + if (!$helper->hasSelect()) + $helper->allSelect(); - public function count() : int { - return $this->_loader->countForQuery($this); - } - - - public function fetchFirst() : ?Storm_Model_Abstract { - return $this->_loader->fetchFirstForQuery($this); - } - - - /** interface Storm_Query_CriteriaInterface */ - - public function beOr() : self { - $this->_criteria->beOr(); - return $this; - } - - - public function or(Storm_Query_CriteriaInterface $criteria) : self { - $this->_criteria->or($criteria); - return $this; - } - - - public function and(Storm_Query_CriteriaInterface $criteria) : self { - $this->_criteria->and($criteria); - return $this; - } - - - /** - * @param $value int | string - */ - public function eq(string $key, $value) : self { - $this->_criteria->eq($key, $value); - return $this; - } - - - /** - * @param $value int | string - */ - public function not_eq(string $key, $value) : self { - $this->_criteria->not_eq($key, $value); - return $this; - } - - - public function like(string $key, string $value) : self { - $this->_criteria->like($key, $value); - return $this; - } - - - public function not_like(string $key, string $value) : self { - $this->_criteria->not_like($key, $value); - return $this; - } - - - /** - * @param $value int | string - */ - public function gt(string $key, $value) : self { - $this->_criteria->gt($key, $value); - return $this; - } - - - /** - * @param $value int | string - */ - public function gt_eq(string $key, $value) : self { - $this->_criteria->gt_eq($key, $value); - return $this; - } - - - /** - * @param $value int | string - */ - public function lt(string $key, $value) : self { - $this->_criteria->lt($key, $value); - return $this; - } - - - /** - * @param $value int | string - */ - public function lt_eq(string $key, $value) : self { - $this->_criteria->lt_eq($key, $value); - return $this; - } - - - public function is_null(string $key) : self { - $this->_criteria->is_null($key); - return $this; - } - - - public function not_is_null(string $key) : self { - $this->_criteria->not_is_null($key); - return $this; - } - - - public function in(string $key, array $array) : self { - $this->_criteria->in($key, $array); - return $this; - } - - - public function not_in(string $key, array $array) : self { - $this->_criteria->not_in($key, $array); - return $this; - } - - - public function start(string $key, string $value) : self { - $this->_criteria->start($key, $value); - return $this; - } - - - public function end(string $key, string $value) : self { - $this->_criteria->end($key, $value); - return $this; - } - - - public function match(Storm_Query_MatchBoolean $match) : self { - $this->_criteria->match($match); - return $this; + return $helper; } /** DB management */ - /** - * @param $select Zend_Db_Table_Select|Storm_Test_ObjectWrapper - */ - public function assemble(object $select) : self { - $sql = new Storm_Query_Sql; - $this->_criteria->assemble($sql); - if ($where = $sql->implode($this->_criteria->separator())) - $select->where($where); - - foreach ($this->_orders as $order) - $order->assemble($select); - - if ($this->_clause_limit) - $this->_clause_limit->assemble($select); - - if ($this->_clause_limit_page) - $this->_clause_limit_page->assemble($select); - - if ($this->_clause_group_by) - $this->_clause_group_by->assemble($select); - - return $this; - } - - - /** Volatile management */ - - public function orderVolatile(array $models) : array { - return (new Storm_Query_Order(array_reverse($this->_orders))) - ->compareVolatile($models); - } - - - public function containsAllAttributes(array $model) : bool { - return $this->_criteria->containsAllAttributes($model); - } - - - public function getLimitValue() : string { - return $this->_clause_limit - ? $this->_clause_limit->getValue() - : ''; - } - - - public function getLimitPageValue() : array { - return $this->_clause_limit_page - ? $this->_clause_limit_page->getValue() - : []; - } - - - public function getGroupByValue() : string { - return $this->_clause_group_by - ? $this->_clause_group_by->getValue() - : ''; - } -} - - - - -class Storm_Query_Order { - - protected ?Storm_Query_Order $_next_query; - protected ?Storm_Query_Clause $_clause; - protected int $_position; - - public function __construct(array $orders, ?int $position = 0) { - $this->_position = $position++; - $this->_clause = array_shift($orders); - - $this->_next_query = ($orders - ? (new Storm_Query_Order($orders, $position)) - : null); - } - - - public function getNextQuery() : ?Storm_Query_Order { - if (!($next_query = $this->_next_query)) - return null; - - return $next_query->getClause() - ? $next_query - : null; - } - - - public function getClause() : ?Storm_Query_Clause { - return $this->_clause; - } - - - public function getPosition() : int { - return 10 ** $this->_position; - } - - - public function compareVolatile(array $models) : array { - if (!$models) - return $models; + public function getAssembleSelect() : Zend_Db_Table_Select { + $select = parent::getAssembleSelect(); + $this->_assembleGroupBy($select); + $this->_assembleOrders($select); - if ($clause = $this->getClause()) - usort($models, fn($a, $b) => $clause->compare($this, $a, $b)); + if ($this->_distinct) + $select->distinct(); - return $models; + return $select; } } diff --git a/src/Storm/Query/Abstract.php b/src/Storm/Query/Abstract.php new file mode 100644 index 0000000000000000000000000000000000000000..10051642e56482b096191f79be2354a92363e5de --- /dev/null +++ b/src/Storm/Query/Abstract.php @@ -0,0 +1,516 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +abstract class Storm_Query_Abstract implements Storm_Query_CriteriaInterface { + + protected array $_errors = []; + protected array $_sub_queries = []; + protected Storm_Query_Helper $_helper; + protected Storm_Query_CriteriaInterface $_criteria; + protected ?Storm_Query_Clause $_clause_limit = null; + protected ?Storm_Query_Clause $_clause_limit_page = null; + + public function getCriteria() : Storm_Query_CriteriaInterface { + return $this->_criteria; + } + + + public function getSubQueries() : array { + return $this->_sub_queries; + } + + + public function getJoins() : array { + return []; + } + + + public function select(?array $select = []) : self { + $this->getHelper()->addSelect($select); + + return $this; + } + + + public function getHelper() : Storm_Query_Helper { + return $this->_helper; + } + + + public function limit(int $limit) : self { + $this->_clause_limit = Storm_Query_Clause::limit($limit); + + return $this; + } + + + /** + * @param $range array | int | string + */ + public function limit_page($range) : self { + $this->_clause_limit_page = Storm_Query_Clause::limitPage($range); + + return $this; + } + + + /** interface Storm_Query_CriteriaInterface */ + + public function beOr() : self { + $this->_criteria->beOr(); + + return $this; + } + + + public function or(Storm_Query_CriteriaInterface $criteria) : self { + $this->_criteria->or($criteria->setAlias($this->getHelper()->getIdentifier())); + + return $this; + } + + + public function and(Storm_Query_CriteriaInterface $criteria) : self { + $this->_criteria->and($criteria->setAlias($this->getHelper()->getIdentifier())); + + return $this; + } + + + /** + * @param $value int | string + */ + public function eq(string $key, $value) : self { + $this->_criteria->eq($key, $value); + + return $this; + } + + + /** + * @param $value int | string + */ + public function not_eq(string $key, $value) : self { + $this->_criteria->not_eq($key, $value); + + return $this; + } + + + public function like(string $key, string $value) : self { + $this->_criteria->like($key, $value); + + return $this; + } + + + public function not_like(string $key, string $value) : self { + $this->_criteria->not_like($key, $value); + + return $this; + } + + + /** + * @param $value int | string + */ + public function gt(string $key, $value) : self { + $this->_criteria->gt($key, $value); + + return $this; + } + + + /** + * @param $value int | string + */ + public function gt_eq(string $key, $value) : self { + $this->_criteria->gt_eq($key, $value); + + return $this; + } + + + /** + * @param $value int | string + */ + public function lt(string $key, $value) : self { + $this->_criteria->lt($key, $value); + + return $this; + } + + + /** + * @param $value int | string + */ + public function lt_eq(string $key, $value) : self { + $this->_criteria->lt_eq($key, $value); + + return $this; + } + + + public function eq_length(string $key, int $length) : self { + $this->_criteria->eq_length($key, $length); + + return $this; + } + + + public function not_eq_length(string $key, int $length) : self { + $this->_criteria->not_eq_length($key, $length); + + return $this; + } + + + public function gt_length(string $key, int $length) : self { + $this->_criteria->gt_length($key, $length); + + return $this; + } + + + public function gt_eq_length(string $key, int $length) : self { + $this->_criteria->gt_eq_length($key, $length); + + return $this; + } + + + public function lt_length(string $key, int $length) : self { + $this->_criteria->lt_length($key, $length); + + return $this; + } + + + public function lt_eq_length(string $key, int $length) : self { + $this->_criteria->lt_eq_length($key, $length); + + return $this; + } + + + public function eq_left(string $key, int $width, string $value) : self { + $this->_criteria->eq_left($key, $width, $value); + + return $this; + } + + + public function not_eq_left(string $key, int $width, string $value) : self { + $this->_criteria->not_eq_left($key, $width, $value); + + return $this; + } + + + public function gt_left(string $key, int $width, string $value) : self { + $this->_criteria->gt_left($key, $width, $value); + + return $this; + } + + + public function gt_eq_left(string $key, int $width, string $value) : self { + $this->_criteria->gt_eq_left($key, $width, $value); + + return $this; + } + + + public function lt_left(string $key, int $width, string $value) : self { + $this->_criteria->lt_left($key, $width, $value); + + return $this; + } + + + public function lt_eq_left(string $key, int $width, string $value) : self { + $this->_criteria->lt_eq_left($key, $width, $value); + + return $this; + } + + + public function is_null(string $key) : self { + $this->_criteria->is_null($key); + + return $this; + } + + + public function not_is_null(string $key) : self { + $this->_criteria->not_is_null($key); + + return $this; + } + + + /** + * @param $array_or_subquery array | Storm_Query_In + */ + public function in(string $key, $array_or_subquery) : self { + if ($array_or_subquery instanceof Storm_Query_In) { + $this->_sub_queries [] = $this->_updateIn($key, $array_or_subquery); + + return $this; + } + + $this->_criteria->in($key, $array_or_subquery); + return $this; + } + + + /** + * @param $array_or_subquery array | Storm_Query_In + */ + public function not_in(string $key, $array_or_subquery) : self { + if ($array_or_subquery instanceof Storm_Query_In) { + $this->_sub_queries [] = $this->_updateIn($key, $array_or_subquery)->beNot(); + + return $this; + } + + $this->_criteria->not_in($key, $array_or_subquery); + return $this; + } + + + protected function _updateIn(string $key, Storm_Query_In $sub_query) : Storm_Query_In { + return $sub_query->setParentHelper($this->getHelper()) + ->setCondition(Storm_Query_Condition::newWith($sub_query, + $key, + $sub_query->getHelper() + ->getFirstSelect())); + } + + + public function start(string $key, string $value) : self { + $this->_criteria->start($key, $value); + + return $this; + } + + + public function end(string $key, string $value) : self { + $this->_criteria->end($key, $value); + + return $this; + } + + + public function match(Storm_Query_MatchBoolean $match) : self { + $this->_criteria->match($match); + + return $this; + } + + + public function exists(Storm_Query_Exists $exists) : self { + $this->_sub_queries [] = $exists->setParentHelper($this->getHelper()); + + return $this; + } + + + public function not_exists(Storm_Query_Exists $exists) : self { + $this->_sub_queries [] = $exists->beNot()->setParentHelper($this->getHelper()); + + return $this; + } + + + /** DB management */ + + public function assembleDb() : string { + return $this->getAssembleSelect()->assemble(); + } + + + public function getAssembleSelect() : Zend_Db_Table_Select { + $select = $this->getHelper()->getLoader()->getTable()->select(); + $this->_assembleFrom($select); + $this->_assembleWhere($select); + $this->_assembleLimit($select); + $this->_assembleLimitPage($select); + + return $select; + } + + + protected function _assembleFrom(Zend_Db_Table_Select $select) : self { + $select->from($this->getHelper()->table(), $this->getHelper()->select()); + + return $this; + } + + + protected function _assembleWhere(Zend_Db_Table_Select $select) : self { + if ($where = $this->_sqlWhere()) + $select->where($where); + + return $this; + } + + + protected function _sqlWhere() : string { + $sql = new Storm_Query_Sql; + $this->_criteria->assemble($sql); + + foreach ($this->getSubQueries() as $sub) + $sql->write($sub->sql()); + + return $sql->implode($this->_criteria->separator()); + } + + + protected function _assembleLimit(Zend_Db_Table_Select $select) : self { + if ($this->_clause_limit) + $this->_clause_limit->assemble($select); + + return $this; + } + + + protected function _assembleLimitPage(Zend_Db_Table_Select $select) : self { + if ($this->_clause_limit_page) + $this->_clause_limit_page->assemble($select); + + return $this; + } + + + /** Volatile management */ + + public function assembleVolatile() : string { + return ($errors = implode(', ', $this->_errors)) + ? $errors + : $this->assembleDb(); + } + + + public function computeErrors() : array { + foreach ($this->getSubQueries() as $sub) + $this->_errors = [...$this->_errors, ...$sub->computeErrors()]; + + return $this->_errors; + } + + + public function instances(?array $instances = []) : array { + $instances [$this->getHelper()->getAlias()] = $this->_getFilteredInstances(); + + foreach ($this->getSubQueries() as $query) + $instances = $query->instances($instances); + + return $instances; + } + + + public function getMatchingInstances() : array { + return $this->computeErrors() + ? [] + : $this->_distinctInstances($this->_filterInstances()); + } + + + protected function _distinctInstances(array $instances) : array { + if ( ! $this->_distinct) + return $instances; + + $old_instances = []; + return array_filter(array_map(function($instance) use (&$old_instances) { + if ($this->_arrayContain($old_instances, $instance)) + return null; + + $old_instances [] = $instance; + return $instance; + }, + $instances)); + } + + + protected function _arrayContain(array $old_instances, array $instance) : bool { + $old_instance = array_shift($old_instances); + if (null === $old_instance) + return false; + + $is_same = true; + foreach ($old_instance as $k => $v) + if (in_array($k, $this->getHelper()->select()) && $v !== $instance[$k]) + $is_same = false; + + return $is_same + ? true + : $this->_arrayContain($old_instances, $instance); + } + + + protected function _filterInstances() : array { + $query_instances = new Storm_Query_Instances($this); + $this->filterWith($query_instances); + + return $query_instances->resultFor($this); + } + + + public function filterWith(Storm_Query_Instances $query_instances) : self { + foreach ($this->getSubQueries() as $sub) + $sub->filterWith($query_instances); + + return $this; + } + + + protected function _getFilteredInstances() : array { + $instances = array_values(array_map(fn($model) => $model->getRawAttributes(), + $this->getHelper() + ->getLoader() + ->getPersistenceStrategy() + ->getInstances())); + + return array_filter($instances, + fn($model) => $this->getCriteria()->containsAllAttributes($model)); + } + + + public function getLimitValue() : string { + return $this->_clause_limit + ? $this->_clause_limit->getValue() + : ''; + } + + + public function getLimitPageValue() : array { + return $this->_clause_limit_page + ? $this->_clause_limit_page->getValue() + : []; + } +} diff --git a/src/Storm/Query/Clause.php b/src/Storm/Query/Clause.php index 301d2711b0ae07ce010ec002eb209da3c74bb8a2..336f8ecc3ab8c863533eb82399ad92f8c1551f16 100644 --- a/src/Storm/Query/Clause.php +++ b/src/Storm/Query/Clause.php @@ -27,17 +27,19 @@ THE SOFTWARE. class Storm_Query_Clause { - protected string $_key; + protected Storm_Query_ClauseKey $_key; protected string $_operator; - protected bool $_negated; + protected string $_alias = ''; + protected bool $_negated = false; protected $_value; const CLAUSE_WHERE = 'where', - CLAUSE_LIKE = 'like', + CLAUSE_LIKE = 'LIKE', CLAUSE_EQUAL = '=', - CLAUSE_IN = 'in', - CLAUSE_IS = 'is', + CLAUSE_NOT_EQUAL = '!=', + CLAUSE_IN = 'IN', + CLAUSE_IS = 'IS', CLAUSE_GREATER = '>', CLAUSE_GREATER_EQUAL = '>=', CLAUSE_LESSER = '<', @@ -47,6 +49,8 @@ class Storm_Query_Clause { CLAUSE_LIMIT_PAGE = 'limitPage', CLAUSE_ORDER_BY = 'order by', CLAUSE_GROUP_BY = 'group by', + CLAUSE_NOT = ' NOT ', + CLAUSE_EXISTS = 'EXISTS', PERCENT = '%'; const CLAUSE_IMPLEMENTATIONS = @@ -62,7 +66,6 @@ class Storm_Query_Clause { self::CLAUSE_MATCH => Storm_Query_Clause_Match::class, self::CLAUSE_LIMIT => Storm_Query_ClauseLimit::class, self::CLAUSE_LIMIT_PAGE => Storm_Query_ClauseLimitPage::class, - self::CLAUSE_ORDER_BY => Storm_Query_Clause_OrderBy::class, self::CLAUSE_GROUP_BY => Storm_Query_ClauseGroupBy::class, self::CLAUSE_WHERE => Storm_Query_ClauseWhere::class ]; @@ -70,8 +73,9 @@ class Storm_Query_Clause { /** * @param $value null | string | Storm_Query_Clause */ - public function __construct(string $key, string $operator, $value) { - $this->_negated = false; + public function __construct(Storm_Query_ClauseKey $key, + string $operator, + $value) { $this->_key = $key; $this->_operator = $operator; $this->_value = $value; @@ -85,10 +89,11 @@ class Storm_Query_Clause { } - public static function newFor(string $key, + public static function newFor(Storm_Query_ClauseKey $key, string $operator, $value) : self { $class = static::CLAUSE_IMPLEMENTATIONS[$operator]; + return new $class($key, $operator, $value); } @@ -106,68 +111,94 @@ class Storm_Query_Clause { if ($is_like) $operator = static::CLAUSE_LIKE; + $matches = null; + if (!$is_like) + preg_match('/^left\(([^,]+),\s+(\d+)\)$/i', $key, $matches); + if ($matches) + return (static::newFor((new Storm_Query_ClauseKey(trim($matches[1]))) + ->setLeft((int)$matches[2]), + $operator, + $value)) + ->setNegated($negated); + if (null === $value) $operator = static::CLAUSE_IS; if (is_array($value)) $operator = static::CLAUSE_IN; - return (static::newFor($key, $operator, $value))->setNegated($negated); + return (static::newFor(new Storm_Query_ClauseKey($key), $operator, $value)) + ->setNegated($negated); } public static function isNull(string $key) : self { - return static::newFor($key, static::CLAUSE_IS, null); + return static::newFor(static::_initKey($key), static::CLAUSE_IS, null); } public static function in(string $key, array $array) : self { - return static::newFor($key, static::CLAUSE_IN, $array); + return static::newFor(static::_initKey($key), static::CLAUSE_IN, $array); + } + + + public static function equal($key, $value) : self { + return static::newFor(static::_initKey($key), static::CLAUSE_EQUAL, $value); } - public static function equal(string $key, $value) : self { - return static::newFor($key, static::CLAUSE_EQUAL, $value); + public static function left(string $key, int $left, string $value) : self { + return static::newFor(static::_initKey($key)->setLeft($left), static::CLAUSE_EQUAL, $value); } public static function like(string $key, string $value) : self { - return static::newFor($key, static::CLAUSE_LIKE, $value); + return static::newFor(static::_initKey($key), static::CLAUSE_LIKE, $value); } public static function start(string $key, string $value) : self { - return static::newFor($key, static::CLAUSE_LIKE, $value . static::PERCENT); + return static::newFor(static::_initKey($key), + static::CLAUSE_LIKE, + $value . static::PERCENT); } public static function end(string $key, string $value) : self { - return static::newFor($key, static::CLAUSE_LIKE, static::PERCENT . $value); + return static::newFor(static::_initKey($key), + static::CLAUSE_LIKE, + static::PERCENT . $value); } - public static function greater(string $key, $value) : self { - return static::newFor($key, static::CLAUSE_GREATER, $value); + public static function greater($key, $value) : self { + return static::newFor(static::_initKey($key), static::CLAUSE_GREATER, $value); } - public static function greaterEqual(string $key, $value) : self { - return static::newFor($key, static::CLAUSE_GREATER_EQUAL, $value); + public static function greaterEqual($key, $value) : self { + return static::newFor(static::_initKey($key), + static::CLAUSE_GREATER_EQUAL, + $value); } - public static function lesser(string $key, $value) : self { - return static::newFor($key, static::CLAUSE_LESSER, $value); + public static function lesser($key, $value) : self { + return static::newFor(static::_initKey($key), static::CLAUSE_LESSER, $value); } - public static function lesserEqual(string $key, $value) : self { - return static::newFor($key, static::CLAUSE_LESSER_EQUAL, $value); + public static function lesserEqual($key, $value) : self { + return static::newFor(static::_initKey($key), + static::CLAUSE_LESSER_EQUAL, + $value); } public static function match(Storm_Query_MatchRating $match) : self { - return static::newFor($match->getKey(), static::CLAUSE_MATCH, $match); + return static::newFor(static::_initKey($match->getKey()), + static::CLAUSE_MATCH, + $match); } @@ -175,7 +206,9 @@ class Storm_Query_Clause { * @param $limit int | string */ public static function limit($limit) : self { - return static::newFor(static::CLAUSE_LIMIT, static::CLAUSE_LIMIT, $limit); + return static::newFor(static::_initKey(static::CLAUSE_LIMIT), + static::CLAUSE_LIMIT, + $limit); } @@ -183,25 +216,41 @@ class Storm_Query_Clause { * @param $range array | int | string */ public static function limitPage($range) : self { - return static::newFor(static::CLAUSE_LIMIT_PAGE, + return static::newFor(static::_initKey(static::CLAUSE_LIMIT_PAGE), static::CLAUSE_LIMIT_PAGE, $range); } public static function group(string $value) : self { - return static::newFor(static::CLAUSE_GROUP_BY, static::CLAUSE_GROUP_BY, $value); + return static::newFor(static::_initKey(static::CLAUSE_GROUP_BY), + static::CLAUSE_GROUP_BY, + $value); } - public static function order(string $key, - ?Storm_Query_Clause $clause = null) : self { - return static::newFor($key, static::CLAUSE_ORDER_BY, $clause); + public static function order(string $key, $value = null) : self { + return Storm_Query_Clause_OrderBy::newFor(static::_initKey($key), static::CLAUSE_ORDER_BY, $value); + } + + + protected static function _initKey($key) : Storm_Query_ClauseKey { + return ($key instanceof Storm_Query_ClauseKey) + ? $key + : new Storm_Query_ClauseKey($key); } public function setNegated(bool $negated) : self { $this->_negated = $negated; + + return $this; + } + + + public function setAlias(string $alias) : self { + $this->_alias = $alias; + return $this; } @@ -211,41 +260,43 @@ class Storm_Query_Clause { } - public function assemble($select) : self { + public function assemble(Zend_Db_Table_Select $select) : self { return $this; } - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { if (static::CLAUSE_WHERE === $this->_operator) return '(' . $this->getValue() . ')'; - return Zend_Db_Table_Abstract::getDefaultAdapter() + return $with_quote + ? Zend_Db_Table_Abstract::getDefaultAdapter() ->quoteInto($this->_clauseFormatDb(), - $this->getValue(), null, null); + $this->getValue(), null, null) + : str_replace('?', $this->getValue(), $this->_clauseFormatDb()); } public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) + if (!$this->_key->existInModel($model)) return $this->_negated; - return ($this->_negated !== ($model[$this->_key] == $this->getValue())); + return $this->contains($this->_key->keyFrom($model), $this->getValue()); } - protected function _clauseFormatDb() : string { - return $this->_key . $this->_getOperator() . '?'; + public function contains($value_1, $value_2) : bool { + return ($this->_negated !== ($value_1 == $value_2)); } - protected function _getOperator() : string { - return ($this->_negated ? 'not ' : '') . $this->_operator; + protected function _clauseFormatDb() : string { + return $this->_key->key($this->_alias) . $this->_getOperator() . '?'; } - protected function _existKeyInModel(array $model) : bool { - return array_key_exists($this->_key, $model); + protected function _getOperator() : string { + return ($this->_negated ? static::CLAUSE_NOT : ' ') . $this->_operator . ' '; } } @@ -261,18 +312,9 @@ class Storm_Query_ClauseLike extends Storm_Query_Clause { } - protected function _clauseFormatDb() : string { - return $this->_key . ' ' . $this->_getOperator() . ' ?'; - } - - - public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) - return $this->_negated; - - $matches = - preg_match(('/^' . str_replace('%', '.*', preg_quote($this->getValue(), '/')) . '$/i'), - $model[$this->_key]); + public function contains($value_1, $value_2) : bool { + $matches = preg_match(('/^' . str_replace('%', '.*', preg_quote($value_2, '/')) . '$/i'), + $value_1); return $this->_negated ? !$matches : $matches; } @@ -284,7 +326,7 @@ class Storm_Query_ClauseLike extends Storm_Query_Clause { class Storm_Query_ClauseEqual extends Storm_Query_Clause { protected function _getOperator() : string { - return ($this->_negated ? '!' : '') . $this->_operator; + return ' ' . ($this->_negated ? static::CLAUSE_NOT_EQUAL : $this->_operator) . ' '; } } @@ -302,21 +344,38 @@ class Storm_Query_ClauseIn extends Storm_Query_Clause { public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) + if (!$this->_key->existInModel($model)) return $this->_negated; if (0 === count($this->getValue())) throw new Storm_Model_Exception(sprintf('array given for %s is empty', - $this->_key)); + $this->_key->getKey())); + + return $this->contains($this->_key->keyFrom($model), $this->getValue()); + } + + + public function contains($value_1, $value_2) : bool { + if ( ! is_array($value_2)) + $value_2 = [$value_2]; - return in_array($model[$this->_key], $this->getValue()) + return in_array($value_1, $value_2) ? !$this->_negated : $this->_negated; } + public function getFormatDb(?bool $with_quote = true) : string { + return $with_quote + ? Zend_Db_Table_Abstract::getDefaultAdapter() + ->quoteInto($this->_clauseFormatDb(), + $this->getValue(), null, null) + : str_replace('?', implode(', ', $this->getValue()), $this->_clauseFormatDb()); + } + + protected function _clauseFormatDb() : string { - return $this->_key . ' ' . $this->_getOperator() . ' (?)'; + return $this->_key->key($this->_alias) . $this->_getOperator() . '(?)'; } } @@ -332,13 +391,13 @@ class Storm_Query_ClauseIsNull extends Storm_Query_Clause { } - public function getFormatDb() : string { - return $this->_key . ' ' . $this->_getOperator() . ' null'; + public function getFormatDb(?bool $with_quote = true) : string { + return $this->_key->key($this->_alias) . $this->_getOperator() . 'null'; } protected function _getOperator() : string { - return $this->_operator . ($this->_negated ? ' not' : ''); + return ' ' . $this->_operator . ($this->_negated ? static::CLAUSE_NOT : ' '); } } @@ -347,11 +406,8 @@ class Storm_Query_ClauseIsNull extends Storm_Query_Clause { class Storm_Query_ClauseGreater extends Storm_Query_Clause { - public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) - return false; - - return $model[$this->_key] > $this->getValue(); + public function contains($value_1, $value_2) : bool { + return $value_1 > $value_2; } } @@ -360,11 +416,8 @@ class Storm_Query_ClauseGreater extends Storm_Query_Clause { class Storm_Query_ClauseGreaterEqual extends Storm_Query_Clause { - public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) - return false; - - return $model[$this->_key] >= $this->getValue(); + public function contains($value_1, $value_2) : bool { + return $value_1 >= $value_2; } } @@ -373,11 +426,8 @@ class Storm_Query_ClauseGreaterEqual extends Storm_Query_Clause { class Storm_Query_ClauseLesser extends Storm_Query_Clause { - public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) - return false; - - return $model[$this->_key] < $this->getValue(); + public function contains($value_1, $value_2) : bool { + return $value_1 < $value_2; } } @@ -386,11 +436,8 @@ class Storm_Query_ClauseLesser extends Storm_Query_Clause { class Storm_Query_ClauseLesserEqual extends Storm_Query_Clause { - public function containAttibuteInVolatile(array $model) : bool { - if (!$this->_existKeyInModel($model)) - return false; - - return $model[$this->_key] <= $this->getValue(); + public function contains($value_1, $value_2) : bool { + return $value_1 <= $value_2; } } @@ -414,7 +461,7 @@ class Storm_Query_ClauseLimit extends Storm_Query_Clause { } - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { return ''; } @@ -451,7 +498,7 @@ class Storm_Query_ClauseLimitPage extends Storm_Query_Clause { } - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { return ''; } @@ -474,12 +521,17 @@ class Storm_Query_ClauseGroupBy extends Storm_Query_Clause { public function assemble($select) : self { - $select->group($this->getValue()); + $alias = ''; + if ($this->_alias) + $alias = $this->_alias . '.'; + + $select->group($alias . $this->getValue()); + return $this; } - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { return ''; } @@ -505,3 +557,67 @@ class Storm_Query_ClauseWhere extends Storm_Query_Clause { return true; } } + + + + +class Storm_Query_ClauseKey { + + protected string $_key; + protected ?int $_left = null; + protected bool $_length = false; + + public function __construct(string $key) { + $this->_key = $key; + } + + + public function getKey() : string { + return $this->_key; + } + + + public function beLength() : self { + $this->_length = true; + + return $this; + } + + + public function setLeft(int $left) : self { + $this->_left = $left; + + return $this; + } + + + public function key(string $alias = '') : string { + $key = Zend_Db_Table_Abstract::getDefaultAdapter()->quoteIdentifier($this->_key, true); + + if ($alias) + $key = $alias . '.' . $key; + + if ($this->_length) + $key = 'LENGTH(' . $key . ')'; + + if (null !== $this->_left) + $key = sprintf('LEFT(%s, %d)', $key, $this->_left); + + return $key; + } + + + public function keyFrom(array $model) { + if (null !== $this->_left) + return substr($model[$this->_key] ?? '', 0, $this->_left); + + return $this->_length + ? strlen($model[$this->_key] ?? '') + : $model[$this->_key] ?? null; + } + + + public function existInModel(array $model) : bool { + return array_key_exists($this->_key, $model); + } +} diff --git a/src/Storm/Query/Clause/Match.php b/src/Storm/Query/Clause/Match.php index e7f48b9810fc03c3cd622844b71b207333d4bceb..240ed6845ca7d3156d5c1c569073aa193c9bd3f9 100644 --- a/src/Storm/Query/Clause/Match.php +++ b/src/Storm/Query/Clause/Match.php @@ -27,7 +27,7 @@ THE SOFTWARE. class Storm_Query_Clause_Match extends Storm_Query_Clause { - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { if (!$value = $this->getValue()) return ''; @@ -40,7 +40,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause { ->quoteInto('?', $content), ($value->isBooleanMode() ? ' IN BOOLEAN MODE' : '')); - return $this->_getOperator() . ' ' . $against; + return $this->_clauseFormatDb() . ' ' . $against; } @@ -89,7 +89,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause { protected function _getOperator() : string { - return $this->_operator . '(' . $this->_key . ')'; + return $this->_operator . '(' . $this->_key->getKey() . ')'; } @@ -100,7 +100,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause { protected function _getContents(array $model) : string { $contents = ''; - foreach (explode(',', $this->_key) as $key) + foreach (explode(',', $this->_key->getKey()) as $key) if (array_key_exists($key, $model)) $contents = implode(' ', array_filter([$contents, $model[$key]])); diff --git a/src/Storm/Query/Clause/OrderBy.php b/src/Storm/Query/Clause/OrderBy.php index 72deff1ba0c1f24b4f10d04cdde4aa47592c29d2..fbe6195ab3c5dfe3674f8d2bbbed08a7c30d0953 100644 --- a/src/Storm/Query/Clause/OrderBy.php +++ b/src/Storm/Query/Clause/OrderBy.php @@ -27,20 +27,32 @@ THE SOFTWARE. class Storm_Query_Clause_OrderBy extends Storm_Query_Clause { - const ORDER_DESC = ' desc'; + const ORDER_DESC = ' DESC'; + const ORDER_ASC = ' ASC'; - protected ?string $_order_mode = null; + protected string $_order_mode = self::ORDER_ASC; + + public static function newFor(Storm_Query_ClauseKey $key, + string $operator, + $value) : self { + if ( ! $value) + return new Storm_Query_Clause_OrderBy($key, $operator, null); + + if ($value instanceof Storm_Query_Clause_Match) + return new Storm_Query_Clause_OrderByForMatch($key, $operator, $value); + + return new Storm_Query_Clause_OrderByWithValue($key, $operator, $value); + } + + + public function assemble(Zend_Db_Table_Select $select) : self { + $select->order(new Zend_Db_Expr($this->_key->key($this->_alias) . $this->_order_mode)); - public function assemble($select) : self { - $select->order((($clause = $this->getValue()) - ? $clause->getFormatDb($select->getTable()) - : $this->_key) - . $this->_order_mode); return $this; } - public function getFormatDb() : string { + public function getFormatDb(?bool $with_quote = true) : string { return ''; } @@ -50,8 +62,9 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause { } - public function setOrder(bool $mode) : self { - $this->_order_mode = $mode ? static::ORDER_DESC : ''; + public function beDesc() : self { + $this->_order_mode = static::ORDER_DESC; + return $this; } @@ -68,11 +81,8 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause { protected function _compareValues(array $a, array $b) : int { - if ($match_clause = $this->getValue()) - return $match_clause->compare($a, $b); - - $first = $this->_existKeyInModel($a) ? $a[$this->_key] : ''; - $second = $this->_existKeyInModel($b) ? $b[$this->_key] : ''; + $first = $this->_key->existInModel($a) ? $a[$this->_key->getKey()] : ''; + $second = $this->_key->existInModel($b) ? $b[$this->_key->getKey()] : ''; return ($first <=> $second); } @@ -82,3 +92,50 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause { return static::ORDER_DESC === $this->_order_mode ? -1 : 1; } } + + + + +class Storm_Query_Clause_OrderByForMatch extends Storm_Query_Clause_OrderBy { + + public function assemble(Zend_Db_Table_Select $select) : self { + $select->order($this->getValue()->getFormatDb() . $this->_order_mode); + + return $this; + } + + + protected function _compareValues(array $a, array $b) : int { + return $this->getValue()->compare($a, $b); + } +} + + + + +class Storm_Query_Clause_OrderByWithValue extends Storm_Query_Clause_OrderBy { + + public function assemble(Zend_Db_Table_Select $select) : self { + $select->order(new Zend_Db_Expr($this->_key->key($this->_alias) + . ' ' . Storm_Query_Clause::CLAUSE_EQUAL . ' ' + . Zend_Db_Table_Abstract::getDefaultAdapter() + ->quoteInto('?', $this->getValue(), null, null) + . $this->_order_mode)); + + return $this; + } + + + protected function _compareValues(array $a, array $b) : int { + $value = $this->getValue(); + + $first = $this->_key->existInModel($a) && $value === $a[$this->_key->getKey()] + ? 1 + : 0; + $second = $this->_key->existInModel($b) && $value === $b[$this->_key->getKey()] + ? 1 + : 0; + + return ($first <=> $second); + } +} diff --git a/src/Storm/Query/Condition.php b/src/Storm/Query/Condition.php new file mode 100644 index 0000000000000000000000000000000000000000..ea334f82b934b60c849ca20093998a7ab650d5fa --- /dev/null +++ b/src/Storm/Query/Condition.php @@ -0,0 +1,348 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Condition { + + protected string $_parent_key; + protected string $_key; + protected string $_operator = Storm_Query_Clause::CLAUSE_EQUAL; + protected bool $_negated = false; + protected ?Storm_Query_Clause $_clause = null; + protected Storm_Query_Abstract $_query; + protected ?Storm_Query_Condition $_child_condition = null; + + protected function __construct(Storm_Query_Abstract $query, string $parent_key, string $key) { + $this->_parent_key = $parent_key; + $this->_key = $key; + $this->_query = $query; + } + + + public static function newWith(Storm_Query_Abstract $query, + string $parent_key, + string $key) : self { + if ($query instanceof Storm_Join_Inner) + return new Storm_Query_ConditionInner($query, $parent_key, $key); + if ($query instanceof Storm_Join_Left) + return new Storm_Query_ConditionLeft($query, $parent_key, $key); + if ($query instanceof Storm_Join_Right) + return new Storm_Query_ConditionRight($query, $parent_key, $key); + + if ($query instanceof Storm_Query_In) + return new Storm_Query_ConditionSubQueries($query, $parent_key, $key); + + if ($query instanceof Storm_Query_Exists) + return new Storm_Query_ConditionSubQueries($query, $parent_key, $key); + + return new static($query, $parent_key, $key); + } + + + public function getParentKey() : string { + return $this->_parent_key; + } + + + public function getQuery() : Storm_Query_Abstract { + return $this->_query; + } + + + public function add(Storm_Query_Condition $child) : self { + if ($this->_child_condition) { + $this->_child_condition->add($child); + + return $this; + } + + $this->_child_condition = $child; + + return $this; + } + + + public function beNotEqual() : self { + $this->_negated = true; + + return $this; + } + + + public function beGreater() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_GREATER; + + return $this; + } + + + public function beGreaterEqual() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_GREATER_EQUAL; + + return $this; + } + + + public function beLesser() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_LESSER; + + return $this; + } + + + public function beLesserEqual() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_LESSER_EQUAL; + + return $this; + } + + + public function beIn() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_IN; + + return $this; + } + + + public function beNotIn() : self { + $this->_operator = Storm_Query_Clause::CLAUSE_IN; + $this->_negated = true; + + return $this; + } + + + public function sql() : array { + if (!$this->_query->getParentHelper()) + return ''; + + if ( ! $this->_child_condition) + return [$this->_clause()->getFormatDb(false)]; + + return [$this->_clause()->getFormatDb(false), + ...$this->_child_condition->sql()]; + } + + + protected function _clause() : Storm_Query_Clause { + if ($this->_clause) + return $this->_clause; + + $value = ($this->_query->getHelper()->getIdentifier() + . '.' . Zend_Db_Table_Abstract::getDefaultAdapter() + ->quoteIdentifier($this->_key, true)); + + return Storm_Query_Clause::newFor(new Storm_Query_ClauseKey($this->_parent_key), + $this->_operator, + $value) + ->setNegated($this->_negated) + ->setAlias($this->_query->getParentHelper()->getIdentifier()); + } + + + public function filterWith(Storm_Query_Instances $query_instances) : self { + if (!$this->_query->getParentHelper()) + return $this; + + return $this->_filter($query_instances); + } + + + protected function _filter(Storm_Query_Instances $query_instances) : self { + return $this; + } + + + public function conditionSelect(Storm_Query_Instance $parent_instance, + Storm_Query_Instance $instance) : bool { + $parent_value = $parent_instance->value($this->_query->getParentHelper()->getAlias(), + $this->_parent_key); + $value = $instance->value($this->_query->getHelper()->getAlias(), $this->_key); + + $contains = $this->_clause()->contains($parent_value, $value); + if ( ! $this->_child_condition) + return $contains; + + return $contains && $this->_child_condition->conditionSelect($parent_instance, $instance); + } + + + protected function _updateQueryInstances(Storm_Query_Instances $query_instances, + Storm_Collection $filter) : self { + $query_instances->addAll([$this->_query->getParentHelper()->getAlias(), + $this->_query->getHelper()->getAlias()], + $filter); + + return $this; + } +} + + + + +class Storm_Query_ConditionInner extends Storm_Query_Condition { + + protected function _filter(Storm_Query_Instances $query_instances) : self { + $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias()); + $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances); + + $query_instances + ->instancesFor($this->_query->getParentHelper()->getAlias()) + ->eachDo(fn($parent) => $condition_filter->updateCrossedWith($parent)); + + return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter()); + } +} + + + + +class Storm_Query_ConditionLeft extends Storm_Query_Condition { + + protected function _filter(Storm_Query_Instances $query_instances) : self { + $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias()); + $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances); + + $query_instances + ->instancesFor($this->_query->getParentHelper()->getAlias()) + ->eachDo(fn($parent) => $condition_filter->updateOuterLeftWith($parent)); + + return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter()); + } +} + + + + +class Storm_Query_ConditionRight extends Storm_Query_Condition { + + protected function _filter(Storm_Query_Instances $query_instances) : self { + $parent_instances = $query_instances + ->instancesFor($this->_query->getParentHelper()->getAlias()); + $condition_filter = new Storm_Query_ConditionFilter($this, $parent_instances); + + $query_instances + ->instancesFor($this->_query->getHelper()->getAlias()) + ->eachDo(fn($child) => $condition_filter->updateOuterRightWith($child)); + + return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter()); + } +} + + + + +class Storm_Query_ConditionSubQueries extends Storm_Query_ConditionInner { + + protected function _filter(Storm_Query_Instances $query_instances) : self { + $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias()); + $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances); + + $query_instances + ->instancesFor($this->_query->getParentHelper()->getAlias()) + ->eachDo(fn($parent) => $condition_filter->updateAnyWith($parent)); + + return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter()); + } +} + + + + +class Storm_Query_ConditionFilter { + + protected Storm_Query_Condition $_condition; + protected Storm_Collection $_child_instances; + protected Storm_Collection $_filter; + + public function __construct(Storm_Query_Condition $condition, + Storm_Collection $child_instances) { + $this->_condition = $condition; + $this->_child_instances = $child_instances; + $this->_filter = new Storm_Collection; + } + + + public function getFilter() : Storm_Collection { + return $this->_filter; + } + + + public function updateCrossedWith(Storm_Query_Instance $parent) : self { + $this->_child_instances + ->select(fn($instance) => $this->_condition->conditionSelect($parent, $instance)) + ->eachDo(fn($instance) => $this->_filter->add((new Storm_Query_Instance) + ->addFrom($parent) + ->addFrom($instance))); + + return $this; + } + + + public function updateOuterLeftWith(Storm_Query_Instance $parent) : self { + $selected = $this->_child_instances + ->select(fn($instance) => $this->_condition->conditionSelect($parent, $instance)); + + return $this->_withSelectedDo($selected, $parent); + } + + + public function updateOuterRightWith(Storm_Query_Instance $parent) : self { + $selected = $this->_child_instances + ->select(fn($instance) => $this->_condition->conditionSelect($instance, $parent)); + + return $this->_withSelectedDo($selected, $parent); + } + + + protected function _withSelectedDo(Storm_Collection $selected, + Storm_Query_Instance $parent) : self { + if ($selected->isEmpty()) { + $this->_filter->add((new Storm_Query_Instance)->addFrom($parent)); + + return $this; + } + + $selected + ->eachDo(fn($instance) => $this->_filter->add((new Storm_Query_Instance) + ->addFrom($parent) + ->addFrom($instance))); + + return $this; + } + + + public function updateAnyWith(Storm_Query_Instance $parent) : self { + $exist = $this->_child_instances + ->detect(fn($instance) => $this->_condition->conditionSelect($parent, $instance)); + + if (($exist && ! $this->_condition->getQuery()->isNot()) + || ( ! $exist && $this->_condition->getQuery()->isNot())) + $this->_filter->add((new Storm_Query_Instance)->addFrom($parent)); + + return $this; + } +} diff --git a/src/Storm/Query/Criteria.php b/src/Storm/Query/Criteria.php index 48ba096139fb96df0ec4e623fe38dd899358b588..e1f4d9a8af4bd37deecbe5b997188b2f957ca144 100644 --- a/src/Storm/Query/Criteria.php +++ b/src/Storm/Query/Criteria.php @@ -28,12 +28,12 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { const - SEPARATOR_AND = 'and', - SEPARATOR_OR = 'or'; + SEPARATOR_AND = 'AND', + SEPARATOR_OR = 'OR'; protected array $_clauses; protected string $_separator; - + protected string $_alias = ''; public function __construct() { $this->_clauses = []; @@ -41,6 +41,12 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { } + public function setAlias(string $alias) : self { + $this->_alias = $alias; + return $this; + } + + public function beOr() : self { $this->_separator = static::SEPARATOR_OR; return $this; @@ -71,7 +77,8 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { return '(' . $sql->implode($clause->separator()) . ')'; } - return $clause->getFormatDb(); + return $clause->setAlias($this->_alias) + ->getFormatDb(); } @@ -113,6 +120,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { */ public function eq(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::equal($key, $value); + return $this; } @@ -123,12 +131,14 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { public function not_eq(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::equal($key, $value) ->setNegated(true); + return $this; } public function like(string $key, string $value) : self { $this->_clauses [] = Storm_Query_Clause::like($key, $value); + return $this; } @@ -136,6 +146,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { public function not_like(string $key, string $value) : self { $this->_clauses [] = Storm_Query_Clause::like($key, $value) ->setNegated(true); + return $this; } @@ -145,6 +156,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { */ public function gt(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::greater($key, $value); + return $this; } @@ -154,6 +166,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { */ public function gt_eq(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::greaterEqual($key, $value); + return $this; } @@ -163,6 +176,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { */ public function lt(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::lesser($key, $value); + return $this; } @@ -172,12 +186,134 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { */ public function lt_eq(string $key, $value) : self { $this->_clauses [] = Storm_Query_Clause::lesserEqual($key, $value); + + return $this; + } + + + public function eq_length(string $key, int $length) : self { + $this->_clauses [] = Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length); + + return $this; + } + + + public function not_eq_length(string $key, int $length) : self { + $this->_clauses [] = Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length) + ->setNegated(true); + + return $this; + } + + + public function gt_length(string $key, int $length) : self { + $this->_clauses [] = + Storm_Query_Clause::greater((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length); + + return $this; + } + + + public function gt_eq_length(string $key, int $length) : self { + $this->_clauses [] = + Storm_Query_Clause::greaterEqual((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length); + + return $this; + } + + + public function lt_length(string $key, int $length) : self { + $this->_clauses [] = + Storm_Query_Clause::lesser((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length); + + return $this; + } + + + public function lt_eq_length(string $key, int $length) : self { + $this->_clauses [] = + Storm_Query_Clause::lesserEqual((new Storm_Query_ClauseKey($key)) + ->beLength(), + $length); + + return $this; + } + + + public function eq_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value); + + return $this; + } + + + public function not_eq_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value) + ->setNegated(true); + + return $this; + } + + + public function gt_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::greater((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value); + + return $this; + } + + + public function gt_eq_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::greaterEqual((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value); + + return $this; + } + + + public function lt_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::lesser((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value); + + return $this; + } + + + public function lt_eq_left(string $key, int $width, string $value) : self { + $this->_clauses [] = + Storm_Query_Clause::lesserEqual((new Storm_Query_ClauseKey($key)) + ->setLeft($width), + $value); + return $this; } public function is_null(string $key) : self { $this->_clauses [] = Storm_Query_Clause::isNull($key); + return $this; } @@ -185,31 +321,42 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { public function not_is_null(string $key) : self { $this->_clauses [] = Storm_Query_Clause::isNull($key) ->setNegated(true); + return $this; } - public function in(string $key, array $array) : self { - $this->_clauses [] = Storm_Query_Clause::in($key, $array); + /** + * @param $array_or_subquery array + */ + public function in(string $key, $array_or_subquery) : self { + $this->_clauses [] = Storm_Query_Clause::in($key, $array_or_subquery); + return $this; } - public function not_in(string $key, array $array) : self { - $this->_clauses [] = Storm_Query_Clause::in($key, $array) + /** + * @param $array_or_subquery array + */ + public function not_in(string $key, $array_or_subquery) : self { + $this->_clauses [] = Storm_Query_Clause::in($key, $array_or_subquery) ->setNegated(true); + return $this; } public function start(string $key, string $value) : self { $this->_clauses [] = Storm_Query_Clause::start($key, $value); + return $this; } public function end(string $key, string $value) : self { $this->_clauses [] = Storm_Query_Clause::end($key, $value); + return $this; } @@ -228,6 +375,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface { public function match(Storm_Query_MatchBoolean $match) : self { $this->_clauses [] = Storm_Query_Clause::match($match); + return $this; } } diff --git a/src/Storm/Query/CriteriaInterface.php b/src/Storm/Query/CriteriaInterface.php index 4f44e2981efcf35723e6f09d2fd516144faf2d3c..3371faf47b2f807712761275473c0f62ab40e9dc 100644 --- a/src/Storm/Query/CriteriaInterface.php +++ b/src/Storm/Query/CriteriaInterface.php @@ -78,10 +78,16 @@ interface Storm_Query_CriteriaInterface { public function not_is_null(string $key) : self; - public function in(string $key, array $array) : self; + /** + * @param $array_or_subquery array | Storm_Query_In + */ + public function in(string $key, $array_or_subquery) : self; - public function not_in(string $key, array $array) : self; + /** + * @param $array_or_subquery array | Storm_Query_In + */ + public function not_in(string $key, $array_or_subquery) : self; public function start(string $key, string $value) : self; @@ -97,4 +103,40 @@ interface Storm_Query_CriteriaInterface { public function match(Storm_Query_MatchBoolean $match) : self; + + + public function eq_length(string $key, int $length) : self; + + + public function not_eq_length(string $key, int $length) : self; + + + public function gt_length(string $key, int $length) : self; + + + public function gt_eq_length(string $key, int $length) : self; + + + public function lt_length(string $key, int $length) : self; + + + public function lt_eq_length(string $key, int $length) : self; + + + public function eq_left(string $key, int $width, string $value) : self; + + + public function not_eq_left(string $key, int $width, string $value) : self; + + + public function gt_left(string $key, int $width, string $value) : self; + + + public function gt_eq_left(string $key, int $width, string $value) : self; + + + public function lt_left(string $key, int $width, string $value) : self; + + + public function lt_eq_left(string $key, int $width, string $value) : self; } diff --git a/src/Storm/Query/Event.php b/src/Storm/Query/Event.php new file mode 100644 index 0000000000000000000000000000000000000000..82189ef539ea7a20e08d45454072c35e31114658 --- /dev/null +++ b/src/Storm/Query/Event.php @@ -0,0 +1,45 @@ +<?php +/* +STORM is under the MIT License (MIT) + +Copyright (c) 2010-2021 Agence Française Informatique http://www.afi-sa.fr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + + +class Storm_Query_Event extends Storm_Event_Abstract { + + protected Storm_Query_Abstract $_query; + + public function __construct(Storm_Query_Abstract $query) { + $this->_query = $query; + } + + + public function getQuery() : Storm_Query_Abstract { + return $this->_query; + } + + + public function isQueryEvent() { + return true; + } +} diff --git a/src/Storm/Query/Exists.php b/src/Storm/Query/Exists.php new file mode 100644 index 0000000000000000000000000000000000000000..d2605456e35fa865bb81ac763f8afbbf6e1f403a --- /dev/null +++ b/src/Storm/Query/Exists.php @@ -0,0 +1,130 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Exists extends Storm_Query_Sub implements Storm_Query_SubInterface { + + public function sql() : string { + $this->getHelper()->addSelect([new Zend_Db_Expr("'x'")]); + + $operator = Storm_Query_Clause::CLAUSE_EXISTS; + if ($this->_is_not) + $operator = trim(Storm_Query_Clause::CLAUSE_NOT . $operator); + + return sprintf($operator . ' (%s)', $this->assembleDb()); + } + + + public function computeErrors() : array { + $errors = []; + + if ($this->getHelper()->hasSelect()) + $errors [] = 'Error: For Exists don\'t add select column'; + + if ( ! $this->_condition) + $errors [] = 'Error: For Exists condition "on" is mandatory'; + + return [...$errors, ...parent::computeErrors()]; + } + + + public function on_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)); + + return $this; + } + + + public function on_not_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beNotEqual()); + + return $this; + } + + + public function on_gt(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beGreater()); + + return $this; + } + + + public function on_gt_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beGreaterEqual()); + + return $this; + } + + + public function on_lt(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beLesser()); + + return $this; + } + + + public function on_lt_eq(string $parent_key, string $key) : self { + $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key) + ->beLesserEqual()); + + return $this; + } + + + protected function _sqlWhere() : string { + $sql = new Storm_Query_Sql; + + $conditions_sqls = $this->_condition + ? $this->_condition->sql() + : []; + foreach ($conditions_sqls as $condition_sql) + $sql->write($condition_sql); + + $this->_criteria->assemble($sql); + + foreach ($this->getSubQueries() as $sub) + $sql->write($sub->sql()); + + return $sql->implode($this->_criteria->separator()); + } + + + protected function _addCondition(Storm_Query_Condition $condition) : self { + if ($this->_condition) { + $this->_condition->add($condition); + + return $this; + } + + $this->_condition = $condition; + + return $this; + } +} diff --git a/src/Storm/Query/Helper.php b/src/Storm/Query/Helper.php new file mode 100644 index 0000000000000000000000000000000000000000..4219e243ab36a7241fc69e210e15542cf86bf0e8 --- /dev/null +++ b/src/Storm/Query/Helper.php @@ -0,0 +1,183 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Helper { + + protected Storm_Model_Loader $_loader; + protected string $_alias; + protected string $_table_name; + protected string $_identifier; + protected ?array $_select = null; + protected bool $_select_with_alias = false; + protected bool $_is_row = false; + + public function __construct(Storm_Model_Loader $loader, ?string $alias = null) { + $this->_loader = $loader; + $this->_table_name = $loader->getTable()->getName(); + $this->_alias = $alias ?? $this->_table_name; + $this->_identifier = Zend_Db_Table_Abstract::getDefaultAdapter() + ->quoteIdentifier($this->_alias, true); + } + + + public function addSelect(array $select) : self { + $this->_select = $select; + + return $this; + } + + + public function hasSelect() : bool { + return null !== $this->_select; + } + + + public function countSelect() : int { + return $this->_select ? count($this->_select) : 0; + } + + + public function beRow() : self { + $this->_is_row = true; + + return $this; + } + + + public function allSelect() : self { + $this->_select = []; + + return $this; + } + + + public function beWithAlias() : self { + if ($this->_select) + $this->_select_with_alias = true; + + return $this; + } + + + public function isWithAlias() : bool { + return $this->_select_with_alias; + } + + + public function getAlias() : string { + return $this->_alias; + } + + + public function getIdentifier() : string { + return $this->_identifier; + } + + + public function getLoader() : Storm_Model_Loader { + return $this->_loader; + } + + + public function getTableName() : string { + return $this->_table_name; + } + + + public function getFirstSelect() : string { + return $this->_select + ? $this->_select[0] ?? '' + : ''; + } + + + public function table() : array { + return [$this->_alias => $this->_table_name]; + } + + + public function loaderForFetch() : Storm_Model_BaseLoader { + return $this->_is_row + ? Storm_Model_Row::getLoader() + : $this->getLoader(); + } + + + /* @return = null | string | array */ + public function select() { + if (null === $this->_select) + return null; + + if (0 === count($this->_select)) + return Zend_Db_Select::SQL_WILDCARD; + + if ( ! $this->_select_with_alias) + return $this->_select; + + $selects = []; + foreach ($this->_select as $value) + $selects [$this->_alias . '_' . $value] = $value; + + return $selects; + } + + + public function selectDefault() : array { + if (null === $this->_select) + return []; + + if (0 === count($this->_select)) + return []; + + if ( ! $this->_select_with_alias) { + $selects = []; + foreach ($this->_select as $value) + $selects [$value] = null; + + return $selects; + } + + $selects = []; + foreach ($this->_select as $value) + $selects [$this->_alias . '_' . $value] = null; + + return $selects; + } + + + public function hasKeyInSelect(string $key) : bool { + if ( ! $this->hasSelect()) + return false; + + return $this->_isAllSelect() || in_array($key, $this->_select); + } + + + protected function _isAllSelect() : bool { + return 0 === count($this->_select); + } +} diff --git a/src/Storm/Query/In.php b/src/Storm/Query/In.php new file mode 100644 index 0000000000000000000000000000000000000000..70b75aab1ded14a3001198251f9fd412ce381d13 --- /dev/null +++ b/src/Storm/Query/In.php @@ -0,0 +1,67 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_In extends Storm_Query_Sub { + + public function sql() : string { + return ($parent_key = $this->_parentKey()) + ? Storm_Query_Clause::newFor(new Storm_Query_ClauseKey($parent_key), + Storm_Query_Clause::CLAUSE_IN, + [$this->assembleDb()]) + ->setNegated($this->_is_not) + ->setAlias($this->getParentHelper()->getIdentifier()) + ->getFormatDb(false) + : ''; + } + + + public function computeErrors() : array { + $errors = []; + + if (0 === ($count = $this->getHelper()->countSelect())) + $errors [] = 'Error: For In add select column is mandatory'; + + if ($count > 1) + $errors [] = 'Error: For In add select must have only one column'; + + return [...$errors, ...parent::computeErrors()]; + } + + + public function setCondition(Storm_Query_Condition $condition) : self { + $this->_condition = $condition->beIn(); + + return $this; + } + + + protected function _parentKey() : string { + return $this->_condition + ? $this->_condition->getParentKey() + : ''; + } +} diff --git a/src/Storm/Query/Instance.php b/src/Storm/Query/Instance.php new file mode 100644 index 0000000000000000000000000000000000000000..5ba974db548db224074a974f7073026b2edd4f04 --- /dev/null +++ b/src/Storm/Query/Instance.php @@ -0,0 +1,100 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Instance { + + protected array $_instances = []; + + public function addWith(string $alias, array $instance) : self { + $this->_instances [$alias] = $instance; + + return $this; + } + + + public function addFrom(Storm_Query_Instance $query_instance) : self { + $this->_instances = array_merge($this->_instances, $query_instance->getInstances()); + + return $this; + } + + + public function getInstances() : array { + return $this->_instances; + } + + + public function value(string $alias, string $key) { + return $this->_instances[$alias][$key] ?? null; + } + + + public function result(Storm_Query_Abstract $query, array $default_instance) : array { + $result_instance = []; + + foreach ($this->_instances as $alias => $instance) { + $helper = $this->findHelperBy($alias, $query); + $result_instance = array_merge($result_instance, + $this->_instanceWith($instance, $helper)); + } + + return array_merge($default_instance, $result_instance); + } + + + public function findHelperBy(string $alias, Storm_Query_Abstract $query) : ?Storm_Query_Helper { + if ($alias === $query->getHelper()->getAlias()) + return $query->getHelper(); + + foreach ($query->getSubQueries() as $sub_query) + if ($helper = $this->findHelperBy($alias, $sub_query)) + return $helper; + + foreach ($query->getJoins() as $join) + if ($helper = $this->findHelperBy($alias, $join)) + return $helper; + + return null; + } + + + protected function _instanceWith(array $instance, ?Storm_Query_Helper $helper) : array { + if (!$helper) + return []; + + $alias = ''; + if ($helper->isWithAlias()) + $alias = $helper->getAlias() . '_'; + + $alias_instance = []; + foreach ($instance as $k => $v) + if ($helper->hasKeyInSelect($k)) + $alias_instance [$alias . $k] = $v; + + return $alias_instance; + } +} diff --git a/src/Storm/Query/Instances.php b/src/Storm/Query/Instances.php new file mode 100644 index 0000000000000000000000000000000000000000..ba2f73377d7bbb9336dc8653b40af75ffe4db611 --- /dev/null +++ b/src/Storm/Query/Instances.php @@ -0,0 +1,106 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Instances { + + const INSTANCE_SEPARATOR = '@@'; + protected array $_instances = []; + + public function __construct(Storm_Query_Abstract $query) { + $this->_init($query); + } + + + protected function _init(Storm_Query_Abstract $query) : self { + foreach ($query->instances() as $alias => $instances) + $this->_instances [$alias] = $this->_addInstance($alias, $instances); + + return $this; + } + + + public function addAll(array $searchs_alias, Storm_Collection $collection) : self { + $all_alias = []; + foreach ($searchs_alias as $search_alias) + $all_alias [] = $this->_aliasKey($search_alias); + + $final_alias = []; + foreach ($all_alias as $alias) { + unset($this->_instances[$alias]); + $final_alias = [...$final_alias, ...explode(static::INSTANCE_SEPARATOR, $alias)]; + } + + $this->_instances [implode(static::INSTANCE_SEPARATOR, + array_unique(array_filter($final_alias)))] = $collection; + return $this; + } + + + public function instancesFor(string $search_alias) : Storm_Collection { + return ($alias = $this->_aliasKey($search_alias)) + ? $this->_instances[$alias] + : new Storm_Collection; + } + + + public function resultFor(Storm_Query_Abstract $query) : array { + $default_instance = $this->defaultInstance($query); + + return $this->instancesFor($query->getHelper()->getAlias()) + ->collect(fn($instance) => $instance->result($query, $default_instance)) + ->getArrayCopy(); + } + + + public function defaultInstance(Storm_Query_Abstract $query) : array { + $default = $query->getHelper()->selectDefault(); + + foreach ($query->getJoins() as $join) + $default = array_merge($default, $this->defaultInstance($join)); + + return $default; + } + + + protected function _addInstance(string $alias, array $instances) : Storm_Collection { + $collection = new Storm_Collection; + + foreach ($instances as $instance) + $collection->add((new Storm_Query_Instance)->addWith($alias, $instance)); + + return $collection; + } + + + protected function _aliasKey(string $search_alias) : string { + foreach ($this->_instances as $alias => $collection) + if (in_array($search_alias, explode(static::INSTANCE_SEPARATOR, $alias))) + return $alias; + + return ''; + } +} diff --git a/src/Storm/Query/Order.php b/src/Storm/Query/Order.php new file mode 100644 index 0000000000000000000000000000000000000000..71f3545c6fa5168a0a5de32bc091253f9aafd8d8 --- /dev/null +++ b/src/Storm/Query/Order.php @@ -0,0 +1,73 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Order { + + protected ?Storm_Query_Order $_next_query; + protected ?Storm_Query_Clause $_clause; + protected int $_position; + + public function __construct(array $orders, ?int $position = 0) { + $this->_position = $position++; + $this->_clause = array_shift($orders); + + $this->_next_query = ($orders + ? (new Storm_Query_Order($orders, $position)) + : null); + } + + + public function getNextQuery() : ?Storm_Query_Order { + if (!($next_query = $this->_next_query)) + return null; + + return $next_query->getClause() + ? $next_query + : null; + } + + + public function getClause() : ?Storm_Query_Clause { + return $this->_clause; + } + + + public function getPosition() : int { + return 10 ** $this->_position; + } + + + public function compareVolatile(array $models) : array { + if (!$models) + return $models; + + if ($clause = $this->getClause()) + usort($models, fn($a, $b) => $clause->compare($this, $a, $b)); + + return $models; + } +} diff --git a/src/Storm/Query/Sql.php b/src/Storm/Query/Sql.php index 53b347978859740a6b0c070175a3c3e208c80a99..bd9b7797787deaa990bc92479be88c918280366c 100644 --- a/src/Storm/Query/Sql.php +++ b/src/Storm/Query/Sql.php @@ -31,6 +31,7 @@ class Storm_Query_Sql { public function write(string $sql) : self { $this->_sql [] = $sql; + return $this; } diff --git a/src/Storm/Query/Sub.php b/src/Storm/Query/Sub.php new file mode 100644 index 0000000000000000000000000000000000000000..04da101de8df7733f3ed6d54d87f38efc114bd22 --- /dev/null +++ b/src/Storm/Query/Sub.php @@ -0,0 +1,84 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +class Storm_Query_Sub extends Storm_Query_Abstract { + + protected bool $_is_not = false; + protected ?Storm_Query_Helper $_parent_helper = null; + protected ?Storm_Query_Condition $_condition = null; + + public function __construct(Storm_Model_Loader $loader, ?string $alias = null) { + $this->_helper = new Storm_Query_Helper($loader, $alias); + $this->_criteria = (new Storm_Query_Criteria) + ->setAlias($this->_helper->getIdentifier()); + } + + + public function setParentHelper(Storm_Query_Helper $parent_helper) : self { + $this->_parent_helper = $parent_helper; + + return $this; + } + + + public function getParentHelper() : ?Storm_Query_Helper { + return $this->_parent_helper; + } + + + public function beNot() : self { + $this->_is_not = true; + + return $this; + } + + + public function isNot() : bool { + return $this->_is_not; + } + + + public function filterWith(Storm_Query_Instances $query_instances) : self { + if ($this->_condition) + $this->_condition->filterWith($query_instances); + + foreach ($this->getSubQueries() as $sub) + $sub->filterWith($query_instances); + + return $this; + } + + + public function computeErrors() : array { + $errors = []; + + foreach ($this->getSubQueries() as $sub) + $errors = [...$errors, ...$sub->computeErrors()]; + + return $errors; + } +} diff --git a/src/Storm/Query/SubInterface.php b/src/Storm/Query/SubInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..696142403249f31c529325a00ba1f5cba8355643 --- /dev/null +++ b/src/Storm/Query/SubInterface.php @@ -0,0 +1,46 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +interface Storm_Query_SubInterface { + + public function on_eq(string $parent_key, string $key) : self; + + + public function on_not_eq(string $parent_key, string $key) : self; + + + public function on_gt(string $parent_key, string $key) : self; + + + public function on_gt_eq(string $parent_key, string $key) : self; + + + public function on_lt(string $parent_key, string $key) : self; + + + public function on_lt_eq(string $parent_key, string $key) : self; +} diff --git a/src/Storm/Query/Trait.php b/src/Storm/Query/Trait.php new file mode 100644 index 0000000000000000000000000000000000000000..1c1d55a2e7ab4850c5b4e47792d4604fac651db3 --- /dev/null +++ b/src/Storm/Query/Trait.php @@ -0,0 +1,140 @@ +<?php +/* + STORM is under the MIT License (MIT) + + Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +trait Storm_Query_Trait { + + protected bool $_distinct = false; + protected array $_orders = []; + protected ?Storm_Query_Clause $_clause_group_by = null; + + public function distinct(array $select) : self { + $this->_distinct = true; + + return $this->select($select); + } + + + public function group(string $value) : self { + $this->_clause_group_by = Storm_Query_Clause::group($value); + + return $this; + } + + + /** + * @param $key_or_clause string|Storm_Query_MatchRating + */ + public function order($key_or_clause, ?string $value = null) : self { + $this->_orders [] = $this->_order($key_or_clause, $value); + + return $this; + } + + + /** + * @param $key_or_clause string|Storm_Query_MatchRating + */ + public function order_desc($key_or_clause, ?string $value = null) : self { + $this->_orders [] = $this->_order($key_or_clause, $value) + ->beDesc(); + + return $this; + } + + + /** + * @param $key_or_clause string|Storm_Query_MatchRating + */ + protected function _order($key_or_clause, ?string $value = null) : Storm_Query_Clause { + if ($key_or_clause instanceof Storm_Query_MatchRating) + return Storm_Query_Clause::order($key_or_clause->getKey(), + Storm_Query_Clause::match($key_or_clause)); + + return Storm_Query_Clause::order($key_or_clause, $value); + } + + + public function fetchAll() : array { + return $this->_prepareHelper() + ->loaderForFetch() + ->fetchForQuery($this); + } + + + public function count() : int { + return $this->_prepareHelper() + ->loaderForFetch() + ->countForQuery($this); + } + + + public function fetchFirst() : ?Storm_Model_Interface { + return $this->_prepareHelper() + ->loaderForFetch() + ->fetchFirstForQuery($this); + } + + + /** DB management */ + + protected function _assembleGroupBy(Zend_Db_Table_Select $select) : self { + if ($this->_clause_group_by) + $this->_clause_group_by->setAlias($this->getHelper()->getAlias())->assemble($select); + + return $this; + } + + + protected function _assembleOrders(Zend_Db_Table_Select $select) : self { + foreach ($this->_orders as $order) + $order->setAlias($this->getHelper()->getIdentifier())->assemble($select); + + return $this; + } + + + /** Volatile management */ + + public function orderVolatile(array $models) : array { + return (new Storm_Query_Order(array_reverse($this->_orders))) + ->compareVolatile($models); + } + + + public function getGroupByValue() : string { + return $this->_clause_group_by + ? $this->_clause_group_by->getValue() + : ''; + } + + + public function notify() : self { + Storm_Events::getInstance()->notify(new Storm_Query_Event($this)); + + return $this; + } +} diff --git a/src/Storm/Test/ModelTestCase.php b/src/Storm/Test/ModelTestCase.php index bf7534a240423b308cbaa6df428239418652c421..bcc6cf968b7c13e1e447dc9dc8b47ef8921bd776 100644 --- a/src/Storm/Test/ModelTestCase.php +++ b/src/Storm/Test/ModelTestCase.php @@ -25,51 +25,26 @@ THE SOFTWARE. */ abstract class Storm_Test_ModelTestCase extends PHPUnit_Framework_TestCase { - use Storm_Test_THelpers; + use Storm_Test_THelpers, Storm_Test_TQueryTests; - protected $_old_adapter; protected bool $_storm_mock_zend_adapter = true; protected function setUp() { Storm_Model_Abstract::unsetLoaders(); - $this->_old_adapter = Zend_Db_Table_Abstract::getDefaultAdapter(); - - if (!$this->_storm_mock_zend_adapter) - return; - - $adapter = new class() extends Zend_Db_Adapter_Mysqli - { - public function __construct($config=[]) { } - - - public function getConfig() { - return ['dbname' => 'mockdb']; - } - - - protected function _connect() { - $this->_connection = Storm_Test_ObjectWrapper::mock() - ->whenCalled('real_escape_string') - ->willDo(fn($value) => $value); - - return true; - } - }; - - Zend_Db_Table_Abstract::setDefaultAdapter($adapter); + $this->_querySetUp(); } protected function tearDown() { Storm_Model_Abstract::unsetLoaders(); - Zend_Db_Table_Abstract::setDefaultAdapter($this->_old_adapter); + $this->_queryTearDown(); } public function assertJSONEquals($expectedJSON, $actualJSON, $message='') { $this->assertEquals(json_decode($expectedJSON), - json_decode($actualJSON), - $message); + json_decode($actualJSON), + $message); } } diff --git a/src/Storm/Test/QueriesObserver.php b/src/Storm/Test/QueriesObserver.php new file mode 100644 index 0000000000000000000000000000000000000000..d2cb8dbd4875f4ea0cee705b6aa38d1824012a10 --- /dev/null +++ b/src/Storm/Test/QueriesObserver.php @@ -0,0 +1,127 @@ +<?php +/* +STORM is under the MIT License (MIT) + +Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + + +class Storm_Test_QueriesObserver { + + protected Storm_Collection $_queries; + protected Storm_Set $_scopes; + + public function __construct() { + $this->_queries = new Storm_Collection; + $this->_scopes = new Storm_Set; + } + + + public function addScope(string $scope) : self { + $this->_scopes->add($scope); + + return $this; + } + + + public function __invoke(Storm_Event_Abstract $event) : self { + if ($this->_scopes->isEmpty() || !$event->isQueryEvent()) + return $this; + + ob_start(); + debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $stack = ob_get_contents(); + ob_end_clean(); + + if ($this->_scopes->detect(fn($scope) => false !== strpos($stack, $scope))) + $this->_addQuery($event->getQuery()); + + return $this; + } + + + public function detectWith(string $sql, ?int $count = null) : ?Storm_Test_QueriesObserverSql { + return $this->_queries + ->detect(fn($observer) => $sql === $observer->getSql() + && (null === $count || $count === $observer->getCount())); + } + + + public function queriesMessages() : array { + return $this->_queries + ->collect(fn($observer) => $observer->getSql() + . sprintf(' / Called %d time(s)', $observer->getCount())) + ->getArrayCopy(); + } + + + protected function _addQuery(Storm_Query_Abstract $query) : self { + $query_observer = new Storm_Test_QueriesObserverSql($query); + + $old_observer = $this->_queries + ->detect(fn($observer) => $observer->getKey() === $query_observer->getKey()); + + if (!$old_observer) { + $this->_queries->add($query_observer); + $old_observer = $query_observer; + } + + $old_observer->increment(); + return $this; + } +} + + + + +class Storm_Test_QueriesObserverSql { + + protected string $_sql = ''; + protected string $_key = ''; + protected int $_count = 0; + + public function __construct(Storm_Query_Abstract $query) { + $this->_sql = $query->assembleVolatile(); + $this->_key = md5($this->_sql); + } + + + public function getKey() : string { + return $this->_key; + } + + + public function getSql() : string { + return $this->_sql; + } + + + public function getCount() : int { + return $this->_count; + } + + + public function increment() : self { + $this->_count++; + return $this; + } +} diff --git a/src/Storm/Test/TQueryTests.php b/src/Storm/Test/TQueryTests.php new file mode 100644 index 0000000000000000000000000000000000000000..19c2bffc8319b57db51f49f570c688de3c3f1504 --- /dev/null +++ b/src/Storm/Test/TQueryTests.php @@ -0,0 +1,145 @@ +<?php +/* +STORM is under the MIT License (MIT) + +Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + + +trait Storm_Test_TQueryTests { + + protected $_old_adapter; + protected array $_storm_scopes = []; + protected ?Storm_Test_QueriesObserver $_storm_queries_observer = null; + + protected function _querySetUp() : void { + $this->_old_adapter = Zend_Db_Table_Abstract::getDefaultAdapter(); + + if (!$this->_storm_mock_zend_adapter) + return; + + $adapter = new class() extends Zend_Db_Adapter_Mysqli { + + public function __construct($config=[]) { } + + + public function getConfig() { + return ['dbname' => 'mockdb']; + } + + + protected function _connect() { + $this->_connection = Storm_Test_ObjectWrapper::mock() + ->whenCalled('real_escape_string') + ->willDo(fn($value) => $value); + + return true; + } + }; + + Zend_Db_Table_Abstract::setDefaultAdapter($adapter); + + $cache_core = new class() extends Zend_Cache_Core { + + public function load($id, + $doNotTestCacheValidity = false, + $doNotUnserialize = false) { + return ['CLEF' => ['SCHEMA_NAME' => null, + 'TABLE_NAME' => 'mock_table', + 'COLUMN_NAME' => 'CLEF', + 'COLUMN_POSITION' => 1, + 'DATA_TYPE' => 'varchar', + 'DEFAULT' => null, + 'NULLABLE' => false, + 'LENGTH' => 100, + 'SCALE' => null, + 'PRECISION' => null, + 'UNSIGNED' => null, + 'PRIMARY' => true, + 'PRIMARY_POSITION' => 1, + 'IDENTITY' => false] + ]; + } + }; + + Zend_Db_Table_Abstract::setDefaultMetadataCache($cache_core); + Storm_Model_Row::beVolatile(); + + $this->_registerScopes(); + } + + + protected function _queryTearDown() : void { + Zend_Db_Table_Abstract::setDefaultAdapter($this->_old_adapter); + Zend_Db_Table_Abstract::setDefaultMetadataCache(null); + } + + + public function assertSql(string $sql) : void { + $this->assertNotNull($this->_queriesObserver()->detectWith($sql), + $this->_queryMessage($sql)); + } + + + public function assertSqlCount(string $sql, int $count) : void { + $this->assertNotNull($this->_queriesObserver()->detectWith($sql, $count), + $this->_queryMessage($sql)); + } + + + public function assertNotSql(string $sql) : void { + $this->assertNull($this->_queriesObserver()->detectWith($sql), + $this->_queryMessage($sql, false)); + } + + + protected function _queriesObserver() : Storm_Test_QueriesObserver { + return $this->_storm_queries_observer + ? $this->_storm_queries_observer + : ($this->_storm_queries_observer = new Storm_Test_QueriesObserver); + } + + + protected function _queryMessage(string $sql, ?bool $negated = true) : string { + $queries = $this->_queriesObserver()->queriesMessages(); + $queries = $queries ? $queries : ['No result']; + + return sprintf("There's %s match for this query:\n - %s\nIN:\n + %s\n", + $negated ? 'no' : 'a', + $sql, + implode("\n + ", $queries)); + } + + + protected function _registerScopes() : self { + if ( ! $this->_storm_scopes) + return $this; + + $this->_storm_queries_observer = new Storm_Test_QueriesObserver; + + foreach ($this->_storm_scopes as $scope) + $this->_storm_queries_observer->addScope($scope); + + Storm_Events::getInstance()->register($this->_storm_queries_observer); + + return $this; + } +} diff --git a/tests/Storm/Model/TableReconnectTest.php b/tests/Storm/Model/TableReconnectTest.php index ac0a4663a0f90c1131c774b6cb870781cf243003..9fa345043e7600f31a1ad90acc36fe9738a7b6e5 100644 --- a/tests/Storm/Model/TableReconnectTest.php +++ b/tests/Storm/Model/TableReconnectTest.php @@ -25,7 +25,9 @@ THE SOFTWARE. */ class Storm_Model_TableReconnectTest extends Storm_Test_ModelTestCase { + protected $_mock; + protected bool $_storm_mock_zend_adapter = false; public function setUp() { parent::setUp(); @@ -182,4 +184,4 @@ class Storm_Model_TableReconnectTest_Adapter extends Zend_Db_Adapter_Mysqli { class Storm_Model_TableReconnectTest_Model extends Storm_Model_Abstract { protected $_table_name = 'reconnect_test'; -} \ No newline at end of file +} diff --git a/tests/Storm/Test/LoaderQueryTest.php b/tests/Storm/Test/LoaderQueryTest.php index 1a3464fe985493f178cdcc88e10200d94533a101..9aa6bc6c08e8723154095ed6e589ed19218889fd 100644 --- a/tests/Storm/Test/LoaderQueryTest.php +++ b/tests/Storm/Test/LoaderQueryTest.php @@ -24,491 +24,556 @@ */ -abstract class Storm_Test_LoaderQueryTestCase extends Storm_Test_ModelTestCase { +class Storm_Test_LoaderQueryVolatileClauseWhereTest + extends Storm_Test_ModelTestCase { - protected - $_select, - $_table, - $_loader; + protected array $_storm_scopes = ['LoaderQueryVolatileClauseWhereTest']; public function setUp() { parent::setUp(); - $this->_table = $this->mock(); - $this->_loader = Storm_Test_Mock_User::getLoader()->setTable($this->_table); - } -} - - - - -class Storm_Test_LoaderQueryDbTest extends Storm_Test_LoaderQueryTestCase { - - public function setUp() { - parent::setUp(); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 100, + 'login' => 'user_admin', + 'level' => 'invite admin redacteur', + 'foo' => 'premier deuxieme troisieme', + 'prefs' => null, + 'sys' => '', + 'date' => '2022-01-01']); - $this->_table - ->whenCalled('select')->answers($this->_select = $this->mock()) - ->whenCalled('fetchAll')->with($this->_select) - ->answers(new Zend_Db_Table_Rowset([])); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 101, + 'login' => 'user_administrateur', + 'level' => 'administrateur', + 'foo' => 'deuxieme redacteur', + 'prefs' => '{"best":true}', + 'sys' => null, + 'date' => '2020-01-01']); - $this->_select->whenCalled('where')->answers($this->_select) - ->whenCalled('getTable')->answers($this->_table); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 102, + 'login' => 'user_invite', + 'level' => 'invite', + 'foo' => 'premier deuxieme', + 'url' => 'https://my-server.org/page_1.html', + 'website' => 'https://my-server.org/pomme+d\'api.php page2.php', + 'sys' => 'content', + 'date' => '2021-01-01']); } /** @test */ - public function withClauseGreaterThanShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->gt('id', '30') - ->fetchAll(); - - $this->assertEquals(['id>\'30\''], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_GT_100_ShouldAnswersUserAdministrateurAndUserInvite() { + $this->assertEquals(['user_administrateur', 'user_invite'], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->gt('id', 100) + ->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 100)'); } /** @test */ - public function withClauseGreaterThanEqualShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->gt_eq('id', '30') - ->fetchAll(); - - $this->assertEquals(['id>=\'30\''], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_GT_EQ_101_ShouldAnswersUserAdministrateurAndUserInvite() { + $this->assertEquals(['user_administrateur', 'user_invite'], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->gt_eq('id', 101) + ->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` >= 101)'); } /** @test */ - public function withClauseLesserThanShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->lt('id', '30') - ->fetchAll(); - - $this->assertEquals(['id<\'30\''], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_LT_102_ShouldAnswersUserAdminAndUserAdministrateur() { + $this->assertEquals(['user_admin', 'user_administrateur'], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->lt('id', 102) + ->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` < 102)', 1); } /** @test */ - public function withClauseLesserThanEqualShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->lt_eq('id', '30') - ->fetchAll(); - - $this->assertEquals(['id<=\'30\''], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_LT_EQ_101_ShouldAnswersUserAdminAndUserAdministrateur() { + $this->assertEquals(['user_admin', 'user_administrateur'], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->lt_eq('id', 101) + ->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` <= 101)'); + $this->assertNotSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 101)'); } /** @test */ - public function withClauseIsShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->is_null('name') - ->fetchAll(); - - $this->assertEquals(['name is null'], - $this->_select->getAttributesForLastCallOn('where')); + public function whereLogin_NOT_EQ_UserAdministrateurShouldReturnUserAdminAndUserInvite() { + $this->assertEquals(['user_admin', 'user_invite'], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->not_eq('login', 'user_administrateur') + ->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`login` != \'user_administrateur\')'); } /** @test */ - public function withClauseIsNotShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->not_is_null('name') - ->fetchAll(); - - $this->assertEquals(['name is not null'], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_IN_100And102_AndFoo_NOT_LIKE_TroisieShouldReturnId_102() { + $this->assertEquals([102], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->in('id', [100, 102]) + ->not_like('foo', '%troisie%') + ->fetchAll())) + ->collect('id') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 102) AND `users`.`foo` NOT LIKE \'%troisie%\')'); } /** @test */ - public function withClauseInShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->in('name', ['Harlock', 'Nausicaa']) - ->fetchAll(); - - $this->assertEquals(['name in (\'Harlock\', \'Nausicaa\')'], - $this->_select->getAttributesForLastCallOn('where')); + public function whereId_IN_100And101_AndFoo_START_DeuxShouldReturnId_101() { + $this->assertEquals([101], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->in('id', [100, 101]) + ->start('foo', 'deux') + ->fetchAll())) + ->collect('id') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 101) AND `users`.`foo` LIKE \'deux%\')'); } /** @test */ - public function withClauseNotInShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->not_in('name', ['Harlock', 'Nausicaa']) - ->fetchAll(); - - $this->assertEquals(['name not in (\'Harlock\', \'Nausicaa\')'], - $this->_select->getAttributesForLastCallOn('where')); + public function whereFoo_END_ActeurShouldReturnId_101() { + $this->assertEquals([101], + (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query() + ->end('foo', 'acteur') + ->fetchAll())) + ->collect('id') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`foo` LIKE \'%acteur\')'); } /** @test */ - public function withClauseEqualShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->eq('left(name, 5)', 'ganda') - ->fetchAll(); + public function withMatchLevelAgainstAdminShouldAnswersOnlyUser_Admin() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level')) + ->against(['admin'])); - $this->assertEquals(['left(name, 5)=\'ganda\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level) AGAINST(\'admin\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseNotEqualShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->not_eq('name', 'Harlock') - ->fetchAll(); + public function withMatchLevelAgainstAdminDeuxiemeShouldAnswersUser_AdminAndUser_AdministrateurAndUser_Invite() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level,foo')) + ->against(['admin', 'deuxieme'])); - $this->assertEquals(['name!=\'Harlock\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'admin deuxieme\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseLikeShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->like('login', '%aus%') - ->fetchAll(); + public function withMatchLevelAgainstStrictAdminRedacteur_DeuxiemeShouldAnswersUser_AdminAndUser_Administrateur() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level,foo', true)) + ->against(['admin', 'redacteur']) + ->against(['deuxieme'])); - $this->assertEquals(['login like \'%aus%\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin', 'user_administrateur'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'+(admin redacteur) +deuxieme\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseLikeAndSlashesShouldReturnQueryWith() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->like('login', '%https://my-server.org%') - ->fetchAll(); + public function withMatchLevelFooExactDeuxiemeRedacteurShouldAnswersOnlyUser_Administrateur() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level,foo')) + ->exact('deuxieme redacteur')); - $this->assertEquals(['login like \'%https://my-server.org%\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_administrateur'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'"deuxieme redacteur"\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseLikeAndPlusAndQuoteShouldReturnQueryWith() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->like('login', '%https://my-server.org/pomme+d\'api%') - ->fetchAll(); + public function withMatchLevelFooDeuxiemeAndAgainstRedacteurAnswers_AllUsers() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level,foo')) + ->against('deuxieme') + ->against('redacteur')); - $this->assertEquals(['login like \'%https://my-server.org/pomme+d\'api%\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'deuxieme redacteur\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseNotLikeShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->not_like('login', '%aus%') - ->fetchAll(); + public function withMatchLevelFooDeuxiemeAndAgainstStrictRedacteurAnswers_User_Admin_User_Administrateur() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('level,foo')) + ->against('deuxieme') + ->against('redacteur', true)); - $this->assertEquals(['login not like \'%aus%\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin', 'user_administrateur'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'deuxieme +redacteur\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseStartShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->start('login', 'aus') - ->fetchAll(); + public function withOrClause_LevelInviteOrFooLikePremierShouldAnswersUser_AdminAndUser_Invite() { + $query = Storm_Test_LoaderQueryUser::query() + ->or((new Storm_Query_Criteria) + ->eq('level', 'invite') + ->like('foo', '%premier%')); - $this->assertEquals(['login like \'aus%\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_admin', 'user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE ((`users`.`level` = \'invite\' OR `users`.`foo` LIKE \'%premier%\'))'); } /** @test */ - public function withClauseEndShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->end('login', 'aus') - ->fetchAll(); + public function withAndClause_LevelInviteOrFooLikePremierShouldAnswersOnlyUser_Invite() { + $query = Storm_Test_LoaderQueryUser::query() + ->and((new Storm_Query_Criteria) + ->eq('level', 'invite') + ->like('foo', '%premier%')); - $this->assertEquals(['login like \'%aus\''], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE ((`users`.`level` = \'invite\' AND `users`.`foo` LIKE \'%premier%\'))'); } /** @test */ - public function withClauseOrShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->or((new Storm_Query_Criteria) - ->eq('login', 'aus') - ->in('role', ['admin', 'invite'])) - ->fetchAll(); + public function urlLikeHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() { + $query = Storm_Test_LoaderQueryUser::query() + ->like('url', '%https://my-server.org%'); - $this->assertEquals(['(login=\'aus\' or role in (\'admin\', \'invite\'))'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`url` LIKE \'%https://my-server.org%\')'); } /** @test */ - public function withClauseAndShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->and((new Storm_Query_Criteria) - ->eq('login', 'aus') - ->in('role', ['admin', 'invite'])) - ->fetchAll(); + public function websiteLikeHttpsDoubleDotSlashSlashPlusQuotePommeShouldAnswersOnlyUserWithSlashesPlusAndQuote() { + $query = Storm_Test_LoaderQueryUser::query() + ->like('website', '%https://my-server.org/pomme+d\'api%'); - $this->assertEquals(['(login=\'aus\' and role in (\'admin\', \'invite\'))'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`website` LIKE \'%https://my-server.org/pomme+d\'api%\')'); } /** @test */ - public function withClauseMatchShouldReturnQueryInBooleanMode() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->match((new Storm_Query_MatchBoolean('login, role')) - ->against(['ADMIN', 'INVITE'])) - ->fetchAll(); + public function urlMatchHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() { + $query = Storm_Test_LoaderQueryUser::query() + ->match((new Storm_Query_MatchBoolean('website'))->against('https://my-server.org/pomme+d\'api.php')); - $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE)'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals(['user_invite'], + (new Storm_Model_Collection($query->fetchAll())) + ->collect('login') + ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(website) AGAINST(\'https://my-server.org/pomme+d\'api.php\' IN BOOLEAN MODE))'); } /** @test */ - public function withClauseMatchMultipleTermsShouldReturnQueryInBooleanMode() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->match((new Storm_Query_MatchBoolean('login,role')) - ->against('ADMIN INVITE') - ->against('HUGO ADRIEN')) - ->fetchAll(); + public function withIsNullPrefsShouldAnswersUser_100_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->is_null('prefs'); - $this->assertEquals(['MATCH(login,role) AGAINST(\'ADMIN INVITE HUGO ADRIEN\' IN BOOLEAN MODE)'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`prefs` IS null)'); } /** @test */ - public function withClauseMatchStrictAndOneExactExpressionWithBooleanModeShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->match((new Storm_Query_MatchBoolean(['login', 'role'], true)) - ->exact('ADMIN INVITE') - ->against('HUGO')) - ->fetchAll(); + public function withIsNotNullPrefsShouldAnswersUsers_101_and_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->not_is_null('prefs'); - $this->assertEquals(['MATCH(login, role) AGAINST(\'+"ADMIN INVITE" +HUGO\' IN BOOLEAN MODE)'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), + Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`prefs` IS NOT null)'); } /** @test */ - public function withClauseMatchOneExactAndOneAgainstStrictInBooleanModeShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->match((new Storm_Query_MatchBoolean(['login', 'role'])) - ->exact('ADMIN INVITE', true) - ->against('HUGO')) - ->fetchAll(); + public function withNotInInviteAdministrateurShouldAnswerUser_100_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->not_in('level', ['invite', 'administrateur']); - $this->assertEquals(['MATCH(login, role) AGAINST(\'+"ADMIN INVITE" HUGO\' IN BOOLEAN MODE)'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`level` NOT IN (\'invite\', \'administrateur\'))'); } /** @test */ - public function withClauseMatchOneExactStrictAndOneAgainstInBooleanModeShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->match((new Storm_Query_MatchBoolean(['login', 'role'])) - ->exact('ADMIN INVITE') - ->against('HUGO', true)) - ->fetchAll(); + public function withClauseLimitOneInIntAndStringShouldReturnOnlyUser_100() { + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], + Storm_Test_LoaderQueryUser::query() + ->limit(1) + ->fetchAll()); - $this->assertEquals(['MATCH(login, role) AGAINST(\'"ADMIN INVITE" +HUGO\' IN BOOLEAN MODE)'], - $this->_select->getAttributesForLastCallOn('where')); + $this->assertSql('SELECT `users`.* FROM `users` LIMIT 1'); } -} - + /** @test */ + public function withEqualLengthOnColumnLevelShouldAnswerUser_102_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->eq_length('level', 6); -class Storm_Test_LoaderQueryLimitTest extends Storm_Test_LoaderQueryTestCase { - - public function setUp() { - parent::setUp(); - - $this->_table - ->whenCalled('select')->answers($this->_select = $this->mock()) - ->whenCalled('fetchAll')->with($this->_select) - ->answers(new Zend_Db_Table_Rowset([])); - - $this->_select->whenCalled('limit')->answers($this->_select); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) = 6)'); } /** @test */ - public function withClauseLimit30ShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->limit(30) - ->fetchAll(); + public function withNotEqualLengthOnColumnLevelShouldAnswerUser_100_101() { + $query = Storm_Test_LoaderQueryUser::query() + ->not_eq_length('level', 6); - $this->assertEquals([30], - $this->_select->getAttributesForLastCallOn('limit')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(101)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) != 6)'); } /** @test */ - public function withClauseLimit30Offset10ShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->limit('10, 30') - ->fetchAll(); + public function withGreaterLengthOnColumnLevelShouldAnswerUser_100_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->gt_length('level', 14); - $this->assertEquals(['10, 30'], - $this->_select->getAttributesForLastCallOn('limit')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) > 14)'); } -} - + /** @test */ + public function withGreaterEqualLengthOnColumnLevelShouldAnswerUser_100_101() { + $query = Storm_Test_LoaderQueryUser::query() + ->gt_eq_length('level', 14); -class Storm_Test_LoaderQueryLimitPageTest extends Storm_Test_LoaderQueryTestCase { + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(101)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) >= 14)'); + } - public function setUp() { - parent::setUp(); - $this->_table - ->whenCalled('select')->answers($this->_select = $this->mock()) - ->whenCalled('fetchAll')->with($this->_select) - ->answers(new Zend_Db_Table_Rowset([])); + /** @test */ + public function withLesserLengthOnColumnLevelShouldAnswerUser_102_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->lt_length('level', 14); - $this->_select->whenCalled('limitPage')->answers($this->_select); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) < 14)'); } /** @test */ - public function withClauseLimitPageShouldReturnQuery() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->limit_page([1, 100]) - ->fetchAll(); + public function withLesserEqualLengthOnColumnLevelShouldAnswerUser_101_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->lt_eq_length('level', 14); - $this->assertEquals([1, 100], - $this->_select->getAttributesForLastCallOn('limitPage')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), + Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) <= 14)'); } -} - + /** @test */ + public function withEqualLeftWidth4OnColumnDateShouldAnswerUser_101_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->eq_left('date', 4, '2020'); -class Storm_Test_LoaderQueryOrderTest extends Storm_Test_LoaderQueryTestCase { + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) = \'2020\')'); + } - public function setUp() { - parent::setUp(); - $this->_table - ->whenCalled('select')->answers($this->_select = $this->mock()) - ->whenCalled('fetchAll')->with($this->_select) - ->answers(new Zend_Db_Table_Rowset([])); + /** @test */ + public function withNotEqualLeftWidth4OnColumnDateShouldAnswerUser_100_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->not_eq_left('date', 4, '2020'); - $this->_select->whenCalled('order')->answers($this->_select) - ->whenCalled('getTable')->answers($this->_table); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) != \'2020\')'); } /** @test */ - public function withClauseOrderIdShouldReturnOrderId() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->order('id') - ->fetchAll(); + public function withGreaterLeftWidth4OnColumnDateShouldAnswerUser_100_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->gt_left('date', 4, '2021'); - $this->assertEquals(['id'], - $this->_select->getAttributesForLastCallOn('order')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) > \'2021\')'); } /** @test */ - public function withClauseOrderIdDescShouldReturnOrderIdDesc() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->order_desc('id') - ->fetchAll(); + public function withGreaterEqualLeftColumnDateShouldAnswerUser_100_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->gt_eq_left('date', 4, '2021'); - $this->assertEquals(['id desc'], - $this->_select->getAttributesForLastCallOn('order')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) >= \'2021\')'); } /** @test */ - public function withMultipleClauseOrderShouldReturnAllOrders() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->order_desc('id') - ->order('name') - ->order_desc('role') - ->fetchAll(); + public function withLesserLeftColumnDateShouldAnswerUser_101_Only() { + $query = Storm_Test_LoaderQueryUser::query() + ->lt_left('date', 4, '2021'); - $this->assertEquals(['id desc', 'name', 'role desc'], - [$this->_select->getFirstAttributeForMethodCallAt('order', 0), - $this->_select->getFirstAttributeForMethodCallAt('order', 1), - $this->_select->getFirstAttributeForMethodCallAt('order', 2)]); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) < \'2021\')'); } /** @test */ - public function withMatchClauseOrderShouldReturnMatchOrder() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->order((new Storm_Query_MatchRating('login, role')) - ->against(['ADMIN', 'INVITE'])) - ->fetchAll(); + public function withLesserEqualLeftColumnDateShouldAnswerUser_101_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->lt_eq_left('date', 4, '2021'); - $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\')'], - $this->_select->getAttributesForLastCallOn('order')); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), + Storm_Test_LoaderQueryUser::find(102)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) <= \'2021\')'); } /** @test */ - public function withMatchClauseOrderDescShouldReturnMatchOrder() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->order_desc((new Storm_Query_MatchBoolean('login, role')) - ->against(['ADMIN', 'INVITE'])) - ->fetchAll(); + public function withClauseLimitPageShouldPaginate() { + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(101)], + Storm_Test_LoaderQueryUser::query() + ->limit_page([0, 2]) + ->fetchAll()); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(101)], + Storm_Test_LoaderQueryUser::query() + ->limit_page([1, 2]) + ->fetchAll()); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)], + Storm_Test_LoaderQueryUser::query() + ->limit_page([2, 2]) + ->fetchAll()); - $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE) desc'], - $this->_select->getAttributesForLastCallOn('order')); + $this->assertSqlCount('SELECT `users`.* FROM `users` LIMIT 2', + 2); + $this->assertSqlCount('SELECT `users`.* FROM `users` LIMIT 2 OFFSET 2', + 1); } -} - - - - -class Storm_Test_LoaderQueryGroupByTest extends Storm_Test_LoaderQueryTestCase { - - public function setUp() { - parent::setUp(); - $this->_table - ->whenCalled('select')->answers($this->_select = $this->mock()) - ->whenCalled('fetchAll')->with($this->_select) - ->answers(new Zend_Db_Table_Rowset([])); - $this->_select->whenCalled('group')->answers($this->_select); + /** @test */ + public function withClauseLimitPageAndAttributeShouldWork() { + $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), + Storm_Test_LoaderQueryUser::find(102)], + Storm_Test_LoaderQueryUser::query() + ->start('foo', 'premier') + ->limit_page([1, 2]) + ->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`foo` LIKE \'premier%\') LIMIT 2'); } /** @test */ - public function withGroupByLoaderShouldCallGroupOnDbSelect() { - Storm_Query::from(Storm_Test_Mock_User::class) - ->group('name') - ->fetchAll(); - - $this->assertEquals(['name'], - $this->_select->getAttributesForLastCallOn('group')); - } -} - + public function queriesObserverShouldDisplaySqlAndCallCount() { + foreach (range(1, 2) as $range) + Storm_Test_LoaderQueryUser::query() + ->eq('id', 100) + ->fetchAll(); + foreach (range(1, 5) as $range) + Storm_Test_LoaderQueryUser::query() + ->gt('id', 100) + ->fetchAll(); + Storm_Test_LoaderQueryUser::query() + ->eq('id', 100) + ->fetchAll(); -class Storm_Test_LoaderQueryUser extends Storm_Model_Abstract { + foreach (range(1, 4) as $range) + Storm_Test_LoaderQueryUser::query() + ->in('id', [100, 102]) + ->gt('id', 100) + ->fetchAll(); - protected $_table_name = 'users'; + $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` = 100)', + 3); + $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 100)', + 5); + $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 102) AND `users`.`id` > 100)', + 4); + } } -class Storm_Test_LoaderQueryVolatileClauseWhereTest extends Storm_Test_ModelTestCase { +class Storm_Test_LoaderQueryVolatileClauseOrderTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileClauseOrderTest']; public function setUp() { parent::setUp(); @@ -517,479 +582,1032 @@ class Storm_Test_LoaderQueryVolatileClauseWhereTest extends Storm_Test_ModelTest ['id' => 100, 'login' => 'user_admin', 'level' => 'invite admin redacteur', - 'foo' => 'premier deuxieme troisieme', - 'prefs' => null]); + 'foo' => 'first second third', + 'order1' => 'abcd', + 'order2' => 2, + 'order3' => 'defg', + 'order4' => 'weburl']); $this->fixture(Storm_Test_LoaderQueryUser::class, ['id' => 101, 'login' => 'user_administrateur', 'level' => 'administrateur', - 'foo' => 'deuxieme redacteur', - 'prefs' => '{"best":true}']); + 'foo' => 'third second forth', + 'order1' => 'abcdef', + 'order2' => 3, + 'order3' => 'defg', + 'order4' => 'no']); $this->fixture(Storm_Test_LoaderQueryUser::class, ['id' => 102, 'login' => 'user_invite', 'level' => 'invite', - 'foo' => 'premier deuxieme', - 'url' => 'https://my-server.org/page_1.html', - 'website' => 'https://my-server.org/pomme+d\'api.php page2.php']); - } - - - /** @test */ - public function whereId_GT_100_ShouldAnswersUserAdministrateurAndUserInvite() { - $this->assertEquals(['user_administrateur', 'user_invite'], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->gt('id', 100) - ->fetchAll())) - ->collect('login') - ->getArrayCopy()); + 'foo' => 'forth fifth seven', + 'order1' => 'cdef', + 'order2' => 1, + 'order3' => 'klmn', + 'order4' => '']); } /** @test */ - public function whereId_GT_EQ_101_ShouldAnswersUserAdministrateurAndUserInvite() { - $this->assertEquals(['user_administrateur', 'user_invite'], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->gt_eq('id', 101) - ->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } - + public function with_Order1Desc_ShouldAnswersInOrdered() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc('order1'); - /** @test */ - public function whereId_LT_102_ShouldAnswersUserAdminAndUserAdministrateur() { - $this->assertEquals(['user_admin', 'user_administrateur'], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->lt('id', 102) - ->fetchAll())) + $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'], + (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order1` DESC'); } /** @test */ - public function whereId_LT_EQ_101_ShouldAnswersUserAdminAndUserAdministrateur() { - $this->assertEquals(['user_admin', 'user_administrateur'], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->lt_eq('id', 101) - ->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } - + public function with_Order3Asc_Order2Desc_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order('order3') + ->order_desc('order2'); - /** @test */ - public function whereLogin_NOT_EQ_UserAdministrateurShouldReturnUserAdminAndUserInvite() { - $this->assertEquals(['user_admin', 'user_invite'], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->not_eq('login', 'user_administrateur') - ->fetchAll())) + $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'], + (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order3` ASC, `users`.`order2` DESC'); } /** @test */ - public function whereId_IN_100And102_AndFoo_NOT_LIKE_TroisieShouldReturnId_102() { - $this->assertEquals([102], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->in('id', [100, 102]) - ->not_like('foo', '%troisie%') - ->fetchAll())) - ->collect('id') - ->getArrayCopy()); - } - - - /** @test */ - public function whereId_IN_100And101_AndFoo_START_DeuxShouldReturnId_101() { - $this->assertEquals([101], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->in('id', [100, 101]) - ->start('foo', 'deux') - ->fetchAll())) - ->collect('id') - ->getArrayCopy()); - } - - - /** @test */ - public function whereFoo_END_ActeurShouldReturnId_101() { - $this->assertEquals([101], - (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->end('foo', 'acteur') - ->fetchAll())) - ->collect('id') - ->getArrayCopy()); - } - + public function with_Order3Desc_Order2Desc_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc('order3') + ->order_desc('order2'); - /** @test */ - public function withMatchLevelAgainstAdminShouldAnswersOnlyUser_Admin() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level')) - ->against(['admin'])); - $this->assertEquals(['user_admin'], + $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order3` DESC, `users`.`order2` DESC'); } /** @test */ - public function withMatchLevelAgainstAdminDeuxiemeShouldAnswersUser_AdminAndUser_AdministrateurAndUser_Invite() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level,foo')) - ->against(['admin', 'deuxieme'])); - $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } - + public function withMatch_Level_Administrateur_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order((new Storm_Query_MatchRating('level')) + ->against('administrateur')); - /** @test */ - public function withMatchLevelAgainstStrictAdminRedacteur_DeuxiemeShouldAnswersUser_AdminAndUser_Administrateur() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level,foo', true)) - ->against(['admin', 'redacteur']) - ->against(['deuxieme'])); - $this->assertEquals(['user_admin', 'user_administrateur'], + $this->assertEquals(['user_admin', 'user_invite', 'user_administrateur'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur\') ASC'); } /** @test */ - public function withMatchLevelFooExactDeuxiemeRedacteurShouldAnswersOnlyUser_Administrateur() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level,foo')) - ->exact('deuxieme redacteur')); - $this->assertEquals(['user_administrateur'], + public function withMatch_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc((new Storm_Query_MatchRating('level')) + ->against('administrateur redacteur')); + + $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur redacteur\') DESC'); } /** @test */ - public function withMatchLevelFooDeuxiemeAndAgainstRedacteurAnswers_AllUsers() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level,foo')) - ->against('deuxieme') - ->against('redacteur')); + public function withMatchInBooleanMode_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc((new Storm_Query_MatchBoolean('level')) + ->against('administrateur redacteur')); + $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur redacteur\' IN BOOLEAN MODE) DESC'); } /** @test */ - public function withMatchLevelFooDeuxiemeAndAgainstStrictRedacteurAnswers_User_Admin_User_Administrateur() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('level,foo')) - ->against('deuxieme') - ->against('redacteur', true)); - $this->assertEquals(['user_admin', 'user_administrateur'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } - + public function withMatch_FooDescExacteInBooleanMode_Forth_Fifth_Level_Administrateur_Redacteur_ShouldAnswersOrderedUser() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc((new Storm_Query_MatchBoolean('foo')) + ->exact('forth fifth')) + ->order((new Storm_Query_MatchRating('level')) + ->against('administrateur redacteur')); - /** @test */ - public function withOrClause_LevelInviteOrFooLikePremierShouldAnswersUser_AdminAndUser_Invite() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->or((new Storm_Query_Criteria) - ->eq('level', 'invite') - ->like('foo', '%premier%')); - $this->assertEquals(['user_admin', 'user_invite'], + $this->assertEquals(['user_invite', 'user_admin', 'user_administrateur'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(foo) AGAINST(\'"forth fifth"\' IN BOOLEAN MODE) DESC, MATCH(level) AGAINST(\'administrateur redacteur\') ASC'); } /** @test */ - public function withAndClause_LevelInviteOrFooLikePremierShouldAnswersOnlyUser_Invite() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->and((new Storm_Query_Criteria) - ->eq('level', 'invite') - ->like('foo', '%premier%')); - $this->assertEquals(['user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } + public function withMatch_FooLevelStrict_InviteRedacteur_SecondThird_ShouldAnswersOrderedUser() { + // https://www.php.net/manual/fr/function.usort + // If two members compare as equal, they retain their original order. Prior to PHP 8.0.0, their relative order in the sorted array was undefined. + if (version_compare(PHP_VERSION, '8.0.0', '<')) + return; + $query = Storm_Test_LoaderQueryUser::query() + ->order((new Storm_Query_MatchBoolean('foo,level', true)) + ->against('invite redacteur') + ->against('second third')); - /** @test */ - public function urlLikeHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->like('url', '%https://my-server.org%'); - $this->assertEquals(['user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), + Storm_Test_LoaderQueryUser::find(102), + Storm_Test_LoaderQueryUser::find(100)], + $query->fetchAll()); + $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(foo,level) AGAINST(\'+(invite redacteur) +(second third)\' IN BOOLEAN MODE) ASC'); } /** @test */ - public function websiteLikeHttpsDoubleDotSlashSlashPlusQuotePommeShouldAnswersOnlyUserWithSlashesPlusAndQuote() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->like('website', '%https://my-server.org/pomme+d\'api%'); + public function selectUsersGroupByOrder3ShouldAnswersUserAdminAndUserInvite() { + $query = Storm_Test_LoaderQueryUser::query() + ->group('order3'); - $this->assertEquals(['user_invite'], + $this->assertEquals([0 => 'user_admin', 2 => 'user_invite'], (new Storm_Model_Collection($query->fetchAll())) ->collect('login') ->getArrayCopy()); + $this->assertSql('SELECT `users`.* FROM `users` GROUP BY `users`.`order3`'); } /** @test */ - public function urlMatchHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->match((new Storm_Query_MatchBoolean('website'))->against('https://my-server.org/pomme+d\'api.php')); - $this->assertEquals(['user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); - } + public function withSelectShouldAnswersWithRowModel() { + $results = Storm_Test_LoaderQueryUser::query() + ->select(['login', 'level']) + ->fetchAll(); + $user = Storm_Test_LoaderQueryUser::find(100); + $this->assertEquals(['login' => $user->getLogin(), + 'level' => $user->getLevel()], + $results[0]->toArray()); + $this->assertTrue($results[0] instanceof Storm_Model_Row); - /** @test */ - public function withIsNullPrefsShouldAnswersUser_100_Only() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->and((new Storm_Query_Criteria)->is_null('prefs')); + $user = Storm_Test_LoaderQueryUser::find(101); + $this->assertEquals(['login' => $user->getLogin(), + 'level' => $user->getLevel()], + $results[1]->toArray()); + $this->assertTrue($results[1] instanceof Storm_Model_Row); - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], - $query->fetchAll()); + $user = Storm_Test_LoaderQueryUser::find(102); + $this->assertEquals(['login' => $user->getLogin(), + 'level' => $user->getLevel()], + $results[2]->toArray()); + $this->assertTrue($results[2] instanceof Storm_Model_Row); + $this->assertSql('SELECT `users`.`login`, `users`.`level` FROM `users`'); } /** @test */ - public function withIsNotNullPrefsShouldAnswersUsers_101_and_102() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->and((new Storm_Query_Criteria)->not_is_null('prefs')); + public function with_Order4WithNoValueDesc_ShouldAnswersOrderedUser_100_101_102() { + $query = Storm_Test_LoaderQueryUser::query() + ->order_desc('order4', 'no'); $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), + Storm_Test_LoaderQueryUser::find(100), Storm_Test_LoaderQueryUser::find(102)], $query->fetchAll()); + $this->assertSqlCount('SELECT `users`.* FROM `users` ORDER BY `users`.`order4` = \'no\' DESC', 1); } +} - /** @test */ - public function withNotInInviteAdministrateurShouldAnswerUser_100_Only() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->not_in('level', ['invite', 'administrateur']); - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], - $query->fetchAll()); - } +class Storm_Test_LoaderQueryVolatileDistinctTest + extends Storm_Test_ModelTestCase { - /** @test */ - public function withClauseLimitOneShouldReturnOnlyUser_100() { - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->limit(1) - ->fetchAll()); + protected array $_storm_scopes = ['LoaderQueryVolatileDistinctTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 100, + 'distinct1' => 'abcd', + 'distinct2' => 1234]); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 101, + 'distinct1' => 'abcd', + 'distinct2' => 1234]); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 102, + 'distinct1' => 'abcd', + 'distinct2' => 4567]); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 103, + 'distinct1' => 'zyw', + 'distinct2' => 987]); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 104, + 'distinct1' => 'fed', + 'distinct2' => 987]); } /** @test */ - public function withClauseLimitOneTwoShouldReturnTwoUser_101_102() { - $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), - Storm_Test_LoaderQueryUser::find(102)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->limit('1, 2') - ->fetchAll()); + public function withDistinct1AndDistinct2ShouldAnswers4Result() { + $row_results = Storm_Test_LoaderQueryUser::query() + ->distinct(['distinct1', 'distinct2']) + ->fetchAll(); + + $this->assertEquals([['distinct1' => 'abcd', 'distinct2' => 1234], + ['distinct1' => 'abcd', 'distinct2' => 4567], + ['distinct1' => 'zyw', 'distinct2' => 987], + ['distinct1' => 'fed', 'distinct2' => 987]], + array_map(fn($row) => $row->toArray(), + $row_results)); + $this->assertSql('SELECT DISTINCT `users`.`distinct1`, `users`.`distinct2` FROM `users`'); + } +} + + + + +class Storm_Test_LoaderQueryVolatileJoinInnerTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileJoinInnerTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 1, + 'name' => 'Movie name 1', + 'date' => '2020']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 2, + 'name' => 'Movie name 2', + 'date' => '2022']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 3, + 'name' => 'Movie name 3', + 'date' => '2021']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 4, + 'name' => 'Movie name 4', + 'date' => '2018']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 5, + 'name' => 'Movie name 5', + 'date' => '2019']); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 1, + 'movie_id' => 3, + 'name' => 'Actor name 1', + 'age' => 25]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'movie_id' => 1, + 'name' => 'Actor name 2', + 'age' => 52]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'movie_id' => 4, + 'name' => 'Actor name 3', + 'age' => 36]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 4, + 'movie_id' => 3, + 'name' => 'Actor name 4', + 'age' => 20]); + } + + + /** @test */ + public function withInnerJoinShouldReturnIntersectMoviesActorsOnlyOneResult() { + $results = Storm_Test_LoaderQueryMovie::join('tab1') + ->select(['name', 'date']) + ->inner(Storm_Test_LoaderQueryActor::inner('tab2') + ->select(['name', 'age']) + ->on_eq('id', 'movie_id') + ->gt('age', 30)) + ->in('date', ['2019', '2020', '2021']) + ->fetchAll(); + + $row = $results[0]; + $this->assertCount(1, $results); + $this->assertEquals('Movie name 1', $row->getTab1Name()); + $this->assertEquals('2020', $row->tab1_date); + $this->assertEquals('Actor name 2', $row->getTab2Name()); + $this->assertEquals(52, $row->tab2_age); + $this->assertTrue($row instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab2`.`age` AS `tab2_age` FROM `movies` AS `tab1` + INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))'); } /** @test */ - public function withClauseLimitPageShouldPaginate() { - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), - Storm_Test_LoaderQueryUser::find(101)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->limit_page([0, 2]) - ->fetchAll()); - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), - Storm_Test_LoaderQueryUser::find(101)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->limit_page([1, 2]) - ->fetchAll()); - $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->limit_page([2, 2]) - ->fetchAll()); + public function withInnerJoinShouldReturnIntersectDedupMovieAndTwoActors() { + $results = Storm_Test_LoaderQueryMovie::join('tab1') + ->select(['name', 'date']) + ->inner(Storm_Test_LoaderQueryActor::inner('tab2') + ->select(['name', 'age']) + ->on_eq('id', 'movie_id') + ->lt('age', 30)) + ->in('date', ['2019', '2020', '2021']) + ->fetchAll(); + + $this->assertCount(2, $results); + $this->assertEquals([['tab1_name' => 'Movie name 3', + 'tab1_date' => '2021', + 'tab2_name' => 'Actor name 1', + 'tab2_age' => 25], + ['tab1_name' => 'Movie name 3', + 'tab1_date' => '2021', + 'tab2_name' => 'Actor name 4', + 'tab2_age' => 20]], + array_map(fn($row) => $row->toArray(), + $results)); + $this->assertTrue($results[0] instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab2`.`age` AS `tab2_age` FROM `movies` AS `tab1` + INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` < 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))'); } /** @test */ - public function withClauseLimitPageAndAttributeShouldWork() { - $this->assertEquals([Storm_Test_LoaderQueryUser::find(100), - Storm_Test_LoaderQueryUser::find(102)], - Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->start('foo', 'premier') - ->limit_page([1, 2]) - ->fetchAll()); + public function withInnerJoinWithoutSelectShouldReturnMovies() { + $results = Storm_Test_LoaderQueryMovie::join('tab1') + ->inner(Storm_Test_LoaderQueryActor::inner('tab2') + ->on_eq('id', 'movie_id') + ->gt('age', 30)) + ->in('date', ['2019', '2020', '2021']) + ->fetchAll(); + + $this->assertCount(1, $results); + $this->assertEquals(['id' => 1, + 'name' => 'Movie name 1', + 'date' => '2020'], + $results[0]->toArray()); + $this->assertTrue($results[0] instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab1`.* FROM `movies` AS `tab1` + INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))'); + } + + + /** @test */ + public function withInnerJoinWithSelectTab2AllShouldReturnActors() { + $results = Storm_Test_LoaderQueryMovie::join('tab1') + ->inner(Storm_Test_LoaderQueryActor::inner('tab2') + ->on_eq('id', 'movie_id') + ->select() + ->gt('age', 30)) + ->in('date', ['2019', '2020', '2021']) + ->fetchAll(); + + $this->assertCount(1, $results); + $this->assertEquals(['id' => 2, + 'movie_id' => 1, + 'name' => 'Actor name 2', + 'age' => 52], + $results[0]->toArray()); + $this->assertTrue($results[0] instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab2`.* FROM `movies` AS `tab1` + INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))'); } } -class Storm_Test_LoaderQueryVolatileClauseOrderTest +class Storm_Test_LoaderQueryVolatileJoinLeftTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileJoinLeftTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 1, + 'name' => 'Movie name 1', + 'date' => '2020']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 2, + 'name' => 'Movie name 2', + 'date' => '2022']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 3, + 'name' => 'Movie name 3', + 'date' => '2021']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 4, + 'name' => 'Movie name 4', + 'date' => '2018']); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 1, + 'movie_id' => 3, + 'name' => 'Actor name 1', + 'age' => 25]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'movie_id' => 1, + 'name' => 'Actor name 2', + 'age' => 52]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'movie_id' => 3, + 'name' => 'Actor name 3', + 'age' => 20]); + } + + + /** @test */ + public function withLeftJoinShouldReturn() { + $row_results = Storm_Test_LoaderQueryMovie::join('tab1') + ->select(['name']) + ->left(Storm_Test_LoaderQueryActor::left('tab2') + ->select(['name']) + ->on_eq('id', 'movie_id')) + ->fetchAll(); + + $this->assertCount(5, $row_results); + $this->assertEquals([['tab1_name' => 'Movie name 1', + 'tab2_name' => 'Actor name 2'], + ['tab1_name' => 'Movie name 2', + 'tab2_name' => null], + ['tab1_name' => 'Movie name 3', + 'tab2_name' => 'Actor name 1'], + ['tab1_name' => 'Movie name 3', + 'tab2_name' => 'Actor name 3'], + ['tab1_name' => 'Movie name 4', + 'tab2_name' => null]], + array_map(fn($row) => $row->toArray(), + $row_results)); + $this->assertTrue($row_results[0] instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab2`.`name` AS `tab2_name` FROM `movies` AS `tab1` + LEFT JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id`'); + } +} + + + + +class Storm_Test_LoaderQueryVolatileJoinRightTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileJoinRightTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 1, + 'name' => 'Movie name 1', + 'date' => '2020']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 2, + 'name' => 'Movie name 2', + 'date' => '2022']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 3, + 'name' => 'Movie name 3', + 'date' => '2021']); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 1, + 'movie_id' => 4, + 'name' => 'Actor name 1', + 'age' => 25]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'movie_id' => 1, + 'name' => 'Actor name 2', + 'age' => 52]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'movie_id' => 1, + 'name' => 'Actor name 3', + 'age' => 20]); + } + + + /** @test */ + public function withRightJoinShouldReturn() { + $row_results = Storm_Test_LoaderQueryMovie::join('tab1') + ->select(['name']) + ->right(Storm_Test_LoaderQueryActor::right('tab2') + ->select(['name']) + ->on_eq('id', 'movie_id')) + ->fetchAll(); + + $this->assertCount(3, $row_results); + $this->assertEquals([['tab1_name' => null, + 'tab2_name' => 'Actor name 1'], + ['tab1_name' => 'Movie name 1', + 'tab2_name' => 'Actor name 2'], + ['tab1_name' => 'Movie name 1', + 'tab2_name' => 'Actor name 3']], + array_map(fn($row) => $row->toArray(), + $row_results)); + $this->assertTrue($row_results[0] instanceof Storm_Model_Row); + + $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab2`.`name` AS `tab2_name` FROM `movies` AS `tab1` + RIGHT JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id`'); + } +} + + + + +class Storm_Test_LoaderQueryVolatileJoinMultipleTest extends Storm_Test_ModelTestCase { + protected array $_storm_scopes = ['LoaderQueryVolatileJoinMultipleTest']; + public function setUp() { parent::setUp(); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 1, + 'name' => 'Movie name 1', + 'date' => '2020']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 2, + 'name' => 'Movie name 2', + 'date' => '2022']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 3, + 'name' => 'Movie name 3', + 'date' => '2021']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 4, + 'name' => 'Movie name 4', + 'date' => '2018']); + $this->fixture(Storm_Test_LoaderQueryMovie::class, + ['id' => 5, + 'name' => 'Movie name 5', + 'date' => '2019']); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 1, + 'movie_id' => 3, + 'name' => 'Actor name 1', + 'age' => 25]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'movie_id' => 1, + 'name' => 'Actor name 2', + 'age' => 52]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'movie_id' => 4, + 'name' => 'Actor name 3', + 'age' => 36]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 4, + 'movie_id' => 3, + 'name' => 'Actor name 4', + 'age' => 20]); + $this->fixture(Storm_Test_LoaderQueryUser::class, - ['id' => 100, - 'login' => 'user_admin', - 'level' => 'invite admin redacteur', - 'foo' => 'first second third', - 'order1' => 'abcd', - 'order2' => 2, - 'order3' => 'defg']); + ['id' => 1, + 'libelle' => 'User 1', + 'movie_id' => 2]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 2, + 'libelle' => 'User 2', + 'movie_id' => 3]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 3, + 'libelle' => 'User 3', + 'movie_id' => 4]); + + $this->fixture(Storm_Test_LoaderQueryProductor::class, + ['id' => 1, + 'label' => 'Productor 1', + 'user_id' => 1, + 'birthday' => '01/01/2015']); + $this->fixture(Storm_Test_LoaderQueryProductor::class, + ['id' => 2, + 'label' => 'Productor 2', + 'user_id' => 1, + 'birthday' => '02/02/2016']); + $this->fixture(Storm_Test_LoaderQueryProductor::class, + ['id' => 3, + 'label' => 'Productor 3', + 'user_id' => 3, + 'birthday' => '03/03/2017']); + $this->fixture(Storm_Test_LoaderQueryProductor::class, + ['id' => 4, + 'label' => 'Productor 4', + 'user_id' => 3, + 'birthday' => '04/04/2018']); + } + + + /** @test */ + public function withInnerJoinShouldReturnIntersectMoviesActorsOnlyOneResult() { + $results = Storm_Test_LoaderQueryMovie::join('tab1') + ->select(['date']) + ->inner(Storm_Test_LoaderQueryActor::inner('tab2') + ->on_eq('id', 'movie_id') + ->select(['name']) + ->gt('age', 30)) + ->inner(Storm_Test_LoaderQueryUser::inner('tab3') + ->on_eq('id', 'movie_id') + ->select(['libelle']) + ->right(Storm_Test_LoaderQueryProductor::right('tab4') + ->on_eq('id', 'user_id') + ->select(['label', 'birthday']))) + ->fetchAll(); + + $this->assertCount(4, $results); + $this->assertSql('SELECT `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab3`.`libelle` AS `tab3_libelle`, `tab4`.`label` AS `tab4_label`, `tab4`.`birthday` AS `tab4_birthday` FROM `movies` AS `tab1` + INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 + INNER JOIN `users` AS `tab3` ON `tab1`.`id` = `tab3`.`movie_id` + RIGHT JOIN `productors` AS `tab4` ON `tab3`.`id` = `tab4`.`user_id`'); + $this->assertEquals([['tab1_date' => null, + 'tab2_name' => null, + 'tab3_libelle' => null, + 'tab4_label' => 'Productor 1', + 'tab4_birthday' => '01/01/2015'], + ['tab1_date' => null, + 'tab2_name' => null, + 'tab3_libelle' => null, + 'tab4_label' => 'Productor 2', + 'tab4_birthday' => '02/02/2016'], + ['tab1_date' => '2018', + 'tab2_name' => 'Actor name 3', + 'tab3_libelle' => 'User 3', + 'tab4_label' => 'Productor 3', + 'tab4_birthday' => '03/03/2017'], + ['tab1_date' => '2018', + 'tab2_name' => 'Actor name 3', + 'tab3_libelle' => 'User 3', + 'tab4_label' => 'Productor 4', + 'tab4_birthday' => '04/04/2018']], + array_map(fn($row) => $row->toArray(), + $results)); + $this->assertTrue($results[0] instanceof Storm_Model_Row); + } +} + + + + +class Storm_Test_LoaderQueryVolatileInSubQueriesTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileInSubQueriesTest']; + + public function setUp() { + parent::setUp(); $this->fixture(Storm_Test_LoaderQueryUser::class, - ['id' => 101, - 'login' => 'user_administrateur', - 'level' => 'administrateur', - 'foo' => 'third second forth', - 'order1' => 'abcdef', - 'order2' => 3, - 'order3' => 'defg']); + ['id' => 1, + 'name' => 'User name 1', + 'year' => '2020', + 'user_id' => 1]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 2, + 'name' => 'User name 2', + 'year' => '2022', + 'user_id' => 2]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 3, + 'name' => 'User name 3', + 'year' => '2021', + 'user_id' => 3]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 4, + 'name' => 'User name 4', + 'year' => '2018', + 'user_id' => 4]); + } + + + /** @test */ + public function withInSubQueriesReturnUser_Name3_Name4() { + $results = Storm_Test_LoaderQueryUser::query() + ->in('user_id', Storm_Test_LoaderQueryUser::subIn('users2') + ->select(['user_id']) + ->in('year', ['2021', '2018'])) + ->fetchAll(); + + $this->assertCount(2, $results); + $this->assertEquals(['User name 3', 'User name 4'], + array_map(fn($row) => $row->getName(), + $results)); + $this->assertTrue($results[0] instanceof Storm_Test_LoaderQueryUser); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`user_id` IN (SELECT `users2`.`user_id` FROM `users` AS `users2` WHERE (`users2`.`year` IN (\'2021\', \'2018\'))))'); + } +} + + + + +class Storm_Test_LoaderQueryVolatileNotInSubQueriesTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileNotInSubQueriesTest']; + + public function setUp() { + parent::setUp(); $this->fixture(Storm_Test_LoaderQueryUser::class, - ['id' => 102, - 'login' => 'user_invite', - 'level' => 'invite', - 'foo' => 'forth fifth seven', - 'order1' => 'cdef', - 'order2' => 1, - 'order3' => 'klmn']); + ['id' => 1, + 'name' => 'User name 1', + 'actor_id' => 1]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 3, + 'name' => 'User name 3', + 'actor_id' => 3]); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'id_actor' => 2, + 'label' => 'Actor 2']); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'id_actor' => 3, + 'label' => 'Actor 3']); } /** @test */ - public function with_Order1Desc_ShouldAnswersInOrdered() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order_desc('order1'); - $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function withNotInSubQueriesReturnUser_Name1_Name2() { + $results = Storm_Test_LoaderQueryUser::query() + ->not_in('actor_id', Storm_Test_LoaderQueryActor::subIn() + ->select(['id_actor']) + ->eq('label', 'Actor 3')) + ->fetchAll(); + + $this->assertCount(2, $results); + $this->assertEquals(['id' => 1, + 'name' => 'User name 1', + 'actor_id' => 1], $results[0]->toArray()); + $this->assertEquals(['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2], $results[1]->toArray()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`actor_id` NOT IN (SELECT `actors`.`id_actor` FROM `actors` WHERE (`actors`.`label` = \'Actor 3\')))'); + } +} + + + + +class Storm_Test_LoaderQueryVolatileExistsSubQueriesTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['LoaderQueryVolatileExistsSubQueriesTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 1, + 'name' => 'User name 1', + 'actor_id' => 1, + 'age' => 21]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2, + 'age' => 22]); + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 3, + 'name' => 'User name 3', + 'actor_id' => 3, + 'age' => 23]); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'id_actor' => 2, + 'label' => 'Actor 2', + 'age' => 22]); + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 3, + 'id_actor' => 3, + 'label' => 'Actor 3', + 'age' => 20]); + } + + + /** @test */ + public function withExistsSubQueriesReturnOnlyUser_Name3() { + $results = Storm_Test_LoaderQueryUser::query() + ->exists(Storm_Test_LoaderQueryActor::subExists() + ->on_eq('actor_id', 'id_actor') + ->on_gt('age', 'age')) + ->fetchAll(); + + $this->assertCount(1, $results); + $this->assertEquals(['id' => 3, + 'name' => 'User name 3', + 'actor_id' => 3, + 'age' => 23], $results[0]->toArray()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` = `actors`.`id_actor` AND `users`.`age` > `actors`.`age`)))'); } /** @test */ - public function with_Order3Asc_Order2Desc_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order('order3') - ->order_desc('order2'); - $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function withExistsSubQueriesReturnOnlyUser_Name2() { + $results = Storm_Test_LoaderQueryUser::query() + ->exists(Storm_Test_LoaderQueryActor::subExists() + ->on_gt_eq('actor_id', 'id_actor') + ->on_lt_eq('age', 'age')) + ->fetchAll(); + + $this->assertCount(1, $results); + $this->assertEquals(['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2, + 'age' => 22], $results[0]->toArray()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` >= `actors`.`id_actor` AND `users`.`age` <= `actors`.`age`)))'); } /** @test */ - public function with_Order3Desc_Order2Desc_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order_desc('order3') - ->order_desc('order2'); - $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function withNotExistsSubQueriesWithTwoConditionReturnUser_Name2_Name3() { + $results = Storm_Test_LoaderQueryUser::query() + ->not_exists(Storm_Test_LoaderQueryActor::subExists() + ->on_not_eq('actor_id', 'id_actor') + ->on_lt('age', 'age')) + ->fetchAll(); + + $this->assertCount(2, $results); + $this->assertEquals(['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2, + 'age' => 22], $results[0]->toArray()); + $this->assertEquals(['id' => 3, + 'name' => 'User name 3', + 'actor_id' => 3, + 'age' => 23], $results[1]->toArray()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (NOT EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` != `actors`.`id_actor` AND `users`.`age` < `actors`.`age`)))'); } /** @test */ - public function withMatch_Level_Administrateur_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order((new Storm_Query_MatchRating('level')) - ->against('administrateur')); - $this->assertEquals(['user_admin', 'user_invite', 'user_administrateur'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function withNotExistsSubQueriesWithOneConditionAndOneCriteriaReturnUser_Name1_Name2() { + $results = Storm_Test_LoaderQueryUser::query() + ->not_exists(Storm_Test_LoaderQueryActor::subExists() + ->on_eq('actor_id', 'id_actor') + ->lt('age', 22)) + ->fetchAll(); + + $this->assertCount(2, $results); + $this->assertEquals(['id' => 1, + 'name' => 'User name 1', + 'actor_id' => 1, + 'age' => 21], $results[0]->toArray()); + $this->assertEquals(['id' => 2, + 'name' => 'User name 2', + 'actor_id' => 2, + 'age' => 22], $results[1]->toArray()); + $this->assertSql('SELECT `users`.* FROM `users` WHERE (NOT EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` = `actors`.`id_actor` AND `actors`.`age` < 22)))'); + } +} + + + + +class Storm_Test_LoaderQueryWithErrorsTest + extends Storm_Test_ModelTestCase { + + protected array $_storm_scopes = ['Storm_Test_LoaderQueryWithErrorsTest']; + + public function setUp() { + parent::setUp(); + + $this->fixture(Storm_Test_LoaderQueryUser::class, + ['id' => 1, + 'name' => 'User name 1', + 'actor_id' => 1, + 'age' => 21]); + + $this->fixture(Storm_Test_LoaderQueryActor::class, + ['id' => 2, + 'id_actor' => 1, + 'label' => 'Actor 2', + 'age' => 22]); } /** @test */ - public function withMatch_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order_desc((new Storm_Query_MatchRating('level')) - ->against('administrateur redacteur')); - $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function existsWithNoConditionAndSelectColumnReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::query() + ->exists(Storm_Test_LoaderQueryActor::subExists() + ->select(['error'])) + ->fetchAll(); + + $this->assertCount(0, $results); + $this->assertSql('Error: For Exists don\'t add select column, Error: For Exists condition "on" is mandatory'); } /** @test */ - public function withMatchInBooleanMode_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order_desc((new Storm_Query_MatchBoolean('level')) - ->against('administrateur redacteur')); - $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function inWithNoSelectColumnReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::query() + ->in('actor_id', Storm_Test_LoaderQueryActor::subIn()) + ->fetchAll(); + + $this->assertCount(0, $results); + $this->assertSql('Error: For In add select column is mandatory'); } /** @test */ - public function withMatch_FooDescExacteInBooleanMode_Forth_Fifth_Level_Administrateur_Redacteur_ShouldAnswersOrderedUser() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order_desc((new Storm_Query_MatchBoolean('foo')) - ->exact('forth fifth')) - ->order((new Storm_Query_MatchRating('level')) - ->against('administrateur redacteur')); - $this->assertEquals(['user_invite', 'user_admin', 'user_administrateur'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function inWithMultipleSelectColumnReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::query() + ->in('actor_id', Storm_Test_LoaderQueryActor::subIn() + ->select(['id', 'id_actor'])) + ->fetchAll(); + + $this->assertCount(0, $results); + $this->assertSql('Error: For In add select must have only one column'); } /** @test */ - public function withMatch_FooLevelStrict_InviteRedacteur_SecondThird_ShouldAnswersOrderedUser() { - // https://www.php.net/manual/fr/function.usort - // If two members compare as equal, they retain their original order. Prior to PHP 8.0.0, their relative order in the sorted array was undefined. - if (version_compare(PHP_VERSION, '8.0.0', '<')) - return; + public function innerWithNoConditionReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::join('tab1') + ->inner(Storm_Test_LoaderQueryActor::inner('tab2')) + ->fetchAll(); - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->order((new Storm_Query_MatchBoolean('foo,level', true)) - ->against('invite redacteur') - ->against('second third')); - $this->assertEquals([Storm_Test_LoaderQueryUser::find(101), - Storm_Test_LoaderQueryUser::find(102), - Storm_Test_LoaderQueryUser::find(100)], - $query->fetchAll()); + $this->assertCount(0, $results); + $this->assertSql('Error: For Inner condition "on" is mandatory'); } /** @test */ - public function selectUsersGroupByOrder3ShouldAnswersUserAdminAndUserInvite() { - $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class) - ->group('order3'); - $this->assertEquals([0 => 'user_admin', 2 => 'user_invite'], - (new Storm_Model_Collection($query->fetchAll())) - ->collect('login') - ->getArrayCopy()); + public function leftWithNoConditionReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::join('tab1') + ->left(Storm_Test_LoaderQueryActor::left('tab2')) + ->fetchAll(); + + $this->assertCount(0, $results); + $this->assertSql('Error: For Left condition "on" is mandatory'); } + + + /** @test */ + public function rightWithNoConditionReturnNothingAndSqlContainErrorMessage() { + $results = Storm_Test_LoaderQueryUser::join('tab1') + ->right(Storm_Test_LoaderQueryActor::right('tab2')) + ->fetchAll(); + + $this->assertCount(0, $results); + $this->assertSql('Error: For Right condition "on" is mandatory'); + } +} + + + + +class Storm_Test_LoaderQueryUser extends Storm_Model_Abstract { + + protected $_table_name = 'users'; +} + + + + +class Storm_Test_LoaderQueryMovie extends Storm_Model_Abstract { + + protected $_table_name = 'movies'; +} + + + + +class Storm_Test_LoaderQueryActor extends Storm_Model_Abstract { + + protected $_table_name = 'actors'; +} + + + + +class Storm_Test_LoaderQueryProductor extends Storm_Model_Abstract { + + protected $_table_name = 'productors'; } diff --git a/tests/Storm/Test/LoaderTest.php b/tests/Storm/Test/LoaderTest.php index 50eec16ee9072b6432b7526be00df3e1d968765c..0ffa2029154771ccf42a2e489e607f58586ca48e 100644 --- a/tests/Storm/Test/LoaderTest.php +++ b/tests/Storm/Test/LoaderTest.php @@ -25,12 +25,14 @@ abstract class Storm_Test_LoaderTestCase extends Storm_Test_ModelTestCase { + protected $_loader, $_table; public function setUp() { parent::setUp(); + $this->_table = $this->mock(); $this->_table ->whenCalled('getAdapter') @@ -112,7 +114,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { public function withNullValueFindAllByShouldBuildSqlWithIsNullStatement() { $select = $this->mock() ->whenCalled('where') - ->with('parent_id is null') + ->with('`parent_id` IS null') ->answers(null) ->whenCalled('where') @@ -157,18 +159,19 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { public function findAllByGeneratedSelects() { return [ - [ ['name' => null] , ['name is null'] ], - [ ['name' => ['Harlock', 'Nausicaa']], ['name in (\'Harlock\', \'Nausicaa\')'] ], - [ ['name not' => 'Harlock'], ['name!=\'Harlock\''] ], + [ ['name' => null] , ['`name` IS null'] ], + [ ['name' => ['Harlock', 'Nausicaa']], ['`name` IN (\'Harlock\', \'Nausicaa\')'] ], + [ ['name not' => 'Harlock'], ['`name` != \'Harlock\''] ], [ ['name not' => ['Harlock', 'Nausicaa']], - ['name not in (\'Harlock\', \'Nausicaa\')'] ], - [ ['name not' => null], ['name is not null'] ], - [ ['login like' => '%aus%'], ['login like \'%aus%\''] ], - [ ['login not like' => '%aus%'], ['login not like \'%aus%\''] ], + ['`name` NOT IN (\'Harlock\', \'Nausicaa\')'] ], + [ ['name not' => null], ['`name` IS NOT null'] ], + [ ['login like' => '%aus%'], ['`login` LIKE \'%aus%\''] ], + [ ['login not like' => '%aus%'], ['`login` NOT LIKE \'%aus%\''] ], [ ['where' => 'id=\'29\' and name=\'lp\''], ['id=\'29\' and name=\'lp\''] ], - [ ['left(name, 5)' => 'ganda'], ['left(name, 5)=\'ganda\''] ], - [ ['id' => '29'], ['id=\'29\''] ], - [ [ Storm_Query_Clause::greater('id', '30') ], ['id>\'30\''] ], + [ ['left(name, 5)' => 'ganda'], ['LEFT(`name`, 5) = \'ganda\''] ], + [ ['id' => '29'], ['`id` = \'29\''] ], + [ [ Storm_Query_Clause::greater('id', '30') ], ['`id` > \'30\''] ], + [ [ Storm_Query_Clause::left('name', 5, 'ganda') ], ['LEFT(`name`, 5) = \'ganda\''] ], ]; } @@ -209,7 +212,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { $this->_table->whenCalled('delete')->answers(10); $this->_loader->basicDeleteBy(['nom' => 'bond', 'prenom' => 'james']); - $this->assertEquals('nom=\'bond\' and prenom=\'james\'', + $this->assertEquals('`nom` = \'bond\' and `prenom` = \'james\'', $this->_table->getFirstAttributeForLastCallOn('delete')); } @@ -219,7 +222,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { $this->_table->whenCalled('delete')->answers(2); $this->_loader->basicDeleteBy(['nom' => ['bond', 'l\'upin']]); - $this->assertEquals("nom in ('bond', 'l'upin')", + $this->assertEquals("`nom` IN ('bond', 'l'upin')", $this->_table->getFirstAttributeForLastCallOn('delete')); } @@ -240,7 +243,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { $this->_loader->basicDeleteBy(['where' => 'nom > "22"', 'nom' => 'bond']); - $this->assertEquals('(nom > "22") and nom=\'bond\'', + $this->assertEquals('(nom > "22") and `nom` = \'bond\'', $this->_table->getFirstAttributeForLastCallOn('delete')); } @@ -262,7 +265,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { ['type' => 'fictive']); $this->assertEquals([['type' => 'fictive'], - "nom in ('bond', 'lupin') and type='real'"], + "`nom` IN ('bond', 'lupin') and `type` = 'real'"], $this->_table->getAttributesForLastCallOn('update')); } @@ -274,7 +277,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { ['type' => 'fictive']); $this->assertEquals([['type' => 'fictive'], - "nom!='bond'"], + "`nom` != 'bond'"], $this->_table->getAttributesForLastCallOn('update')); } @@ -293,7 +296,7 @@ class Storm_Test_LoaderSaveWithIdTest extends Storm_Test_LoaderTestCase { ->answers($this->_select = $this->mock()); $this->_select - ->whenCalled('where')->with('id=4')->answers($this->_select) + ->whenCalled('where')->with('`id` = 4')->answers($this->_select) ->whenCalled('from')->with($this->_table, ['count(*) as numberof'])->answers($this->_select); }