From c388d79a75904bff421025b2efcfc74c8d567f9e Mon Sep 17 00:00:00 2001 From: Nemanja Ognjanovic Date: Mon, 18 May 2015 17:16:21 +0200 Subject: [PATCH 01/17] Acl\Factory\MemoryTest now expects different exception message for phalcon 2.0 --- .gitignore | 2 ++ tests/Phalcon/Acl/Factory/MemoryTest.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 65accae58..8a5fdfee9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ composer.phar /codeception/acceptance/WebGuy.php /codeception/functional/TestGuy.php /codeception/unit/CodeGuy.php +.vagrant +Vagrantfile diff --git a/tests/Phalcon/Acl/Factory/MemoryTest.php b/tests/Phalcon/Acl/Factory/MemoryTest.php index 24269927a..2a73e1fca 100644 --- a/tests/Phalcon/Acl/Factory/MemoryTest.php +++ b/tests/Phalcon/Acl/Factory/MemoryTest.php @@ -39,11 +39,11 @@ public function testFactoryShouldThrowExceptionIfResourceOptionIsMissing() /** * @expectedException \Phalcon\Acl\Exception - * @expectedExceptionMessage Key "actions" must exist and must be traversable. + * @expectedExceptionMessage Invalid value for accessList */ public function testFactoryShouldThrowExceptionIfActionsKeyIsMissing() { - $this->markTestSkipped('Fails due to a bug in Phalcon. See https://github.com/phalcon/cphalcon/pull/10226'); + //$this->markTestSkipped('Fails due to a bug in Phalcon. See https://github.com/phalcon/cphalcon/pull/10226'); $config = new \Phalcon\Config\Adapter\Ini(__DIR__ . '/_fixtures/acl.ini'); unset($config->acl->resource->index->actions); From 7287c2d6672ec2f0dce6a152c2133b41343c71bf Mon Sep 17 00:00:00 2001 From: Nemanja Ognjanovic Date: Mon, 18 May 2015 17:40:35 +0200 Subject: [PATCH 02/17] version 2.0.x ? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 684b2b774..98b99ab4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: php env: - PHALCON_VERSION="2.0.x" - - PHALCON_VERSION="2.0.0" +# - PHALCON_VERSION="2.0.0" install: - tests/memcached.sh From ac29f419b605ee4db62fe363e3db4070ab46a841 Mon Sep 17 00:00:00 2001 From: Nemanja Ognjanovic Date: Tue, 19 May 2015 09:37:57 +0200 Subject: [PATCH 03/17] Acl\Factory\MemoryTest::testFactoryShouldThrowExceptionIfActionsKeyIsMissing is now skipped if Phalcon version is 2.0.0 --- .travis.yml | 2 +- tests/Phalcon/Acl/Factory/MemoryTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 98b99ab4f..684b2b774 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: php env: - PHALCON_VERSION="2.0.x" -# - PHALCON_VERSION="2.0.0" + - PHALCON_VERSION="2.0.0" install: - tests/memcached.sh diff --git a/tests/Phalcon/Acl/Factory/MemoryTest.php b/tests/Phalcon/Acl/Factory/MemoryTest.php index 2a73e1fca..d7d2a8fab 100644 --- a/tests/Phalcon/Acl/Factory/MemoryTest.php +++ b/tests/Phalcon/Acl/Factory/MemoryTest.php @@ -43,7 +43,9 @@ public function testFactoryShouldThrowExceptionIfResourceOptionIsMissing() */ public function testFactoryShouldThrowExceptionIfActionsKeyIsMissing() { - //$this->markTestSkipped('Fails due to a bug in Phalcon. See https://github.com/phalcon/cphalcon/pull/10226'); + if (version_compare(\Phalcon\Version::get(), '2.0.0', '=')) { + $this->markTestSkipped('Fails due to a bug in Phalcon. See https://github.com/phalcon/cphalcon/pull/10226'); + } $config = new \Phalcon\Config\Adapter\Ini(__DIR__ . '/_fixtures/acl.ini'); unset($config->acl->resource->index->actions); From 98f1a2ea445ebd961339dd69e591df33c861db99 Mon Sep 17 00:00:00 2001 From: Green Cat Date: Thu, 28 May 2015 16:05:44 +0100 Subject: [PATCH 04/17] Removes unnecessary constructor --- Library/Phalcon/Mvc/Model/Behavior/Blameable.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/Behavior/Blameable.php b/Library/Phalcon/Mvc/Model/Behavior/Blameable.php index 0bddfa558..3dba52a34 100644 --- a/Library/Phalcon/Mvc/Model/Behavior/Blameable.php +++ b/Library/Phalcon/Mvc/Model/Behavior/Blameable.php @@ -11,16 +11,6 @@ class Blameable extends Behavior implements BehaviorInterface { - /** - * Class constructor. - * - * @param array $options - */ - public function __construct($options = null) - { - $this->_options = $options; - } - /** * {@inheritdoc} * From f465085b5f22c84c0092794b8487e39c9fbac62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Tue, 2 Jun 2015 23:49:41 +0200 Subject: [PATCH 05/17] Add eager-loading --- .../Mvc/Model/EagerLoading/EagerLoad.php | 243 ++++++++++++ .../Phalcon/Mvc/Model/EagerLoading/Loader.php | 355 ++++++++++++++++++ .../Mvc/Model/EagerLoading/QueryBuilder.php | 19 + .../Phalcon/Mvc/Model/EagerLoadingTrait.php | 122 ++++++ Library/Phalcon/Mvc/Model/README.md | 56 +++ 5 files changed, 795 insertions(+) create mode 100644 Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php create mode 100644 Library/Phalcon/Mvc/Model/EagerLoading/Loader.php create mode 100644 Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php create mode 100644 Library/Phalcon/Mvc/Model/EagerLoadingTrait.php create mode 100644 Library/Phalcon/Mvc/Model/README.md diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php new file mode 100644 index 000000000..5464598be --- /dev/null +++ b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php @@ -0,0 +1,243 @@ += 0; + } + + $this->relation = $relation; + $this->constraints = is_callable($constraints) ? $constraints : NULL; + $this->parent = $parent; + } + + /** + * @return null|Phalcon\Mvc\ModelInterface[] + */ + public function getSubject() { + return $this->subject; + } + + /** + * Executes each db query needed + * + * Note: The {$alias} property is set two times because Phalcon Model ignores + * empty arrays when overloading property set. + * + * Also {@see https://github.com/stibiumz/phalcon.eager-loading/issues/1} + * + * @return $this + */ + public function load() { + $relation = $this->relation; + + $alias = $relation->getOptions(); + $alias = strtolower($alias['alias']); + $relField = $relation->getFields(); + $relReferencedModel = $relation->getReferencedModel(); + $relReferencedField = $relation->getReferencedFields(); + $relIrModel = $relation->getIntermediateModel(); + $relIrField = $relation->getIntermediateFields(); + $relIrReferencedField = $relation->getIntermediateReferencedFields(); + + // PHQL has problems with this slash + if ($relReferencedModel[0] === '\\') { + $relReferencedModel = ltrim($relReferencedModel, '\\'); + } + + $bindValues = []; + + foreach ($this->parent->getSubject() as $record) { + $bindValues[$record->readAttribute($relField)] = TRUE; + } + + $bindValues = array_keys($bindValues); + + $subjectSize = count($this->parent->getSubject()); + $isManyToManyForMany = FALSE; + + $builder = new QueryBuilder; + $builder->from($relReferencedModel); + + if ($isThrough = $relation->isThrough()) { + if ($subjectSize === 1) { + // The query is for a single model + $builder + ->innerJoin( + $relIrModel, + sprintf( + '[%s].[%s] = [%s].[%s]', + $relIrModel, + $relIrReferencedField, + $relReferencedModel, + $relReferencedField + ) + ) + ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) + ; + } + else { + // The query is for many models, so it's needed to execute an + // extra query + $isManyToManyForMany = TRUE; + + $relIrValues = new QueryBuilder; + $relIrValues = $relIrValues + ->from($relIrModel) + ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) + ->getQuery() + ->execute() + ->setHydrateMode(Resultset::HYDRATE_ARRAYS) + ; + + $bindValues = $modelReferencedModelValues = array (); + + foreach ($relIrValues as $row) { + $bindValues[$row[$relIrReferencedField]] = TRUE; + $modelReferencedModelValues[$row[$relIrField]][$row[$relIrReferencedField]] = TRUE; + } + + unset ($relIrValues, $row); + + $builder->inWhere("[{$relReferencedField}]", array_keys($bindValues)); + } + } + else { + $builder->inWhere("[{$relReferencedField}]", $bindValues); + } + + if ($this->constraints) { + call_user_func($this->constraints, $builder); + } + + $records = array (); + + if ($isManyToManyForMany) { + foreach ($builder->getQuery()->execute() as $record) { + $records[$record->readAttribute($relReferencedField)] = $record; + } + + foreach ($this->parent->getSubject() as $record) { + $referencedFieldValue = $record->readAttribute($relField); + + if (isset ($modelReferencedModelValues[$referencedFieldValue])) { + $referencedModels = array (); + + foreach ($modelReferencedModelValues[$referencedFieldValue] as $idx => $_) { + $referencedModels[] = $records[$idx]; + } + + $record->{$alias} = $referencedModels; + + if (static::$isPhalcon2) { + $record->{$alias} = NULL; + $record->{$alias} = $referencedModels; + } + } + else { + $record->{$alias} = NULL; + $record->{$alias} = array (); + } + } + + $records = array_values($records); + } + else { + // We expect a single object or a set of it + $isSingle = ! $isThrough && ( + $relation->getType() === Relation::HAS_ONE || + $relation->getType() === Relation::BELONGS_TO + ); + + if ($subjectSize === 1) { + // Keep all records in memory + foreach ($builder->getQuery()->execute() as $record) { + $records[] = $record; + } + + $record = $this->parent->getSubject(); + $record = $record[0]; + + if ($isSingle) { + $record->{$alias} = empty ($records) ? NULL : $records[0]; + } + else { + if (empty ($records)) { + $record->{$alias} = NULL; + $record->{$alias} = array (); + } + else { + $record->{$alias} = $records; + + if (static::$isPhalcon2) { + $record->{$alias} = NULL; + $record->{$alias} = $records; + } + } + } + } + else { + $indexedRecords = array (); + + // Keep all records in memory + foreach ($builder->getQuery()->execute() as $record) { + $records[] = $record; + + if ($isSingle) { + $indexedRecords[$record->readAttribute($relReferencedField)] = $record; + } + else { + $indexedRecords[$record->readAttribute($relReferencedField)][] = $record; + } + } + + foreach ($this->parent->getSubject() as $record) { + $referencedFieldValue = $record->readAttribute($relField); + + if (isset ($indexedRecords[$referencedFieldValue])) { + $record->{$alias} = $indexedRecords[$referencedFieldValue]; + + if (static::$isPhalcon2 && is_array($indexedRecords[$referencedFieldValue])) { + $record->{$alias} = NULL; + $record->{$alias} = $indexedRecords[$referencedFieldValue]; + } + } + else { + $record->{$alias} = NULL; + + if (! $isSingle) { + $record->{$alias} = array (); + } + } + } + } + } + + $this->subject = $records; + + return $this; + } +} diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php new file mode 100644 index 000000000..c08f41432 --- /dev/null +++ b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php @@ -0,0 +1,355 @@ +mustReturnAModel = FALSE; + } + else { + $className = get_class($from); + $from = array ($from); + + $this->mustReturnAModel = TRUE; + } + + if ($error) { + throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); + } + + $this->subject = $from; + $this->subjectClassName = $className; + $this->eagerLoads = empty ($arguments) ? array () : static::parseArguments($arguments); + } + + /** + * Create and get from a mixed $subject + * + * @param ModelInterface|ModelInterface[]|Simple $subject + * @param mixed ...$arguments + * @throws \InvalidArgumentException + * @return mixed + */ + static public function from($subject) { + if ($subject instanceof ModelInterface) { + $ret = call_user_func_array('static::fromModel', func_get_args()); + } + else if ($subject instanceof Simple) { + $ret = call_user_func_array('static::fromResultset', func_get_args()); + } + else if (is_array($subject)) { + $ret = call_user_func_array('static::fromArray', func_get_args()); + } + else { + throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); + } + + return $ret; + } + + /** + * Create and get from a Model + * + * @param ModelInterface $subject + * @param mixed ...$arguments + * @return ModelInterface + */ + static public function fromModel(ModelInterface $subject) { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * Create and get from an array + * + * @param ModelInterface[] $subject + * @param mixed ...$arguments + * @return array + */ + static public function fromArray(array $subject) { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * Create and get from a Resultset + * + * @param Simple $subject + * @param mixed ...$arguments + * @return Simple + */ + static public function fromResultset(Simple $subject) { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * @return null|ModelInterface[]|ModelInterface + */ + public function get() { + $ret = $this->subject; + + if (NULL !== $ret && $this->mustReturnAModel) { + $ret = $ret[0]; + } + + return $ret; + } + + /** + * @return null|ModelInterface[] + */ + public function getSubject() { + return $this->subject; + } + + /** + * Parses the arguments that will be resolved to Relation instances + * + * @param array $arguments + * @throws \InvalidArgumentException + * @return array + */ + static private function parseArguments(array $arguments) { + if (empty ($arguments)) { + throw new \InvalidArgumentException('Arguments can not be empty'); + } + + $relations = array (); + + if (count($arguments) === 1 && isset ($arguments[0]) && is_array($arguments[0])) { + foreach ($arguments[0] as $relationAlias => $queryConstraints) { + if (is_string($relationAlias)) { + $relations[$relationAlias] = is_callable($queryConstraints) ? $queryConstraints : NULL; + } + else { + if (is_string($queryConstraints)) { + $relations[$queryConstraints] = NULL; + } + } + } + } + else { + foreach ($arguments as $relationAlias) { + if (is_string($relationAlias)) { + $relations[$relationAlias] = NULL; + } + } + } + + if (empty ($relations)) { + throw new \InvalidArgumentException; + } + + return $relations; + } + + /** + * @param string $relationAlias + * @param null|callable $constraints + * @return $this + */ + public function addEagerLoad($relationAlias, $constraints = NULL) { + if (! is_string($relationAlias)) { + throw new \InvalidArgumentException(sprintf( + '$relationAlias expects to be a string, `%s` given', + gettype($relationAlias) + )); + } + + if ($constraints !== NULL && ! is_callable($constraints)) { + throw new \InvalidArgumentException(sprintf( + '$constraints expects to be a callable, `%s` given', + gettype($constraints) + )); + } + + $this->eagerLoads[$relationAlias] = $constraints; + + return $this; + } + + /** + * Resolves the relations + * + * @throws \RuntimeException + * @return EagerLoad[] + */ + private function buildTree() { + uksort($this->eagerLoads, 'strcmp'); + + $di = \Phalcon\DI::getDefault(); + $mM = $di['modelsManager']; + + $eagerLoads = $resolvedRelations = array (); + + foreach ($this->eagerLoads as $relationAliases => $queryConstraints) { + $nestingLevel = 0; + $relationAliases = explode('.', $relationAliases); + $nestingLevels = count($relationAliases); + + do { + do { + $alias = $relationAliases[$nestingLevel]; + $name = join('.', array_slice($relationAliases, 0, $nestingLevel + 1)); + } + while (isset ($eagerLoads[$name]) && ++$nestingLevel); + + if ($nestingLevel === 0) { + $parentClassName = $this->subjectClassName; + } + else { + $parentName = join('.', array_slice($relationAliases, 0, $nestingLevel)); + $parentClassName = $resolvedRelations[$parentName]->getReferencedModel(); + + if ($parentClassName[0] === '\\') { + ltrim($parentClassName, '\\'); + } + } + + if (! isset ($resolvedRelations[$name])) { + $mM->load($parentClassName); + $relation = $mM->getRelationByAlias($parentClassName, $alias); + + if (! $relation instanceof Relation) { + throw new \RuntimeException(sprintf( + 'There is no defined relation for the model `%s` using alias `%s`', + $parentClassName, + $alias + )); + } + + $resolvedRelations[$name] = $relation; + } + else { + $relation = $resolvedRelations[$name]; + } + + $relType = $relation->getType(); + + if ($relType !== Relation::BELONGS_TO && + $relType !== Relation::HAS_ONE && + $relType !== Relation::HAS_MANY && + $relType !== Relation::HAS_MANY_THROUGH) { + + throw new \RuntimeException(sprintf('Unknown relation type `%s`', $relType)); + } + + if (is_array($relation->getFields()) || + is_array($relation->getReferencedFields())) { + + throw new \RuntimeException('Relations with composite keys are not supported'); + } + + $parent = $nestingLevel > 0 ? $eagerLoads[$parentName] : $this; + $constraints = $nestingLevel + 1 === $nestingLevels ? $queryConstraints : NULL; + + $eagerLoads[$name] = new EagerLoad($relation, $constraints, $parent); + } + while (++$nestingLevel < $nestingLevels); + } + + return $eagerLoads; + } + + /** + * @return $this + */ + public function execute() { + foreach ($this->buildTree() as $eagerLoad) { + $eagerLoad->load(); + } + + return $this; + } + + /** + * Loader::execute() alias + * + * @return $this + */ + public function load() { + foreach ($this->buildTree() as $eagerLoad) { + $eagerLoad->load(); + } + + return $this; + } +} diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php b/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php new file mode 100644 index 000000000..75bd3c9cf --- /dev/null +++ b/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php @@ -0,0 +1,19 @@ +andWhere($conditions, $bindParams, $bindTypes); + } +} diff --git a/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php new file mode 100644 index 000000000..55ad79780 --- /dev/null +++ b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php @@ -0,0 +1,122 @@ + + * request->getQuery('page', 'int') - 1) * $limit; + * + * $manufacturers = Manufacturer::with('Robots.Parts', [ + * 'limit' => [$limit, $offset] + * ]); + * + * foreach ($manufacturers as $manufacturer) { + * foreach ($manufacturer->robots as $robot) { + * foreach ($robot->parts as $part) { ... } + * } + * } + * + * + * + * @param mixed ...$arguments + * @return Phalcon\Mvc\ModelInterface[] + */ + static public function with() { + $arguments = func_get_args(); + + if (! empty ($arguments)) { + $numArgs = count($arguments); + $lastArg = $numArgs - 1; + $parameters = NULL; + + if ($numArgs >= 2 && is_array($arguments[$lastArg])) { + $parameters = $arguments[$lastArg]; + + unset ($arguments[$lastArg]); + + if (isset ($parameters['columns'])) { + throw new \LogicException('Results from database must be full models, do not use `columns` key'); + } + } + } + else { + throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); + } + + $ret = static::find($parameters); + + if ($ret[0]) { + array_unshift($arguments, $ret); + + $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromResultset', $arguments); + } + + return $ret; + } + + /** + * Same as EagerLoadingTrait::with() for a single record + * + * @param mixed ...$arguments + * @return false|Phalcon\Mvc\ModelInterface + */ + static public function findFirstWith() { + $arguments = func_get_args(); + + if (! empty ($arguments)) { + $numArgs = count($arguments); + $lastArg = $numArgs - 1; + $parameters = NULL; + + if ($numArgs >= 2 && is_array($arguments[$lastArg])) { + $parameters = $arguments[$lastArg]; + + unset ($arguments[$lastArg]); + + if (isset ($parameters['columns'])) { + throw new \LogicException('Results from database must be full models, do not use `columns` key'); + } + } + } + else { + throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); + } + + if ($ret = static::findFirst($parameters)) { + array_unshift($arguments, $ret); + + $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); + } + + return $ret; + } + + /** + * + * load('Robots.Parts'); + * + * foreach ($manufacturer->robots as $robot) { + * foreach ($robot->parts as $part) { ... } + * } + * + * + * @param mixed ...$arguments + * @return self + */ + public function load() { + $arguments = func_get_args(); + + array_unshift($arguments, $this); + + return call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); + } +} diff --git a/Library/Phalcon/Mvc/Model/README.md b/Library/Phalcon/Mvc/Model/README.md new file mode 100644 index 000000000..20ff6d38d --- /dev/null +++ b/Library/Phalcon/Mvc/Model/README.md @@ -0,0 +1,56 @@ +Eager loading usage +----- + +The usage is similar to Laravel, in PHP >=5.4 within a model that uses `Phalcon\Mvc\Model\EagerLoadingTrait` trait you can do the following: + +```php +parts; // $robot->__get('parts') +} + +// Or + +$robot = Robot::findFirst()->load('Parts'); + +// Equivalent to: + +$robot = Robot::findFirst(); +$robots->parts; // $robot->__get('parts') + +// Because Robot::find() returns a resultset, so in that case this is solved with: +$robots = Loader::fromResultset(Robot::find(), 'Parts'); # Equivalent to the second example + +// Multiple and nested relations can be used too +$robots = Robot::with('Parts', 'Foo.Bar'); + +// And arguments can be passed to the find method +$robots = Robot::with('Parts', 'Foo.Bar', ['limit' => 5]); + +// And constraints +$robots = Robot::with( + [ + 'Parts', + 'Foo.Bar' => function (QueryBuilder $builder) { + // Limit Bar + $builder->limit(5); + } + ], + [ + 'limit' => 5 + ] +); + +``` + +PHP versions under 5.4 do not support traits, use instead `Phalcon\Mvc\Model\EagerLoading\Loader` methods + +Distributed as single package at https://github.com/stibiumz/phalcon.eager-loading From b6f0ad6762c73e3cd73944f66882602a04ef0c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 06:44:12 +0200 Subject: [PATCH 06/17] PSR2 fixes --- .../Mvc/Model/EagerLoading/EagerLoad.php | 467 ++++++------ .../Phalcon/Mvc/Model/EagerLoading/Loader.php | 699 +++++++++--------- .../Mvc/Model/EagerLoading/QueryBuilder.php | 28 +- .../Phalcon/Mvc/Model/EagerLoadingTrait.php | 236 +++--- 4 files changed, 713 insertions(+), 717 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php index 5464598be..543ac0d56 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php @@ -1,243 +1,238 @@ = 0; - } - - $this->relation = $relation; - $this->constraints = is_callable($constraints) ? $constraints : NULL; - $this->parent = $parent; - } - - /** - * @return null|Phalcon\Mvc\ModelInterface[] - */ - public function getSubject() { - return $this->subject; - } - - /** - * Executes each db query needed - * - * Note: The {$alias} property is set two times because Phalcon Model ignores - * empty arrays when overloading property set. - * - * Also {@see https://github.com/stibiumz/phalcon.eager-loading/issues/1} - * - * @return $this - */ - public function load() { - $relation = $this->relation; - - $alias = $relation->getOptions(); - $alias = strtolower($alias['alias']); - $relField = $relation->getFields(); - $relReferencedModel = $relation->getReferencedModel(); - $relReferencedField = $relation->getReferencedFields(); - $relIrModel = $relation->getIntermediateModel(); - $relIrField = $relation->getIntermediateFields(); - $relIrReferencedField = $relation->getIntermediateReferencedFields(); - - // PHQL has problems with this slash - if ($relReferencedModel[0] === '\\') { - $relReferencedModel = ltrim($relReferencedModel, '\\'); - } - - $bindValues = []; - - foreach ($this->parent->getSubject() as $record) { - $bindValues[$record->readAttribute($relField)] = TRUE; - } - - $bindValues = array_keys($bindValues); - - $subjectSize = count($this->parent->getSubject()); - $isManyToManyForMany = FALSE; - - $builder = new QueryBuilder; - $builder->from($relReferencedModel); - - if ($isThrough = $relation->isThrough()) { - if ($subjectSize === 1) { - // The query is for a single model - $builder - ->innerJoin( - $relIrModel, - sprintf( - '[%s].[%s] = [%s].[%s]', - $relIrModel, - $relIrReferencedField, - $relReferencedModel, - $relReferencedField - ) - ) - ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) - ; - } - else { - // The query is for many models, so it's needed to execute an - // extra query - $isManyToManyForMany = TRUE; - - $relIrValues = new QueryBuilder; - $relIrValues = $relIrValues - ->from($relIrModel) - ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) - ->getQuery() - ->execute() - ->setHydrateMode(Resultset::HYDRATE_ARRAYS) - ; - - $bindValues = $modelReferencedModelValues = array (); - - foreach ($relIrValues as $row) { - $bindValues[$row[$relIrReferencedField]] = TRUE; - $modelReferencedModelValues[$row[$relIrField]][$row[$relIrReferencedField]] = TRUE; - } - - unset ($relIrValues, $row); - - $builder->inWhere("[{$relReferencedField}]", array_keys($bindValues)); - } - } - else { - $builder->inWhere("[{$relReferencedField}]", $bindValues); - } - - if ($this->constraints) { - call_user_func($this->constraints, $builder); - } - - $records = array (); - - if ($isManyToManyForMany) { - foreach ($builder->getQuery()->execute() as $record) { - $records[$record->readAttribute($relReferencedField)] = $record; - } - - foreach ($this->parent->getSubject() as $record) { - $referencedFieldValue = $record->readAttribute($relField); - - if (isset ($modelReferencedModelValues[$referencedFieldValue])) { - $referencedModels = array (); - - foreach ($modelReferencedModelValues[$referencedFieldValue] as $idx => $_) { - $referencedModels[] = $records[$idx]; - } - - $record->{$alias} = $referencedModels; - - if (static::$isPhalcon2) { - $record->{$alias} = NULL; - $record->{$alias} = $referencedModels; - } - } - else { - $record->{$alias} = NULL; - $record->{$alias} = array (); - } - } - - $records = array_values($records); - } - else { - // We expect a single object or a set of it - $isSingle = ! $isThrough && ( - $relation->getType() === Relation::HAS_ONE || - $relation->getType() === Relation::BELONGS_TO - ); - - if ($subjectSize === 1) { - // Keep all records in memory - foreach ($builder->getQuery()->execute() as $record) { - $records[] = $record; - } - - $record = $this->parent->getSubject(); - $record = $record[0]; - - if ($isSingle) { - $record->{$alias} = empty ($records) ? NULL : $records[0]; - } - else { - if (empty ($records)) { - $record->{$alias} = NULL; - $record->{$alias} = array (); - } - else { - $record->{$alias} = $records; - - if (static::$isPhalcon2) { - $record->{$alias} = NULL; - $record->{$alias} = $records; - } - } - } - } - else { - $indexedRecords = array (); - - // Keep all records in memory - foreach ($builder->getQuery()->execute() as $record) { - $records[] = $record; - - if ($isSingle) { - $indexedRecords[$record->readAttribute($relReferencedField)] = $record; - } - else { - $indexedRecords[$record->readAttribute($relReferencedField)][] = $record; - } - } - - foreach ($this->parent->getSubject() as $record) { - $referencedFieldValue = $record->readAttribute($relField); - - if (isset ($indexedRecords[$referencedFieldValue])) { - $record->{$alias} = $indexedRecords[$referencedFieldValue]; - - if (static::$isPhalcon2 && is_array($indexedRecords[$referencedFieldValue])) { - $record->{$alias} = NULL; - $record->{$alias} = $indexedRecords[$referencedFieldValue]; - } - } - else { - $record->{$alias} = NULL; - - if (! $isSingle) { - $record->{$alias} = array (); - } - } - } - } - } - - $this->subject = $records; - - return $this; - } +final class EagerLoad +{ + /** @var RelationInterface */ + private $relation; + /** @var null|callable */ + private $constraints; + /** @var Loader|EagerLoad */ + private $parent; + /** @var null|Phalcon\Mvc\ModelInterface[] */ + private $subject; + /** @var boolean */ + private static $isPhalcon2; + + /** + * @param RelationInterface + * @param null|callable $constraints + * @param Loader|EagerLoad $parent + */ + public function __construct(Relation $relation, $constraints, $parent) + { + if (static::$isPhalcon2 === null) { + static::$isPhalcon2 = version_compare(\Phalcon\Version::get(), '2.0.0') >= 0; + } + + $this->relation = $relation; + $this->constraints = is_callable($constraints) ? $constraints : null; + $this->parent = $parent; + } + + /** + * @return null|Phalcon\Mvc\ModelInterface[] + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Executes each db query needed + * + * Note: The {$alias} property is set two times because Phalcon Model ignores + * empty arrays when overloading property set. + * + * Also {@see https://github.com/stibiumz/phalcon.eager-loading/issues/1} + * + * @return $this + */ + public function load() + { + $relation = $this->relation; + + $alias = $relation->getOptions(); + $alias = strtolower($alias['alias']); + $relField = $relation->getFields(); + $relReferencedModel = $relation->getReferencedModel(); + $relReferencedField = $relation->getReferencedFields(); + $relIrModel = $relation->getIntermediateModel(); + $relIrField = $relation->getIntermediateFields(); + $relIrReferencedField = $relation->getIntermediateReferencedFields(); + + // PHQL has problems with this slash + if ($relReferencedModel[0] === '\\') { + $relReferencedModel = ltrim($relReferencedModel, '\\'); + } + + $bindValues = []; + + foreach ($this->parent->getSubject() as $record) { + $bindValues[$record->readAttribute($relField)] = true; + } + + $bindValues = array_keys($bindValues); + + $subjectSize = count($this->parent->getSubject()); + $isManyToManyForMany = false; + + $builder = new QueryBuilder; + $builder->from($relReferencedModel); + + if ($isThrough = $relation->isThrough()) { + if ($subjectSize === 1) { + // The query is for a single model + $builder + ->innerJoin( + $relIrModel, + sprintf( + '[%s].[%s] = [%s].[%s]', + $relIrModel, + $relIrReferencedField, + $relReferencedModel, + $relReferencedField + ) + ) + ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) + ; + } else { + // The query is for many models, so it's needed to execute an + // extra query + $isManyToManyForMany = true; + + $relIrValues = new QueryBuilder; + $relIrValues = $relIrValues + ->from($relIrModel) + ->inWhere("[{$relIrModel}].[{$relIrField}]", $bindValues) + ->getQuery() + ->execute() + ->setHydrateMode(Resultset::HYDRATE_ARRAYS) + ; + + $bindValues = $modelReferencedModelValues = array (); + + foreach ($relIrValues as $row) { + $bindValues[$row[$relIrReferencedField]] = true; + $modelReferencedModelValues[$row[$relIrField]][$row[$relIrReferencedField]] = true; + } + + unset ($relIrValues, $row); + + $builder->inWhere("[{$relReferencedField}]", array_keys($bindValues)); + } + } else { + $builder->inWhere("[{$relReferencedField}]", $bindValues); + } + + if ($this->constraints) { + call_user_func($this->constraints, $builder); + } + + $records = array (); + + if ($isManyToManyForMany) { + foreach ($builder->getQuery()->execute() as $record) { + $records[$record->readAttribute($relReferencedField)] = $record; + } + + foreach ($this->parent->getSubject() as $record) { + $referencedFieldValue = $record->readAttribute($relField); + + if (isset ($modelReferencedModelValues[$referencedFieldValue])) { + $referencedModels = array (); + + foreach ($modelReferencedModelValues[$referencedFieldValue] as $idx => $_) { + $referencedModels[] = $records[$idx]; + } + + $record->{$alias} = $referencedModels; + + if (static::$isPhalcon2) { + $record->{$alias} = null; + $record->{$alias} = $referencedModels; + } + } else { + $record->{$alias} = null; + $record->{$alias} = array (); + } + } + + $records = array_values($records); + } else { + // We expect a single object or a set of it + $isSingle = ! $isThrough && ( + $relation->getType() === Relation::HAS_ONE || + $relation->getType() === Relation::BELONGS_TO + ); + + if ($subjectSize === 1) { + // Keep all records in memory + foreach ($builder->getQuery()->execute() as $record) { + $records[] = $record; + } + + $record = $this->parent->getSubject(); + $record = $record[0]; + + if ($isSingle) { + $record->{$alias} = empty ($records) ? null : $records[0]; + } else { + if (empty ($records)) { + $record->{$alias} = null; + $record->{$alias} = array (); + } else { + $record->{$alias} = $records; + + if (static::$isPhalcon2) { + $record->{$alias} = null; + $record->{$alias} = $records; + } + } + } + } else { + $indexedRecords = array (); + + // Keep all records in memory + foreach ($builder->getQuery()->execute() as $record) { + $records[] = $record; + + if ($isSingle) { + $indexedRecords[$record->readAttribute($relReferencedField)] = $record; + } else { + $indexedRecords[$record->readAttribute($relReferencedField)][] = $record; + } + } + + foreach ($this->parent->getSubject() as $record) { + $referencedFieldValue = $record->readAttribute($relField); + + if (isset ($indexedRecords[$referencedFieldValue])) { + $record->{$alias} = $indexedRecords[$referencedFieldValue]; + + if (static::$isPhalcon2 && is_array($indexedRecords[$referencedFieldValue])) { + $record->{$alias} = null; + $record->{$alias} = $indexedRecords[$referencedFieldValue]; + } + } else { + $record->{$alias} = null; + + if (! $isSingle) { + $record->{$alias} = array (); + } + } + } + } + } + + $this->subject = $records; + + return $this; + } } diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php index c08f41432..4404b168d 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php @@ -1,355 +1,350 @@ mustReturnAModel = FALSE; - } - else { - $className = get_class($from); - $from = array ($from); - - $this->mustReturnAModel = TRUE; - } - - if ($error) { - throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); - } - - $this->subject = $from; - $this->subjectClassName = $className; - $this->eagerLoads = empty ($arguments) ? array () : static::parseArguments($arguments); - } - - /** - * Create and get from a mixed $subject - * - * @param ModelInterface|ModelInterface[]|Simple $subject - * @param mixed ...$arguments - * @throws \InvalidArgumentException - * @return mixed - */ - static public function from($subject) { - if ($subject instanceof ModelInterface) { - $ret = call_user_func_array('static::fromModel', func_get_args()); - } - else if ($subject instanceof Simple) { - $ret = call_user_func_array('static::fromResultset', func_get_args()); - } - else if (is_array($subject)) { - $ret = call_user_func_array('static::fromArray', func_get_args()); - } - else { - throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); - } - - return $ret; - } - - /** - * Create and get from a Model - * - * @param ModelInterface $subject - * @param mixed ...$arguments - * @return ModelInterface - */ - static public function fromModel(ModelInterface $subject) { - $reflection = new \ReflectionClass(__CLASS__); - $instance = $reflection->newInstanceArgs(func_get_args()); - - return $instance->execute()->get(); - } - - /** - * Create and get from an array - * - * @param ModelInterface[] $subject - * @param mixed ...$arguments - * @return array - */ - static public function fromArray(array $subject) { - $reflection = new \ReflectionClass(__CLASS__); - $instance = $reflection->newInstanceArgs(func_get_args()); - - return $instance->execute()->get(); - } - - /** - * Create and get from a Resultset - * - * @param Simple $subject - * @param mixed ...$arguments - * @return Simple - */ - static public function fromResultset(Simple $subject) { - $reflection = new \ReflectionClass(__CLASS__); - $instance = $reflection->newInstanceArgs(func_get_args()); - - return $instance->execute()->get(); - } - - /** - * @return null|ModelInterface[]|ModelInterface - */ - public function get() { - $ret = $this->subject; - - if (NULL !== $ret && $this->mustReturnAModel) { - $ret = $ret[0]; - } - - return $ret; - } - - /** - * @return null|ModelInterface[] - */ - public function getSubject() { - return $this->subject; - } - - /** - * Parses the arguments that will be resolved to Relation instances - * - * @param array $arguments - * @throws \InvalidArgumentException - * @return array - */ - static private function parseArguments(array $arguments) { - if (empty ($arguments)) { - throw new \InvalidArgumentException('Arguments can not be empty'); - } - - $relations = array (); - - if (count($arguments) === 1 && isset ($arguments[0]) && is_array($arguments[0])) { - foreach ($arguments[0] as $relationAlias => $queryConstraints) { - if (is_string($relationAlias)) { - $relations[$relationAlias] = is_callable($queryConstraints) ? $queryConstraints : NULL; - } - else { - if (is_string($queryConstraints)) { - $relations[$queryConstraints] = NULL; - } - } - } - } - else { - foreach ($arguments as $relationAlias) { - if (is_string($relationAlias)) { - $relations[$relationAlias] = NULL; - } - } - } - - if (empty ($relations)) { - throw new \InvalidArgumentException; - } - - return $relations; - } - - /** - * @param string $relationAlias - * @param null|callable $constraints - * @return $this - */ - public function addEagerLoad($relationAlias, $constraints = NULL) { - if (! is_string($relationAlias)) { - throw new \InvalidArgumentException(sprintf( - '$relationAlias expects to be a string, `%s` given', - gettype($relationAlias) - )); - } - - if ($constraints !== NULL && ! is_callable($constraints)) { - throw new \InvalidArgumentException(sprintf( - '$constraints expects to be a callable, `%s` given', - gettype($constraints) - )); - } - - $this->eagerLoads[$relationAlias] = $constraints; - - return $this; - } - - /** - * Resolves the relations - * - * @throws \RuntimeException - * @return EagerLoad[] - */ - private function buildTree() { - uksort($this->eagerLoads, 'strcmp'); - - $di = \Phalcon\DI::getDefault(); - $mM = $di['modelsManager']; - - $eagerLoads = $resolvedRelations = array (); - - foreach ($this->eagerLoads as $relationAliases => $queryConstraints) { - $nestingLevel = 0; - $relationAliases = explode('.', $relationAliases); - $nestingLevels = count($relationAliases); - - do { - do { - $alias = $relationAliases[$nestingLevel]; - $name = join('.', array_slice($relationAliases, 0, $nestingLevel + 1)); - } - while (isset ($eagerLoads[$name]) && ++$nestingLevel); - - if ($nestingLevel === 0) { - $parentClassName = $this->subjectClassName; - } - else { - $parentName = join('.', array_slice($relationAliases, 0, $nestingLevel)); - $parentClassName = $resolvedRelations[$parentName]->getReferencedModel(); - - if ($parentClassName[0] === '\\') { - ltrim($parentClassName, '\\'); - } - } - - if (! isset ($resolvedRelations[$name])) { - $mM->load($parentClassName); - $relation = $mM->getRelationByAlias($parentClassName, $alias); - - if (! $relation instanceof Relation) { - throw new \RuntimeException(sprintf( - 'There is no defined relation for the model `%s` using alias `%s`', - $parentClassName, - $alias - )); - } - - $resolvedRelations[$name] = $relation; - } - else { - $relation = $resolvedRelations[$name]; - } - - $relType = $relation->getType(); - - if ($relType !== Relation::BELONGS_TO && - $relType !== Relation::HAS_ONE && - $relType !== Relation::HAS_MANY && - $relType !== Relation::HAS_MANY_THROUGH) { - - throw new \RuntimeException(sprintf('Unknown relation type `%s`', $relType)); - } - - if (is_array($relation->getFields()) || - is_array($relation->getReferencedFields())) { - - throw new \RuntimeException('Relations with composite keys are not supported'); - } - - $parent = $nestingLevel > 0 ? $eagerLoads[$parentName] : $this; - $constraints = $nestingLevel + 1 === $nestingLevels ? $queryConstraints : NULL; - - $eagerLoads[$name] = new EagerLoad($relation, $constraints, $parent); - } - while (++$nestingLevel < $nestingLevels); - } - - return $eagerLoads; - } - - /** - * @return $this - */ - public function execute() { - foreach ($this->buildTree() as $eagerLoad) { - $eagerLoad->load(); - } - - return $this; - } - - /** - * Loader::execute() alias - * - * @return $this - */ - public function load() { - foreach ($this->buildTree() as $eagerLoad) { - $eagerLoad->load(); - } - - return $this; - } +use Phalcon\Mvc\ModelInterface; +use Phalcon\Mvc\Model\Relation; +use Phalcon\Mvc\Model\Resultset\Simple; + +final class Loader +{ + const E_INVALID_SUBJECT = 'Expected value of `subject` is either a ModelInterface object, a Simple object or an array of ModelInterface objects'; + + /** @var ModelInterface[] */ + protected $subject; + /** @var string */ + protected $subjectClassName; + /** @var array */ + protected $eagerLoads; + /** @var boolean */ + protected $mustReturnAModel; + + /** + * @param ModelInterface|ModelInterface[]|Simple $from + * @param ...$arguments + * @throws \InvalidArgumentException + */ + public function __construct($from) + { + $error = false; + $arguments = array_slice(func_get_args(), 1); + + if (! $from instanceof ModelInterface) { + if (! $from instanceof Simple) { + if (! is_array($from)) { + $error = true; + } else { + $from = array_filter($from); + + if (empty ($from)) { + $error = true; + } else { + $className = null; + + foreach ($from as $el) { + if ($el instanceof ModelInterface) { + if ($className === null) { + $className = get_class($el); + } else { + if ($className !== get_class($el)) { + $error = true; + break; + } + } + } else { + $error = true; + break; + } + } + } + } + } else { + $prev = $from; + $from = array (); + + foreach ($prev as $record) { + $from[] = $record; + } + + if (empty ($from)) { + $error = true; + } else { + $className = get_class($record); + } + } + + $this->mustReturnAModel = false; + } else { + $className = get_class($from); + $from = array ($from); + + $this->mustReturnAModel = true; + } + + if ($error) { + throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); + } + + $this->subject = $from; + $this->subjectClassName = $className; + $this->eagerLoads = empty ($arguments) ? array () : static::parseArguments($arguments); + } + + /** + * Create and get from a mixed $subject + * + * @param ModelInterface|ModelInterface[]|Simple $subject + * @param mixed ...$arguments + * @throws \InvalidArgumentException + * @return mixed + */ + public static function from($subject) + { + if ($subject instanceof ModelInterface) { + $ret = call_user_func_array('static::fromModel', func_get_args()); + } elseif ($subject instanceof Simple) { + $ret = call_user_func_array('static::fromResultset', func_get_args()); + } elseif (is_array($subject)) { + $ret = call_user_func_array('static::fromArray', func_get_args()); + } else { + throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); + } + + return $ret; + } + + /** + * Create and get from a Model + * + * @param ModelInterface $subject + * @param mixed ...$arguments + * @return ModelInterface + */ + public static function fromModel(ModelInterface $subject) + { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * Create and get from an array + * + * @param ModelInterface[] $subject + * @param mixed ...$arguments + * @return array + */ + public static function fromArray(array $subject) + { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * Create and get from a Resultset + * + * @param Simple $subject + * @param mixed ...$arguments + * @return Simple + */ + public static function fromResultset(Simple $subject) + { + $reflection = new \ReflectionClass(__CLASS__); + $instance = $reflection->newInstanceArgs(func_get_args()); + + return $instance->execute()->get(); + } + + /** + * @return null|ModelInterface[]|ModelInterface + */ + public function get() + { + $ret = $this->subject; + + if (null !== $ret && $this->mustReturnAModel) { + $ret = $ret[0]; + } + + return $ret; + } + + /** + * @return null|ModelInterface[] + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Parses the arguments that will be resolved to Relation instances + * + * @param array $arguments + * @throws \InvalidArgumentException + * @return array + */ + private static function parseArguments(array $arguments) + { + if (empty ($arguments)) { + throw new \InvalidArgumentException('Arguments can not be empty'); + } + + $relations = array (); + + if (count($arguments) === 1 && isset ($arguments[0]) && is_array($arguments[0])) { + foreach ($arguments[0] as $relationAlias => $queryConstraints) { + if (is_string($relationAlias)) { + $relations[$relationAlias] = is_callable($queryConstraints) ? $queryConstraints : null; + } else { + if (is_string($queryConstraints)) { + $relations[$queryConstraints] = null; + } + } + } + } else { + foreach ($arguments as $relationAlias) { + if (is_string($relationAlias)) { + $relations[$relationAlias] = null; + } + } + } + + if (empty ($relations)) { + throw new \InvalidArgumentException; + } + + return $relations; + } + + /** + * @param string $relationAlias + * @param null|callable $constraints + * @return $this + */ + public function addEagerLoad($relationAlias, $constraints = null) + { + if (! is_string($relationAlias)) { + throw new \InvalidArgumentException(sprintf( + '$relationAlias expects to be a string, `%s` given', + gettype($relationAlias) + )); + } + + if ($constraints !== null && ! is_callable($constraints)) { + throw new \InvalidArgumentException(sprintf( + '$constraints expects to be a callable, `%s` given', + gettype($constraints) + )); + } + + $this->eagerLoads[$relationAlias] = $constraints; + + return $this; + } + + /** + * Resolves the relations + * + * @throws \RuntimeException + * @return EagerLoad[] + */ + private function buildTree() + { + uksort($this->eagerLoads, 'strcmp'); + + $di = \Phalcon\DI::getDefault(); + $mM = $di['modelsManager']; + + $eagerLoads = $resolvedRelations = array (); + + foreach ($this->eagerLoads as $relationAliases => $queryConstraints) { + $nestingLevel = 0; + $relationAliases = explode('.', $relationAliases); + $nestingLevels = count($relationAliases); + + do { + do { + $alias = $relationAliases[$nestingLevel]; + $name = join('.', array_slice($relationAliases, 0, $nestingLevel + 1)); + } while (isset ($eagerLoads[$name]) && ++$nestingLevel); + + if ($nestingLevel === 0) { + $parentClassName = $this->subjectClassName; + } else { + $parentName = join('.', array_slice($relationAliases, 0, $nestingLevel)); + $parentClassName = $resolvedRelations[$parentName]->getReferencedModel(); + + if ($parentClassName[0] === '\\') { + ltrim($parentClassName, '\\'); + } + } + + if (! isset ($resolvedRelations[$name])) { + $mM->load($parentClassName); + $relation = $mM->getRelationByAlias($parentClassName, $alias); + + if (! $relation instanceof Relation) { + throw new \RuntimeException(sprintf( + 'There is no defined relation for the model `%s` using alias `%s`', + $parentClassName, + $alias + )); + } + + $resolvedRelations[$name] = $relation; + } else { + $relation = $resolvedRelations[$name]; + } + + $relType = $relation->getType(); + + if ($relType !== Relation::BELONGS_TO && + $relType !== Relation::HAS_ONE && + $relType !== Relation::HAS_MANY && + $relType !== Relation::HAS_MANY_THROUGH) { + throw new \RuntimeException(sprintf('Unknown relation type `%s`', $relType)); + } + + if (is_array($relation->getFields()) || + is_array($relation->getReferencedFields())) { + throw new \RuntimeException('Relations with composite keys are not supported'); + } + + $parent = $nestingLevel > 0 ? $eagerLoads[$parentName] : $this; + $constraints = $nestingLevel + 1 === $nestingLevels ? $queryConstraints : null; + + $eagerLoads[$name] = new EagerLoad($relation, $constraints, $parent); + } while (++$nestingLevel < $nestingLevels); + } + + return $eagerLoads; + } + + /** + * @return $this + */ + public function execute() + { + foreach ($this->buildTree() as $eagerLoad) { + $eagerLoad->load(); + } + + return $this; + } + + /** + * Loader::execute() alias + * + * @return $this + */ + public function load() + { + foreach ($this->buildTree() as $eagerLoad) { + $eagerLoad->load(); + } + + return $this; + } } diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php b/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php index 75bd3c9cf..95c1d9e8c 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/QueryBuilder.php @@ -2,18 +2,22 @@ use Phalcon\Mvc\Model\Query\Builder; -final class QueryBuilder extends Builder { - const E_NOT_ALLOWED_METHOD_CALL = 'When eager loading relations queries must return full entities'; - - public function distinct($distinct) { - throw new \LogicException(static::E_NOT_ALLOWED_METHOD_CALL); - } +final class QueryBuilder extends Builder +{ + const E_NOT_ALLOWED_METHOD_CALL = 'When eager loading relations queries must return full entities'; + + public function distinct($distinct) + { + throw new \LogicException(static::E_NOT_ALLOWED_METHOD_CALL); + } - public function columns($columns) { - throw new \LogicException(static::E_NOT_ALLOWED_METHOD_CALL); - } + public function columns($columns) + { + throw new \LogicException(static::E_NOT_ALLOWED_METHOD_CALL); + } - public function where($conditions, $bindParams = NULL, $bindTypes = NULL) { - return $this->andWhere($conditions, $bindParams, $bindTypes); - } + public function where($conditions, $bindParams = null, $bindTypes = null) + { + return $this->andWhere($conditions, $bindParams, $bindTypes); + } } diff --git a/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php index 55ad79780..9adb31f08 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php +++ b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php @@ -2,121 +2,123 @@ use Phalcon\Mvc\Model\EagerLoading\Loader; -trait EagerLoadingTrait { - - /** - * - * request->getQuery('page', 'int') - 1) * $limit; - * - * $manufacturers = Manufacturer::with('Robots.Parts', [ - * 'limit' => [$limit, $offset] - * ]); - * - * foreach ($manufacturers as $manufacturer) { - * foreach ($manufacturer->robots as $robot) { - * foreach ($robot->parts as $part) { ... } - * } - * } - * - * - * - * @param mixed ...$arguments - * @return Phalcon\Mvc\ModelInterface[] - */ - static public function with() { - $arguments = func_get_args(); - - if (! empty ($arguments)) { - $numArgs = count($arguments); - $lastArg = $numArgs - 1; - $parameters = NULL; - - if ($numArgs >= 2 && is_array($arguments[$lastArg])) { - $parameters = $arguments[$lastArg]; - - unset ($arguments[$lastArg]); - - if (isset ($parameters['columns'])) { - throw new \LogicException('Results from database must be full models, do not use `columns` key'); - } - } - } - else { - throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); - } - - $ret = static::find($parameters); - - if ($ret[0]) { - array_unshift($arguments, $ret); - - $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromResultset', $arguments); - } - - return $ret; - } - - /** - * Same as EagerLoadingTrait::with() for a single record - * - * @param mixed ...$arguments - * @return false|Phalcon\Mvc\ModelInterface - */ - static public function findFirstWith() { - $arguments = func_get_args(); - - if (! empty ($arguments)) { - $numArgs = count($arguments); - $lastArg = $numArgs - 1; - $parameters = NULL; - - if ($numArgs >= 2 && is_array($arguments[$lastArg])) { - $parameters = $arguments[$lastArg]; - - unset ($arguments[$lastArg]); - - if (isset ($parameters['columns'])) { - throw new \LogicException('Results from database must be full models, do not use `columns` key'); - } - } - } - else { - throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); - } - - if ($ret = static::findFirst($parameters)) { - array_unshift($arguments, $ret); - - $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); - } - - return $ret; - } - - /** - * - * load('Robots.Parts'); - * - * foreach ($manufacturer->robots as $robot) { - * foreach ($robot->parts as $part) { ... } - * } - * - * - * @param mixed ...$arguments - * @return self - */ - public function load() { - $arguments = func_get_args(); - - array_unshift($arguments, $this); - - return call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); - } +trait EagerLoadingTrait +{ + + /** + * + * request->getQuery('page', 'int') - 1) * $limit; + * + * $manufacturers = Manufacturer::with('Robots.Parts', [ + * 'limit' => [$limit, $offset] + * ]); + * + * foreach ($manufacturers as $manufacturer) { + * foreach ($manufacturer->robots as $robot) { + * foreach ($robot->parts as $part) { ... } + * } + * } + * + * + * + * @param mixed ...$arguments + * @return Phalcon\Mvc\ModelInterface[] + */ + public static function with() + { + $arguments = func_get_args(); + + if (! empty ($arguments)) { + $numArgs = count($arguments); + $lastArg = $numArgs - 1; + $parameters = null; + + if ($numArgs >= 2 && is_array($arguments[$lastArg])) { + $parameters = $arguments[$lastArg]; + + unset ($arguments[$lastArg]); + + if (isset ($parameters['columns'])) { + throw new \LogicException('Results from database must be full models, do not use `columns` key'); + } + } + } else { + throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); + } + + $ret = static::find($parameters); + + if ($ret[0]) { + array_unshift($arguments, $ret); + + $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromResultset', $arguments); + } + + return $ret; + } + + /** + * Same as EagerLoadingTrait::with() for a single record + * + * @param mixed ...$arguments + * @return false|Phalcon\Mvc\ModelInterface + */ + public static function findFirstWith() + { + $arguments = func_get_args(); + + if (! empty ($arguments)) { + $numArgs = count($arguments); + $lastArg = $numArgs - 1; + $parameters = null; + + if ($numArgs >= 2 && is_array($arguments[$lastArg])) { + $parameters = $arguments[$lastArg]; + + unset ($arguments[$lastArg]); + + if (isset ($parameters['columns'])) { + throw new \LogicException('Results from database must be full models, do not use `columns` key'); + } + } + } else { + throw new \BadMethodCallException(sprintf('%s requires at least one argument', __METHOD__)); + } + + if ($ret = static::findFirst($parameters)) { + array_unshift($arguments, $ret); + + $ret = call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); + } + + return $ret; + } + + /** + * + * load('Robots.Parts'); + * + * foreach ($manufacturer->robots as $robot) { + * foreach ($robot->parts as $part) { ... } + * } + * + * + * @param mixed ...$arguments + * @return self + */ + public function load() + { + $arguments = func_get_args(); + + array_unshift($arguments, $this); + + return call_user_func_array('Phalcon\Mvc\Model\EagerLoading\Loader::fromModel', $arguments); + } } From 7c721097a555facbee63bc7c586a099bc0153fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 09:22:57 +0200 Subject: [PATCH 07/17] Add eager-loading tests --- .travis.yml | 2 + .../Model/EagerLoading/EagerLoadingTest.php | 391 ++++++++++++++++++ .../resources/Models/AbstractModel-PHP53.php | 5 + .../resources/Models/AbstractModel-PHP54.php | 6 + .../resources/Models/AbstractModel.php | 7 + .../EagerLoading/resources/Models/Bug.php | 20 + .../resources/Models/Manufacturer.php | 19 + .../resources/Models/NotSupportedRelation.php | 23 ++ .../EagerLoading/resources/Models/Part.php | 27 ++ .../EagerLoading/resources/Models/Purpose.php | 20 + .../EagerLoading/resources/Models/Robot.php | 74 ++++ .../resources/Models/RobotPart.php | 20 + .../Model/EagerLoading/resources/schema.sql | 55 +++ 13 files changed, 669 insertions(+) create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel-PHP53.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel-PHP54.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Bug.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Manufacturer.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/NotSupportedRelation.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Part.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Purpose.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Robot.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/RobotPart.php create mode 100644 tests/Phalcon/Mvc/Model/EagerLoading/resources/schema.sql diff --git a/.travis.yml b/.travis.yml index 6978eb42e..027ad191f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ before_script: - php -r 'echo \Phalcon\Version::get()."\n";' - composer self-update - composer update + - mysql -e 'create database eager_loading_tests charset=utf8mb4 collate=utf8mb4_unicode_ci;' + - mysql -uroot eager_loading_tests < tests/Phalcon/Mvc/Model/EagerLoading/resources/schema.sql php: - 5.3 diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php new file mode 100644 index 000000000..1d84ee95f --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php @@ -0,0 +1,391 @@ +previousDependencyInjector = DI::getDefault(); + + $di = new DI; + + $di->set('modelsMetadata', function () { + return new MemoryMetadata; + }, true); + + $di->set('modelsManager', function () { + return new ModelsManager; + }, true); + + $di->set('db', function () { + return new MysqlAdapter(array ( + 'host' => 'localhost', + 'port' => '3306', + 'username' => 'root', + 'password' => '', + 'dbname' => 'eager_loading_tests', + 'charset' => 'utf8mb4', + )); + }, true); + + if ($this->previousDependencyInjector instanceof DI) { + DI::setDefault($di); + } + + spl_autoload_register(array (__CLASS__, 'autoloadModels'), true, true); + } + + public function tearDownAfterClass() + { + if ($this->previousDependencyInjector instanceof DI) { + DI::setDefault($this->previousDependencyInjector); + } + + spl_autoload_unregister(array (__CLASS__, 'autoloadModels')); + } + + public static function autoloadModels($class) + { + $len = strlen($prefix); + + if (strpos($class, 'EagerLoadingTestModel\\') === 0) { + $class = substr($class, strlen('EagerLoadingTestModel\\')); + + if (ctype_alpha($class)) { + $file = __DIR__ . "/resources/Models/{$class}.php"; + + if (file_exists($file)) { + require $file; + } + } + } + } + + public function testBelongsTo() + { + $rawly = Bug::findFirstById(1); + $rawly->robot; + + $eagerly = Loader::fromModel(Bug::findFirstById(1), 'Robot'); + + $this->assertTrue(property_exists($eagerly, 'robot')); + $this->assertInstanceOf('EagerLoadingTestModel\Robot', $eagerly->robot); + $this->assertEquals($rawly->robot->readAttribute('id'), $eagerly->robot->readAttribute('id')); + + // Reverse + $rawly = Robot::findFirstById(2); + $rawly->bugs = $this->resultSetToEagerLoadingEquivalent($rawly->bugs); + + $eagerly = Loader::fromModel(Robot::findFirstById(2), 'Bugs'); + + $this->assertTrue(property_exists($eagerly, 'bugs')); + $this->assertContainsOnlyInstancesOf('EagerLoadingTestModel\Bug', $eagerly->bugs); + + $getIds = function ($obj) { + return $obj->readAttribute('id'); + }; + + $this->assertEquals(array_map($getIds, $rawly->bugs), array_map($getIds, $eagerly->bugs)); + $this->assertEmpty(Loader::fromModel(Robot::findFirstById(1), 'Bugs')->bugs); + + // Test from multiple + $rawly = $this->resultSetToEagerLoadingEquivalent(Bug::find(array ('limit' => 10))); + foreach ($rawly as $bug) { + $bug->robot; + } + + $eagerly = Loader::fromResultset(Bug::find(array ('limit' => 10)), 'Robot'); + + $this->assertTrue(is_array($eagerly)); + $this->assertTrue(array_reduce($eagerly, function ($res, $bug) { + return $res && property_exists($bug, 'robot'); + }, true)); + + $getIds = function ($obj) { + return property_exists($obj, 'robot') && isset ($obj->robot) ? $obj->robot->readAttribute('id') : null; + }; + + $this->assertEquals(array_map($getIds, $rawly), array_map($getIds, $eagerly)); + } + + public function testBelongsToDeep() + { + $rawly = Manufacturer::findFirstById(1); + $rawly->robots = $this->resultSetToEagerLoadingEquivalent($rawly->robots); + + foreach ($rawly->robots as $robot) { + $robot->parent; + } + + $eagerly = Loader::fromModel(Manufacturer::findFirstById(1), 'Robots.Parent'); + + $this->assertTrue(property_exists($eagerly->robots[0], 'parent')); + $this->assertNull($eagerly->robots[0]->parent); + $this->assertInstanceOf('EagerLoadingTestModel\Robot', $eagerly->robots[2]->parent); + + $getIds = function ($obj) { + return property_exists($obj, 'parent') && isset ($obj->parent) ? $obj->parent->readAttribute('id') : null; + }; + + $this->assertEquals(array_map($getIds, $eagerly->robots), array_map($getIds, $rawly->robots)); + } + + public function testHasOne() + { + $rawly = Robot::findFirstById(1); + $rawly->purpose; + + $eagerly = Loader::fromModel(Robot::findFirstById(1), 'Purpose'); + + $this->assertTrue(property_exists($eagerly, 'purpose')); + $this->assertInstanceOf('EagerLoadingTestModel\Purpose', $eagerly->purpose); + $this->assertEquals($rawly->purpose->readAttribute('id'), $eagerly->purpose->readAttribute('id')); + } + + public function testHasMany() + { + $rawly = Manufacturer::findFirstById(1); + $rawly->robots; + + $eagerly = Loader::fromModel(Manufacturer::findFirstById(1), 'Robots'); + + $this->assertTrue(property_exists($eagerly, 'robots')); + $this->assertTrue(is_array($eagerly->robots)); + $this->assertSame(count($eagerly->robots), $rawly->robots->count()); + + $getIds = function ($arr) { + $ret = array (); + + foreach ($arr as $r) { + if (is_object($r)) { + $ret[] = $r->readAttribute('id'); + } + } + + return $ret; + }; + + $this->assertEquals( + $getIds($this->resultSetToEagerLoadingEquivalent($rawly->robots)), + $getIds($eagerly->robots) + ); + } + + public function testHasManyToMany() + { + $rawly = Robot::findFirstById(1); + $rawly->parts; + + $eagerly = Loader::fromModel(Robot::findFirstById(1), 'Parts'); + + $this->assertTrue(property_exists($eagerly, 'parts')); + $this->assertTrue(is_array($eagerly->parts)); + $this->assertSame(count($eagerly->parts), $rawly->parts->count()); + + $getIds = function ($arr) { + $ret = array (); + + foreach ($arr as $r) { + if (is_object($r)) { + $ret[] = $r->readAttribute('id'); + } + } + + return $ret; + }; + + $this->assertEquals( + $getIds($this->resultSetToEagerLoadingEquivalent($rawly->parts)), + $getIds($eagerly->parts) + ); + } + + /** + * @requires PHP 5.4 + */ + public function testModelMethods() + { + $this->assertTrue(is_array(Robot::with('Parts'))); + $this->assertTrue(is_object(Robot::findFirstById(1)->load('Parts'))); + $this->assertTrue(is_object(Robot::findFirstWith('Parts', array ('id = 1')))); + } + + /** + * @requires PHP 5.4 + * @dataProvider dp1 + */ + public function testShouldThrowBadMethodCallExceptionIfArgumentsWereNotProvided($method) + { + $this->setExpectedException('BadMethodCallException'); + call_user_func(array ('Robot', $method)); + } + + public function dp1() + { + return array (array ('with'), array ('findFirstWith')); + } + + /** + * @requires PHP 5.4 + * @dataProvider dp2 + */ + public function testShouldThrowLogicExceptionIfTheEntityWillBeIncomplete($method, $args) + { + $this->setExpectedException('LogicException'); + call_user_func_array(array ('Robot', $method), $args); + } + + public function dp2() + { + return array ( + array ('with', array ('Parts', array ('columns' => 'id'))), + array ('findFirstWith', array ('Parts', array ('columns' => 'id'))), + array ('with', array (array ('Parts' => function ($builder) { + $builder->columns(array ('id')); + }))), + ); + } + + /** + * @dataProvider dp3 + */ + public function testShouldThrowInvalidArgumentExceptionIfLoaderSubjectIsNotValid($args) + { + $this->setExpectedException('InvalidArgumentException'); + $reflection = new ReflectionClass('Phalcon\Mvc\Model\EagerLoading\Loader'); + $reflection->newInstance($args); + } + + public function dp3() + { + return array ( + array (null), + array (array ()), + array (range(0, 5)), + array (array (Robot::findFirstById(1), Bug::findFirstById(1))), + array (Robot::find('id > 1000')) + ); + } + + /** + * @dataProvider dp4 + */ + public function testShouldThrowRuntimeExceptionIfTheRelationIsNotDefinedOrSupported($args) + { + $this->setExpectedException('RuntimeException'); + $reflection = new ReflectionClass('Phalcon\Mvc\Model\EagerLoading\Loader'); + $reflection->newInstanceArgs($args)->execute(); + } + + public function dp4() + { + return array ( + array (array (Robot::findFirst(), 'NotSupportedRelations')), + array (array (Robot::findFirst(), 'NonexistentRelation')), + ); + } + + /** + * @requires PHP 5.4 + */ + public function testManyEagerLoadsAndConstraints() + { + $manufacturers = Manufacturer::with( + array ( + 'Robots' => function ($builder) { + $builder->where('id < 25'); + }, + 'Robots.Bugs' => function ($builder) { + $builder->limit(2); + }, + 'Robots.Parts' + ), + array ('id < 50') + ); + + $this->assertEquals( + array_sum(array_map(function ($o) { + return count($o->robots); + }, $manufacturers)), + Robot::count(array ('id < 25 AND manufacturer_id < 50')) + ); + + $this->assertEquals( + array_sum(array_map(function ($o) { + $c = 0; + + foreach ($o->robots as $r) { + $c += count($r->bugs); + } + + return $c; + }, $manufacturers)), + 2 + ); + + $manufacturers = Manufacturer::with( + array ( + 'Robots.Bugs' => function ($builder) { + $builder->where('id > 10000'); + } + ), + array ( + 'limit' => 5, + 'order' => 'id ASC' + ) + ); + + $this->assertEquals( + array_sum(array_map(function ($o) { + return count($o->robots); + }, $manufacturers)), + Robot::count(array ('manufacturer_id < 6')) + ); + + $robots = array (); + + foreach ($manufacturers as $m) { + $robots = array_merge($robots, $m->robots); + } + + $this->assertEquals( + array_sum(array_map(function ($o) { + return count($o->bugs); + }, $robots)), + 0 + ); + } + + protected function resultSetToEagerLoadingEquivalent($val) + { + $ret = $val; + + if ($val instanceof SimpleResultset) { + $ret = array (); + + if ($val->count() > 0) { + foreach ($val as $model) { + $ret[] = $model; + } + } + } + + return $ret; + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel-PHP53.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel-PHP53.php new file mode 100644 index 000000000..5728da633 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/AbstractModel-PHP53.php @@ -0,0 +1,5 @@ += 0 ? '54' : '53') . '.php'; + + require_once $file; +}); diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Bug.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Bug.php new file mode 100644 index 000000000..4625a1c52 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Bug.php @@ -0,0 +1,20 @@ +belongsTo('robot_id', 'EagerLoadingTestModel\Robot', 'id', array ( + 'alias' => 'Robot', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + )); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Manufacturer.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Manufacturer.php new file mode 100644 index 000000000..c56b92b58 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Manufacturer.php @@ -0,0 +1,19 @@ +hasMany('id', 'EagerLoadingTestModel\Robot', 'manufacturer_id', array ( + 'alias' => 'Robots', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + )); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/NotSupportedRelation.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/NotSupportedRelation.php new file mode 100644 index 000000000..594a3f10d --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/NotSupportedRelation.php @@ -0,0 +1,23 @@ +belongsTo(array ('id','robot_id'), 'EagerLoadingTestModel\Robots', array ('id','robot_id'), array ( + 'alias' => 'Robot', + 'foreignKey' => true + )); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Part.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Part.php new file mode 100644 index 000000000..7b6acb9c6 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Part.php @@ -0,0 +1,27 @@ +hasManyToMany( + 'id', + 'EagerLoadingTestModel\RobotPart', + 'part_id', + 'robot_id', + 'EagerLoadingTestModel\Robot', + 'id', + array ( + 'alias' => 'Robots', + 'foreignKey' => array ( + 'action' => Relation::ACTION_RESTRICT + ) + ) + ); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Purpose.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Purpose.php new file mode 100644 index 000000000..14d7d321c --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Purpose.php @@ -0,0 +1,20 @@ +belongsTo('robot_id', 'EagerLoadingTestModel\Robot', 'id', array ( + 'alias' => 'Robot', + 'foreignKey' => array ( + 'action' => Relation::ACTION_RESTRICT + ) + )); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Robot.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Robot.php new file mode 100644 index 000000000..1f67677f5 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/Robot.php @@ -0,0 +1,74 @@ +belongsTo('manufacturer_id', 'EagerLoadingTestModel\Manufacturer', 'id', array ( + 'alias' => 'Manufacturer', + 'foreignKey' => true + )); + + // Recursive relation + $this->belongsTo('parent_id', 'EagerLoadingTestModel\Robot', 'id', array ( + 'alias' => 'Parent', + 'foreignKey' => true + )); + + $this->hasMany('id', 'EagerLoadingTestModel\Robot', 'parent_id', array ( + 'alias' => 'Children', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + )); + + $this->hasOne('id', 'EagerLoadingTestModel\Purpose', 'robot_id', array ( + 'alias' => 'Purpose', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + )); + + $this->hasMany('id', 'EagerLoadingTestModel\Bug', 'robot_id', array ( + 'alias' => 'Bugs', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + )); + + $this->hasManyToMany( + 'id', + 'EagerLoadingTestModel\RobotPart', + 'robot_id', + 'part_id', + 'EagerLoadingTestModel\Part', + 'id', + array ( + 'alias' => 'Parts', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + ) + ); + + // Wrong relation + $this->hasMany( + array ('id', 'parent_id'), + 'EagerLoadingTestModel\NotSupportedRelation', + array ('id', 'robot_id'), + array ( + 'alias' => 'NotSupportedRelations', + 'foreignKey' => array ( + 'action' => Relation::ACTION_CASCADE + ) + ) + ); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/RobotPart.php b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/RobotPart.php new file mode 100644 index 000000000..dc9facbbc --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/Models/RobotPart.php @@ -0,0 +1,20 @@ +belongsTo('robot_id', 'EagerLoadingTestModel\Robot', 'id', array ( + 'alias' => 'Robot', + 'foreignKey' => true + )); + + $this->belongsTo('part_id', 'EagerLoadingTestModel\Part', 'id', array ( + 'alias' => 'Part', + 'foreignKey' => true + )); + } +} diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/resources/schema.sql b/tests/Phalcon/Mvc/Model/EagerLoading/resources/schema.sql new file mode 100644 index 000000000..7ee704ad6 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/EagerLoading/resources/schema.sql @@ -0,0 +1,55 @@ +CREATE TABLE IF NOT EXISTS `bug` ( + `id` serial, + `name` varchar(100) NOT NULL, + `robot_id` bigint unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY (`robot_id`) +); + +CREATE TABLE IF NOT EXISTS `manufacturer` ( + `id` serial, + `name` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE IF NOT EXISTS `part` ( + `id` serial, + `name` varchar(1020) NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE IF NOT EXISTS `purpose` ( + `id` serial, + `name` varchar(100) NOT NULL, + `robot_id` bigint unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY (`robot_id`) +); + +CREATE TABLE IF NOT EXISTS `robot` ( + `id` serial, + `name` varchar(100) NOT NULL, + `parent_id` bigint unsigned DEFAULT NULL, + `manufacturer_id` bigint unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY (`parent_id`), + KEY (`manufacturer_id`) +); + +CREATE TABLE IF NOT EXISTS `robot_part` ( + `robot_id` bigint unsigned NOT NULL, + `part_id` bigint unsigned NOT NULL, + PRIMARY KEY (`robot_id`, `part_id`) +); + +INSERT INTO `bug` (`id`,`name`,`robot_id`) VALUES (1,'a-0',10),(2,'b-0',80),(3,'c-0',18),(4,'d-0',127),(5,'e-0',53),(6,'f-0',94),(7,'g-0',134),(8,'h-0',156),(9,'i-0',127),(10,'j-0',45),(11,'k-0',41),(12,'l-0',51),(13,'m-0',68),(14,'n-0',4),(15,'o-0',144),(16,'p-0',23),(17,'q-0',34),(18,'r-0',146),(19,'s-0',55),(20,'t-0',39),(21,'u-0',66),(22,'v-0',33),(23,'w-0',44),(24,'x-0',74),(25,'y-0',89),(26,'z-0',46),(27,'a-1',67),(28,'b-1',150),(29,'c-1',29),(30,'d-1',50),(31,'e-1',133),(32,'f-1',151),(33,'g-1',122),(34,'h-1',158),(35,'i-1',158),(36,'j-1',167),(37,'k-1',194),(38,'l-1',169),(39,'m-1',81),(40,'n-1',18),(41,'o-1',143),(42,'p-1',5),(43,'q-1',110),(44,'r-1',48),(45,'s-1',13),(46,'t-1',79),(47,'u-1',34),(48,'v-1',47),(49,'w-1',23),(50,'x-1',175),(51,'y-1',74),(52,'z-1',27),(53,'a-2',114),(54,'b-2',139),(55,'c-2',200),(56,'d-2',109),(57,'e-2',3),(58,'f-2',19),(59,'g-2',29),(60,'h-2',116),(61,'i-2',22),(62,'j-2',29),(63,'k-2',63),(64,'l-2',70),(65,'m-2',51),(66,'n-2',103),(67,'o-2',12),(68,'p-2',60),(69,'q-2',104),(70,'r-2',54),(71,'s-2',149),(72,'t-2',2),(73,'u-2',96),(74,'v-2',53),(75,'w-2',93),(76,'x-2',158),(77,'y-2',175),(78,'z-2',167),(79,'a-3',39),(80,'b-3',33),(81,'c-3',64),(82,'d-3',167),(83,'e-3',14),(84,'f-3',73),(85,'g-3',190),(86,'h-3',45),(87,'i-3',131),(88,'j-3',89),(89,'k-3',143),(90,'l-3',198),(91,'m-3',158),(92,'n-3',191),(93,'o-3',21),(94,'p-3',197),(95,'q-3',84),(96,'r-3',77),(97,'s-3',42),(98,'t-3',102),(99,'u-3',34),(100,'v-3',134),(101,'w-3',46),(102,'x-3',39),(103,'y-3',129),(104,'z-3',174),(105,'a-4',136),(106,'b-4',79),(107,'c-4',190),(108,'d-4',177),(109,'e-4',29),(110,'f-4',199),(111,'g-4',91),(112,'h-4',108),(113,'i-4',76),(114,'j-4',129),(115,'k-4',28),(116,'l-4',195),(117,'m-4',57),(118,'n-4',59),(119,'o-4',25),(120,'p-4',85),(121,'q-4',99),(122,'r-4',31),(123,'s-4',75),(124,'t-4',43),(125,'u-4',188),(126,'v-4',77),(127,'w-4',10),(128,'x-4',155),(129,'y-4',198),(130,'z-4',41),(131,'a-5',72),(132,'b-5',174),(133,'c-5',144),(134,'d-5',52),(135,'e-5',80),(136,'f-5',162),(137,'g-5',20),(138,'h-5',47),(139,'i-5',2),(140,'j-5',22),(141,'k-5',57),(142,'l-5',9),(143,'m-5',107),(144,'n-5',99); + +INSERT INTO `manufacturer` (`id`,`name`) VALUES (1,'a-0'),(2,'b-0'),(3,'c-0'),(4,'d-0'),(5,'e-0'),(6,'f-0'),(7,'g-0'),(8,'h-0'),(9,'i-0'),(10,'j-0'),(11,'k-0'),(12,'l-0'),(13,'m-0'),(14,'n-0'),(15,'o-0'),(16,'p-0'),(17,'q-0'),(18,'r-0'),(19,'s-0'),(20,'t-0'),(21,'u-0'),(22,'v-0'),(23,'w-0'),(24,'x-0'),(25,'y-0'),(26,'z-0'),(27,'a-1'),(28,'b-1'),(29,'c-1'),(30,'d-1'),(31,'e-1'),(32,'f-1'),(33,'g-1'),(34,'h-1'),(35,'i-1'),(36,'j-1'),(37,'k-1'),(38,'l-1'),(39,'m-1'),(40,'n-1'),(41,'o-1'),(42,'p-1'),(43,'q-1'),(44,'r-1'),(45,'s-1'),(46,'t-1'),(47,'u-1'),(48,'v-1'),(49,'w-1'),(50,'x-1'),(51,'y-1'),(52,'z-1'),(53,'a-2'),(54,'b-2'),(55,'c-2'),(56,'d-2'),(57,'e-2'),(58,'f-2'),(59,'g-2'),(60,'h-2'),(61,'i-2'),(62,'j-2'),(63,'k-2'),(64,'l-2'),(65,'m-2'),(66,'n-2'),(67,'o-2'),(68,'p-2'),(69,'q-2'),(70,'r-2'),(71,'s-2'),(72,'t-2'),(73,'u-2'),(74,'v-2'),(75,'w-2'),(76,'x-2'),(77,'y-2'),(78,'z-2'),(79,'a-3'),(80,'b-3'),(81,'c-3'),(82,'d-3'),(83,'e-3'),(84,'f-3'),(85,'g-3'),(86,'h-3'),(87,'i-3'),(88,'j-3'),(89,'k-3'),(90,'l-3'),(91,'m-3'),(92,'n-3'),(93,'o-3'),(94,'p-3'),(95,'q-3'),(96,'r-3'),(97,'s-3'),(98,'t-3'),(99,'u-3'),(100,'v-3'); + +INSERT INTO `part` (`id`,`name`) VALUES (1,'a-0'),(2,'b-0'),(3,'c-0'),(4,'d-0'),(5,'e-0'),(6,'f-0'),(7,'g-0'),(8,'h-0'),(9,'i-0'),(10,'j-0'),(11,'k-0'),(12,'l-0'),(13,'m-0'),(14,'n-0'),(15,'o-0'),(16,'p-0'),(17,'q-0'),(18,'r-0'),(19,'s-0'),(20,'t-0'),(21,'u-0'),(22,'v-0'),(23,'w-0'),(24,'x-0'),(25,'y-0'),(26,'z-0'),(27,'a-1'),(28,'b-1'),(29,'c-1'),(30,'d-1'),(31,'e-1'),(32,'f-1'),(33,'g-1'),(34,'h-1'),(35,'i-1'),(36,'j-1'),(37,'k-1'),(38,'l-1'),(39,'m-1'),(40,'n-1'),(41,'o-1'),(42,'p-1'),(43,'q-1'),(44,'r-1'),(45,'s-1'),(46,'t-1'),(47,'u-1'),(48,'v-1'),(49,'w-1'),(50,'x-1'),(51,'y-1'),(52,'z-1'),(53,'a-2'),(54,'b-2'),(55,'c-2'),(56,'d-2'),(57,'e-2'),(58,'f-2'),(59,'g-2'),(60,'h-2'),(61,'i-2'),(62,'j-2'),(63,'k-2'),(64,'l-2'),(65,'m-2'),(66,'n-2'),(67,'o-2'),(68,'p-2'),(69,'q-2'),(70,'r-2'),(71,'s-2'),(72,'t-2'),(73,'u-2'),(74,'v-2'),(75,'w-2'),(76,'x-2'),(77,'y-2'),(78,'z-2'),(79,'a-3'),(80,'b-3'),(81,'c-3'),(82,'d-3'),(83,'e-3'),(84,'f-3'),(85,'g-3'),(86,'h-3'),(87,'i-3'),(88,'j-3'),(89,'k-3'),(90,'l-3'),(91,'m-3'),(92,'n-3'),(93,'o-3'),(94,'p-3'),(95,'q-3'),(96,'r-3'),(97,'s-3'),(98,'t-3'),(99,'u-3'),(100,'v-3'); + +INSERT INTO `purpose` (`id`,`name`,`robot_id`) VALUES (1,'a-0',1),(2,'b-0',2),(3,'c-0',3),(4,'d-0',4),(5,'e-0',5),(6,'f-0',6),(7,'g-0',7),(8,'h-0',8),(9,'i-0',9),(10,'j-0',10),(11,'k-0',11),(12,'l-0',12),(13,'m-0',13),(14,'n-0',14),(15,'o-0',15),(16,'p-0',16),(17,'q-0',17),(18,'r-0',18),(19,'s-0',19),(20,'t-0',20),(21,'u-0',21),(22,'v-0',22),(23,'w-0',23),(24,'x-0',24),(25,'y-0',25),(26,'z-0',26),(27,'a-1',27),(28,'b-1',28),(29,'c-1',29),(30,'d-1',30),(31,'e-1',31),(32,'f-1',32),(33,'g-1',33),(34,'h-1',34),(35,'i-1',35),(36,'j-1',36),(37,'k-1',37),(38,'l-1',38),(39,'m-1',39),(40,'n-1',40),(41,'o-1',41),(42,'p-1',42),(43,'q-1',43),(44,'r-1',44),(45,'s-1',45),(46,'t-1',46),(47,'u-1',47),(48,'v-1',48),(49,'w-1',49),(50,'x-1',50),(51,'y-1',51),(52,'z-1',52),(53,'a-2',53),(54,'b-2',54),(55,'c-2',55),(56,'d-2',56),(57,'e-2',57),(58,'f-2',58),(59,'g-2',59),(60,'h-2',60),(61,'i-2',61),(62,'j-2',62),(63,'k-2',63),(64,'l-2',64),(65,'m-2',65),(66,'n-2',66),(67,'o-2',67),(68,'p-2',68),(69,'q-2',69),(70,'r-2',70),(71,'s-2',71),(72,'t-2',72),(73,'u-2',73),(74,'v-2',74),(75,'w-2',75),(76,'x-2',76),(77,'y-2',77),(78,'z-2',78),(79,'a-3',79),(80,'b-3',80),(81,'c-3',81),(82,'d-3',82),(83,'e-3',83),(84,'f-3',84),(85,'g-3',85),(86,'h-3',86),(87,'i-3',87),(88,'j-3',88),(89,'k-3',89),(90,'l-3',90),(91,'m-3',91),(92,'n-3',92),(93,'o-3',93),(94,'p-3',94),(95,'q-3',95),(96,'r-3',96),(97,'s-3',97),(98,'t-3',98),(99,'u-3',99),(100,'v-3',100),(101,'w-3',101),(102,'x-3',102),(103,'y-3',103),(104,'z-3',104),(105,'a-4',105),(106,'b-4',106),(107,'c-4',107),(108,'d-4',108),(109,'e-4',109),(110,'f-4',110),(111,'g-4',111),(112,'h-4',112),(113,'i-4',113),(114,'j-4',114),(115,'k-4',115),(116,'l-4',116),(117,'m-4',117),(118,'n-4',118),(119,'o-4',119),(120,'p-4',120),(121,'q-4',121),(122,'r-4',122),(123,'s-4',123),(124,'t-4',124),(125,'u-4',125),(126,'v-4',126),(127,'w-4',127),(128,'x-4',128),(129,'y-4',129),(130,'z-4',130),(131,'a-5',131),(132,'b-5',132),(133,'c-5',133),(134,'d-5',134),(135,'e-5',135),(136,'f-5',136),(137,'g-5',137),(138,'h-5',138),(139,'i-5',139),(140,'j-5',140),(141,'k-5',141),(142,'l-5',142),(143,'m-5',143),(144,'n-5',144),(145,'o-5',145),(146,'p-5',146),(147,'q-5',147),(148,'r-5',148),(149,'s-5',149),(150,'t-5',150),(151,'u-5',151),(152,'v-5',152),(153,'w-5',153),(154,'x-5',154),(155,'y-5',155),(156,'z-5',156),(157,'a-6',157),(158,'b-6',158),(159,'c-6',159),(160,'d-6',160),(161,'e-6',161),(162,'f-6',162),(163,'g-6',163),(164,'h-6',164),(165,'i-6',165),(166,'j-6',166),(167,'k-6',167),(168,'l-6',168),(169,'m-6',169),(170,'n-6',170),(171,'o-6',171),(172,'p-6',172),(173,'q-6',173),(174,'r-6',174),(175,'s-6',175),(176,'t-6',176),(177,'u-6',177),(178,'v-6',178),(179,'w-6',179),(180,'x-6',180),(181,'y-6',181),(182,'z-6',182),(183,'a-7',183),(184,'b-7',184),(185,'c-7',185),(186,'d-7',186),(187,'e-7',187),(188,'f-7',188),(189,'g-7',189),(190,'h-7',190),(191,'i-7',191),(192,'j-7',192),(193,'k-7',193),(194,'l-7',194),(195,'m-7',195),(196,'n-7',196),(197,'o-7',197),(198,'p-7',198),(199,'q-7',199),(200,'r-7',200); + +INSERT INTO `robot` (`id`,`name`,`parent_id`,`manufacturer_id`) VALUES (1,'a-0',NULL,85),(2,'b-0',1,92),(3,'c-0',1,64),(4,'d-0',NULL,16),(5,'e-0',NULL,43),(6,'f-0',NULL,2),(7,'g-0',6,48),(8,'h-0',NULL,83),(9,'i-0',8,81),(10,'j-0',1,75),(11,'k-0',3,61),(12,'l-0',NULL,61),(13,'m-0',6,78),(14,'n-0',4,100),(15,'o-0',4,71),(16,'p-0',NULL,47),(17,'q-0',7,96),(18,'r-0',9,87),(19,'s-0',16,17),(20,'t-0',NULL,99),(21,'u-0',9,21),(22,'v-0',10,10),(23,'w-0',15,77),(24,'x-0',1,87),(25,'y-0',10,90),(26,'z-0',NULL,100),(27,'a-1',13,79),(28,'b-1',NULL,70),(29,'c-1',6,4),(30,'d-1',4,32),(31,'e-1',16,30),(32,'f-1',NULL,58),(33,'g-1',27,75),(34,'h-1',19,94),(35,'i-1',NULL,94),(36,'j-1',NULL,1),(37,'k-1',NULL,96),(38,'l-1',NULL,8),(39,'m-1',2,56),(40,'n-1',34,4),(41,'o-1',32,25),(42,'p-1',21,94),(43,'q-1',NULL,94),(44,'r-1',NULL,69),(45,'s-1',NULL,30),(46,'t-1',16,47),(47,'u-1',1,72),(48,'v-1',NULL,79),(49,'w-1',NULL,84),(50,'x-1',NULL,30),(51,'y-1',NULL,37),(52,'z-1',7,69),(53,'a-2',50,9),(54,'b-2',NULL,44),(55,'c-2',NULL,71),(56,'d-2',NULL,6),(57,'e-2',NULL,50),(58,'f-2',55,97),(59,'g-2',NULL,66),(60,'h-2',5,2),(61,'i-2',35,2),(62,'j-2',18,4),(63,'k-2',8,13),(64,'l-2',36,65),(65,'m-2',NULL,10),(66,'n-2',58,99),(67,'o-2',NULL,23),(68,'p-2',5,94),(69,'q-2',NULL,68),(70,'r-2',16,54),(71,'s-2',15,71),(72,'t-2',16,5),(73,'u-2',NULL,14),(74,'v-2',NULL,29),(75,'w-2',53,69),(76,'x-2',36,84),(77,'y-2',NULL,13),(78,'z-2',36,33),(79,'a-3',9,91),(80,'b-3',42,62),(81,'c-3',NULL,68),(82,'d-3',NULL,47),(83,'e-3',27,68),(84,'f-3',27,22),(85,'g-3',68,51),(86,'h-3',NULL,64),(87,'i-3',3,23),(88,'j-3',71,28),(89,'k-3',74,2),(90,'l-3',NULL,17),(91,'m-3',63,5),(92,'n-3',NULL,48),(93,'o-3',2,84),(94,'p-3',92,24),(95,'q-3',88,5),(96,'r-3',NULL,98),(97,'s-3',NULL,44),(98,'t-3',8,71),(99,'u-3',NULL,25),(100,'v-3',66,90),(101,'w-3',NULL,2),(102,'x-3',67,57),(103,'y-3',71,7),(104,'z-3',NULL,98),(105,'a-4',NULL,60),(106,'b-4',74,82),(107,'c-4',86,33),(108,'d-4',65,31),(109,'e-4',NULL,15),(110,'f-4',36,81),(111,'g-4',NULL,90),(112,'h-4',NULL,3),(113,'i-4',29,2),(114,'j-4',71,62),(115,'k-4',97,41),(116,'l-4',97,20),(117,'m-4',114,89),(118,'n-4',95,57),(119,'o-4',NULL,29),(120,'p-4',NULL,70),(121,'q-4',63,46),(122,'r-4',48,48),(123,'s-4',NULL,1),(124,'t-4',26,57),(125,'u-4',NULL,65),(126,'v-4',NULL,64),(127,'w-4',11,48),(128,'x-4',31,26),(129,'y-4',NULL,26),(130,'z-4',NULL,21),(131,'a-5',NULL,41),(132,'b-5',82,40),(133,'c-5',NULL,77),(134,'d-5',NULL,87),(135,'e-5',55,58),(136,'f-5',NULL,3),(137,'g-5',NULL,73),(138,'h-5',123,91),(139,'i-5',136,18),(140,'j-5',47,86),(141,'k-5',50,25),(142,'l-5',113,11),(143,'m-5',NULL,39),(144,'n-5',102,31),(145,'o-5',73,82),(146,'p-5',NULL,47),(147,'q-5',97,12),(148,'r-5',25,20),(149,'s-5',103,23),(150,'t-5',NULL,99),(151,'u-5',32,9),(152,'v-5',NULL,94),(153,'w-5',124,78),(154,'x-5',96,37),(155,'y-5',NULL,57),(156,'z-5',64,2),(157,'a-6',NULL,89),(158,'b-6',NULL,56),(159,'c-6',NULL,38),(160,'d-6',48,80),(161,'e-6',13,31),(162,'f-6',46,81),(163,'g-6',NULL,9),(164,'h-6',40,91),(165,'i-6',124,57),(166,'j-6',NULL,94),(167,'k-6',NULL,38),(168,'l-6',57,74),(169,'m-6',112,98),(170,'n-6',88,1),(171,'o-6',NULL,62),(172,'p-6',NULL,80),(173,'q-6',50,25),(174,'r-6',NULL,29),(175,'s-6',146,37),(176,'t-6',NULL,8),(177,'u-6',65,33),(178,'v-6',NULL,56),(179,'w-6',44,56),(180,'x-6',130,100),(181,'y-6',NULL,13),(182,'z-6',NULL,30),(183,'a-7',NULL,9),(184,'b-7',82,64),(185,'c-7',NULL,93),(186,'d-7',NULL,25),(187,'e-7',103,90),(188,'f-7',147,70),(189,'g-7',182,6),(190,'h-7',88,61),(191,'i-7',NULL,87),(192,'j-7',NULL,21),(193,'k-7',23,28),(194,'l-7',NULL,6),(195,'m-7',NULL,59),(196,'n-7',97,84),(197,'o-7',NULL,31),(198,'p-7',61,21),(199,'q-7',35,87),(200,'r-7',NULL,39); + +INSERT INTO `robot_part` (`robot_id`,`part_id`) VALUES (1,14),(1,38),(1,83),(2,57),(2,73),(2,76),(3,41),(3,52),(4,54),(4,55),(4,73),(5,98),(6,47),(7,54),(7,66),(8,34),(8,62),(8,72),(9,18),(9,86),(9,89),(10,66),(10,82),(10,86),(11,31),(11,74),(12,3),(12,86),(12,95),(13,58),(14,16),(14,31),(14,64),(15,76),(16,1),(16,59),(16,73),(17,13),(17,87),(18,28),(19,47),(19,61),(20,16),(20,69),(21,26),(21,74),(21,90),(22,20),(22,27),(23,27),(23,85),(24,50),(25,15),(25,49),(25,77),(26,38),(26,52),(26,75),(27,10),(27,100),(28,64),(28,93),(29,7),(29,53),(30,76),(31,49),(32,15),(33,83),(34,77),(35,30),(35,99),(36,93),(37,68),(37,94),(38,29),(39,72),(39,85),(40,14),(40,54),(40,93),(41,79),(42,91),(42,95),(42,98),(43,57),(43,73),(43,93),(44,33),(45,48),(46,52),(46,58),(47,21),(47,38),(48,16),(48,60),(49,79),(49,95),(50,25),(50,47),(50,54),(51,3),(52,37),(52,87),(53,37),(53,69),(54,28),(54,44),(54,82),(55,25),(55,61),(56,78),(57,63),(58,8),(58,96),(58,100),(59,46),(59,52),(60,25),(61,34),(61,53),(61,82),(62,88),(63,6),(63,83),(63,92),(64,32),(64,54),(65,43),(65,91),(66,16),(66,29),(66,90),(67,36),(67,78),(67,97),(68,22),(69,78),(70,24),(71,26),(72,75),(73,25),(73,61),(74,67),(75,31),(75,49),(75,66),(76,22),(76,24),(76,43),(77,22),(77,69),(77,94),(78,50),(79,45),(80,73),(80,76),(81,4),(82,79),(83,65),(83,69),(83,80),(84,6),(84,69),(85,4),(85,64),(85,96),(86,3),(87,15),(87,53),(88,33),(88,60),(89,37),(89,40),(89,54),(90,19),(90,62),(91,64),(91,88),(92,28),(92,52),(92,93),(93,43),(93,80),(94,74),(95,38),(95,61),(95,70),(96,41),(96,69),(97,4),(97,100),(98,29),(98,39),(99,5),(99,28),(99,93),(100,66),(101,45),(101,91),(102,2),(102,61),(102,95),(103,53),(104,64),(105,8),(105,43),(105,99),(106,18),(106,87),(106,90),(107,49),(107,55),(107,75),(108,8),(108,48),(108,73),(109,13),(109,89),(109,91),(110,16),(110,78),(111,63),(111,89),(112,75),(112,83),(113,60),(113,95),(114,11),(115,15),(115,36),(115,70),(116,6),(116,89),(117,17),(117,46),(118,15),(118,28),(119,17),(119,92),(120,67),(121,11),(121,90),(122,7),(122,94),(122,100),(123,79),(124,6),(124,26),(124,65),(125,91),(126,59),(127,83),(127,96),(127,97),(128,2),(129,69),(129,84),(130,7),(130,52),(131,12),(131,16),(132,1),(132,14),(133,1),(133,44),(133,75),(134,14),(134,17),(134,42),(135,9),(135,11),(135,16),(136,25),(136,40),(137,22),(137,86),(138,32),(139,35),(140,14),(140,50),(141,67),(141,78),(141,86),(142,49),(143,35),(143,44),(143,91),(144,13),(144,50),(145,25),(145,56),(145,64),(146,21),(146,46),(146,77),(147,14),(147,50),(148,46),(148,87),(149,90),(150,85),(151,8),(151,54),(152,69),(153,54),(153,56),(153,99),(154,38),(154,84),(155,44),(155,85),(156,18),(157,84),(158,22),(159,45),(160,33),(160,48),(161,36),(161,41),(161,80),(162,41),(162,85),(163,4),(163,32),(164,10),(164,39),(164,92),(165,55),(165,67),(166,58),(167,14),(167,24),(167,53),(168,10),(168,58),(168,71),(169,53),(169,68),(170,93),(171,18),(171,23),(171,74),(172,14),(172,32),(173,16),(174,28),(174,55),(175,61),(176,50),(177,19),(178,23),(178,56),(178,92),(179,21),(179,65),(180,42),(180,63),(181,93),(182,38),(182,69),(183,7),(184,1),(184,44),(184,89),(185,31),(185,100),(186,51),(187,32),(187,48),(187,70),(188,50),(188,57),(189,46),(190,80),(190,81),(191,45),(192,25),(193,26),(193,32),(194,33),(195,12),(195,84),(196,60),(197,73),(198,53),(199,14),(199,78),(200,18),(200,20),(200,83); \ No newline at end of file From df79f6c08492ff46a4d91af5bf8b5a9816fc7f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 10:51:48 +0200 Subject: [PATCH 08/17] CS fixes and method declaration error --- .../Phalcon/Mvc/Model/EagerLoading/EagerLoad.php | 4 ++-- Library/Phalcon/Mvc/Model/EagerLoading/Loader.php | 14 +++++++------- Library/Phalcon/Mvc/Model/EagerLoadingTrait.php | 4 ++-- .../Mvc/Model/EagerLoading/EagerLoadingTest.php | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php index 543ac0d56..5741897ab 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php @@ -167,7 +167,7 @@ public function load() $records = array_values($records); } else { // We expect a single object or a set of it - $isSingle = ! $isThrough && ( + $isSingle = !$isThrough && ( $relation->getType() === Relation::HAS_ONE || $relation->getType() === Relation::BELONGS_TO ); @@ -223,7 +223,7 @@ public function load() } else { $record->{$alias} = null; - if (! $isSingle) { + if (!$isSingle) { $record->{$alias} = array (); } } diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php index 4404b168d..66e72e846 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php @@ -27,9 +27,9 @@ public function __construct($from) $error = false; $arguments = array_slice(func_get_args(), 1); - if (! $from instanceof ModelInterface) { - if (! $from instanceof Simple) { - if (! is_array($from)) { + if (!$from instanceof ModelInterface) { + if (!$from instanceof Simple) { + if (!is_array($from)) { $error = true; } else { $from = array_filter($from); @@ -225,14 +225,14 @@ private static function parseArguments(array $arguments) */ public function addEagerLoad($relationAlias, $constraints = null) { - if (! is_string($relationAlias)) { + if (!is_string($relationAlias)) { throw new \InvalidArgumentException(sprintf( '$relationAlias expects to be a string, `%s` given', gettype($relationAlias) )); } - if ($constraints !== null && ! is_callable($constraints)) { + if ($constraints !== null && !is_callable($constraints)) { throw new \InvalidArgumentException(sprintf( '$constraints expects to be a callable, `%s` given', gettype($constraints) @@ -281,11 +281,11 @@ private function buildTree() } } - if (! isset ($resolvedRelations[$name])) { + if (!isset ($resolvedRelations[$name])) { $mM->load($parentClassName); $relation = $mM->getRelationByAlias($parentClassName, $alias); - if (! $relation instanceof Relation) { + if (!$relation instanceof Relation) { throw new \RuntimeException(sprintf( 'There is no defined relation for the model `%s` using alias `%s`', $parentClassName, diff --git a/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php index 9adb31f08..6725082aa 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php +++ b/Library/Phalcon/Mvc/Model/EagerLoadingTrait.php @@ -31,7 +31,7 @@ public static function with() { $arguments = func_get_args(); - if (! empty ($arguments)) { + if (!empty ($arguments)) { $numArgs = count($arguments); $lastArg = $numArgs - 1; $parameters = null; @@ -70,7 +70,7 @@ public static function findFirstWith() { $arguments = func_get_args(); - if (! empty ($arguments)) { + if (!empty ($arguments)) { $numArgs = count($arguments); $lastArg = $numArgs - 1; $parameters = null; diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php index 1d84ee95f..749699ad2 100644 --- a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php +++ b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php @@ -19,7 +19,7 @@ class EagerLoadingTest extends \PHPUnit_Framework_TestCase { protected $previousDependencyInjector; - public function setUpBeforeClass() + public static function setUpBeforeClass() { $this->previousDependencyInjector = DI::getDefault(); @@ -51,7 +51,7 @@ public function setUpBeforeClass() spl_autoload_register(array (__CLASS__, 'autoloadModels'), true, true); } - public function tearDownAfterClass() + public static function tearDownAfterClass() { if ($this->previousDependencyInjector instanceof DI) { DI::setDefault($this->previousDependencyInjector); From 1a0475ed7c7e63fcb9b334a109e2d6bf5421e83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 10:54:51 +0200 Subject: [PATCH 09/17] facepalm --- .../Mvc/Model/EagerLoading/EagerLoadingTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php index 749699ad2..18e60e593 100644 --- a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php +++ b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php @@ -17,11 +17,11 @@ class EagerLoadingTest extends \PHPUnit_Framework_TestCase { - protected $previousDependencyInjector; + protected static $previousDependencyInjector; public static function setUpBeforeClass() { - $this->previousDependencyInjector = DI::getDefault(); + self::$previousDependencyInjector = DI::getDefault(); $di = new DI; @@ -44,7 +44,7 @@ public static function setUpBeforeClass() )); }, true); - if ($this->previousDependencyInjector instanceof DI) { + if (self::$previousDependencyInjector instanceof DI) { DI::setDefault($di); } @@ -53,8 +53,8 @@ public static function setUpBeforeClass() public static function tearDownAfterClass() { - if ($this->previousDependencyInjector instanceof DI) { - DI::setDefault($this->previousDependencyInjector); + if (self::$previousDependencyInjector instanceof DI) { + DI::setDefault(self::$previousDependencyInjector); } spl_autoload_unregister(array (__CLASS__, 'autoloadModels')); From 22c0451b0d0cda3c8ba786579c208a5ca0b79a60 Mon Sep 17 00:00:00 2001 From: Nemanja Ognjanovic Date: Wed, 3 Jun 2015 19:51:36 +0200 Subject: [PATCH 10/17] Phalcon\Mvc\Model\MetaData\Base now implments MetaDataInterface to comply with method signatures like \Phalcon\Mvc\Model::_doLowInsert --- Library/Phalcon/Mvc/Model/MetaData/Base.php | 2 +- tests/Phalcon/Mvc/Model/MetaData/BaseTest.php | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/Phalcon/Mvc/Model/MetaData/BaseTest.php diff --git a/Library/Phalcon/Mvc/Model/MetaData/Base.php b/Library/Phalcon/Mvc/Model/MetaData/Base.php index 23408a10a..2a70c6a3c 100644 --- a/Library/Phalcon/Mvc/Model/MetaData/Base.php +++ b/Library/Phalcon/Mvc/Model/MetaData/Base.php @@ -29,7 +29,7 @@ * @license New BSD License * @link http://phalconphp.com/ */ -abstract class Base extends MetaData +abstract class Base extends MetaData implements \Phalcon\Mvc\Model\MetaDataInterface { /** * Default options for cache backend. diff --git a/tests/Phalcon/Mvc/Model/MetaData/BaseTest.php b/tests/Phalcon/Mvc/Model/MetaData/BaseTest.php new file mode 100644 index 000000000..37ff9cab8 --- /dev/null +++ b/tests/Phalcon/Mvc/Model/MetaData/BaseTest.php @@ -0,0 +1,32 @@ +assertInstanceOf('Phalcon\Mvc\Model\MetaDataInterface', $stub); + } + + public function testRedisMetaDataAdapterImplementsMetaDataInterface() + { + $redisMock = $this->getMockBuilder('Phalcon\Mvc\Model\MetaData\Redis') + ->disableOriginalConstructor() + ->getMock(); + $this->assertInstanceOf('Phalcon\Mvc\Model\MetaDataInterface', $redisMock); + } +} + +class BaseMetaDataStub extends \Phalcon\Mvc\Model\MetaData\Base +{ + /** + * Returns cache backend instance. + * + * @return \Phalcon\Cache\BackendInterface + */ + protected function getCacheBackend() + { + return new \Phalcon\Cache\Backend\Memory(); + } +} From 00f98729cc6da5e2d2c49b9335e4a78271f329cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 20:31:42 +0200 Subject: [PATCH 11/17] Fix unexpected behavior with PHPUnit --- tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php index 18e60e593..99111cdeb 100644 --- a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php +++ b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php @@ -15,11 +15,13 @@ use Phalcon\Mvc\Model\Resultset\Simple as SimpleResultset; use Phalcon\Mvc\Model\EagerLoading\Loader; +EagerLoadingTest::setUpBeforeClassAndDataProviders(); + class EagerLoadingTest extends \PHPUnit_Framework_TestCase { protected static $previousDependencyInjector; - public static function setUpBeforeClass() + public static function setUpBeforeClassAndDataProviders() { self::$previousDependencyInjector = DI::getDefault(); From e50094fe3a638900b12fe25ec6f5acb2e7f318ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Wed, 3 Jun 2015 22:50:29 +0200 Subject: [PATCH 12/17] Fix errors with autoloading --- tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php index 99111cdeb..357ebbee1 100644 --- a/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php +++ b/tests/Phalcon/Mvc/Model/EagerLoading/EagerLoadingTest.php @@ -64,7 +64,7 @@ public static function tearDownAfterClass() public static function autoloadModels($class) { - $len = strlen($prefix); + $len = strlen('EagerLoadingTestModel\\'); if (strpos($class, 'EagerLoadingTestModel\\') === 0) { $class = substr($class, strlen('EagerLoadingTestModel\\')); @@ -235,7 +235,7 @@ public function testModelMethods() public function testShouldThrowBadMethodCallExceptionIfArgumentsWereNotProvided($method) { $this->setExpectedException('BadMethodCallException'); - call_user_func(array ('Robot', $method)); + call_user_func(array ('EagerLoadingTestModel\Robot', $method)); } public function dp1() @@ -250,7 +250,7 @@ public function dp1() public function testShouldThrowLogicExceptionIfTheEntityWillBeIncomplete($method, $args) { $this->setExpectedException('LogicException'); - call_user_func_array(array ('Robot', $method), $args); + call_user_func_array(array ('EagerLoadingTestModel\Robot', $method), $args); } public function dp2() From 3c0374c262a8fb0706d97d89284a00c36dd3d37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Fri, 5 Jun 2015 08:13:37 +0200 Subject: [PATCH 13/17] array syntax fix --- Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php index 5741897ab..f52c5ae9c 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/EagerLoad.php @@ -72,7 +72,7 @@ public function load() $relReferencedModel = ltrim($relReferencedModel, '\\'); } - $bindValues = []; + $bindValues = array (); foreach ($this->parent->getSubject() as $record) { $bindValues[$record->readAttribute($relField)] = true; From 24d8b32ecfb6596b9e6862b3a62d2697e5c53228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Enr=C3=ADquez?= Date: Fri, 5 Jun 2015 09:39:49 +0200 Subject: [PATCH 14/17] CS fix --- Library/Phalcon/Mvc/Model/EagerLoading/Loader.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php index 66e72e846..4d8ca9306 100644 --- a/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php +++ b/Library/Phalcon/Mvc/Model/EagerLoading/Loader.php @@ -6,7 +6,9 @@ final class Loader { - const E_INVALID_SUBJECT = 'Expected value of `subject` is either a ModelInterface object, a Simple object or an array of ModelInterface objects'; + const E_INVALID_SUBJECT = <<<'MSG' +Expected value of `subject` is either a ModelInterface object, a Simple object or an array of ModelInterface objects +MSG; /** @var ModelInterface[] */ protected $subject; From 5be326f3de6f212ded07ffef92cd416a8c1b8f9d Mon Sep 17 00:00:00 2001 From: Erik Wiesenthal Date: Tue, 9 Jun 2015 17:14:47 +0200 Subject: [PATCH 15/17] Update MysqlExtended.php --- Library/Phalcon/Db/Dialect/MysqlExtended.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Phalcon/Db/Dialect/MysqlExtended.php b/Library/Phalcon/Db/Dialect/MysqlExtended.php index 33f24b900..b79275101 100644 --- a/Library/Phalcon/Db/Dialect/MysqlExtended.php +++ b/Library/Phalcon/Db/Dialect/MysqlExtended.php @@ -36,7 +36,7 @@ class MysqlExtended extends \Phalcon\Db\Dialect\Mysql * * @return string */ - public function getSqlExpression($expression, $escapeChar = null) + public function getSqlExpression(array $expression, $escapeChar = NULL) { if ($expression["type"] == 'functionCall') { switch ($expression["name"]) { From f0a7bae54e3b33d3565d565cc71ecb477fd53976 Mon Sep 17 00:00:00 2001 From: Erik Wiesenthal Date: Tue, 9 Jun 2015 17:16:45 +0200 Subject: [PATCH 16/17] Update MysqlExtended.php --- Library/Phalcon/Db/Dialect/MysqlExtended.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Phalcon/Db/Dialect/MysqlExtended.php b/Library/Phalcon/Db/Dialect/MysqlExtended.php index b79275101..43f3cd6bc 100644 --- a/Library/Phalcon/Db/Dialect/MysqlExtended.php +++ b/Library/Phalcon/Db/Dialect/MysqlExtended.php @@ -36,7 +36,7 @@ class MysqlExtended extends \Phalcon\Db\Dialect\Mysql * * @return string */ - public function getSqlExpression(array $expression, $escapeChar = NULL) + public function getSqlExpression(array $expression, $escapeChar = null) { if ($expression["type"] == 'functionCall') { switch ($expression["name"]) { From 429d6a594fac467a4dfaf0c4adcf48e9f42cb5e0 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Wed, 10 Jun 2015 12:03:07 +0300 Subject: [PATCH 17/17] Added test support for Phalcon 2.0.3 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 027ad191f..9bfd54fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: php env: - PHALCON_VERSION="2.0.x" + - PHALCON_VERSION="phalcon-v2.0.3" - PHALCON_VERSION="phalcon-v2.0.2" - PHALCON_VERSION="phalcon-v2.0.1" - PHALCON_VERSION="phalcon-v2.0.0"