diff --git a/README.md b/README.md index c8c6955..82ce3bc 100644 --- a/README.md +++ b/README.md @@ -1 +1,99 @@ -# phalcon-eagerload \ No newline at end of file + + +### phalcon-eagerload + +> 允许phalcon进行数据预加载的扩展 + + + +## 安装 + +```bash +composer require sowork/phalcon-eagerload +``` + + + +## 使用 + +* 在你的Model中引入 `Sowork\EagerLoad\Traits\EagerLoadingTrait` 文件,并定义相对的关联关系 + + ```php + belongsTo( 'id', App\Author::class,'bookId', [ + 'alias' => 'blogs' + ]); + } + } + ``` + + + +* 使用with()方法对数据进行预加载 + + ```php + // 加载单个关联关系 + $books = App\Book::with('author')->findFirst([ + 'conditions' => 'id = 10', + ]); + + // 加载多个关联关系 + $books = App\Book::with('author', 'publisher')->find(); + + // 加载嵌套关联关系 + $books = App\Book::with('author.contacts')->find(); + + // 加载带条件约束的关联关系 + $users = App\User::with(['posts' => function ($query) { + $query->where('title', 'like', '%first%'); + }])->find(); + ``` + + + +* 使用load()方法对数据进行懒惰渴求式加载 + ```php + // 这在你需要动态决定是否加载关联模型时可能很有用 + $books = App\Book::find(); // or App\Book::findFirst() + + if ($someCondition) { + $books->load('author', 'publisher'); + } + + // 也可以通过条件限制 + + $books->load(['author' => function ($query) { + $query->orderBy('published_date', 'asc'); + }]); + + ``` + + +### 返回结果 + +* 根据列表或者对象,分别会返回 `Phalcon\Mvc\ModelInterface` 或 `Tightenco\Collect\Support\Collection` 对象 + + + +### 集合操作 + +> 当返回结果是一个集合时,更方便我们对数据结果进行处理,具体集合的操作方法请看[相关文档](#https://xueyuanjun.com/post/19507.html) \ No newline at end of file diff --git a/composer.json b/composer.json index c679a19..8c03e46 100644 --- a/composer.json +++ b/composer.json @@ -8,13 +8,12 @@ "email": "soworkxing@gmail.com" } ], - "require": {}, + "require": { + "tightenco/collect": "^6.0" + }, "autoload": { "psr-4": { "Sowork\\EagerLoad\\": "src/" } - }, - "require-dev": { - "phalcon/dd": "^1.1" } } diff --git a/src/EagerLoadServiceProvider.php b/src/EagerLoadServiceProvider.php new file mode 100644 index 0000000..0fd8e7b --- /dev/null +++ b/src/EagerLoadServiceProvider.php @@ -0,0 +1,38 @@ +addCollectionMethods(); + } + + public function addCollectionMethods() + { + Collection::macro('load', function($relations){ + /** + * Load a set of relationships onto the collection. + * + * @param mixed $relations + * @return $this + */ + if ($this->isNotEmpty()) { + if (is_string($relations)) { + $relations = func_get_args(); + } + + return (new QueryBuilder())->with( + array_merge([$this, get_class($this->first())], is_string($relations) ? func_get_args() : $relations) + ); + } + return $this; + }); + } +} \ No newline at end of file diff --git a/src/Model/EagerLoading/EagerLoad.php b/src/Model/EagerLoading/EagerLoad.php index 1f82a16..bf857f1 100644 --- a/src/Model/EagerLoading/EagerLoad.php +++ b/src/Model/EagerLoading/EagerLoad.php @@ -1,10 +1,11 @@ = 0; - } - $this->relation = $relation; $this->constraints = is_callable($constraints) ? $constraints : null; $this->parent = $parent; @@ -45,6 +40,18 @@ public function __construct(Relation $relation, $constraints, $parent, $aliasNam $this->aliasName = $aliasName; } + private function getBindValues($subject, string $relField) + { + if ($subject instanceof Model) { + return [$subject->readAttribute($relField)]; + } + + if ($subject instanceof Collection) { + return $subject->pluck($relField)->all(); + } + return []; + } + /** * @return null|\Phalcon\Mvc\ModelInterface[] */ @@ -53,6 +60,25 @@ public function getSubject() return $this->subject; } + public function getRelationDefinition(RelationInterface $relation): array + { + $alias = $relation->getOptions(); + $definition['alias'] = strtolower($alias['alias']); + $definition['relField'] = $relation->getFields(); + $definition['relReferencedModel'] = $relation->getReferencedModel(); + $definition['relReferencedField'] = $relation->getReferencedFields(); + $definition['relIrModel'] = $relation->getIntermediateModel(); + $definition['relIrField'] = $relation->getIntermediateFields(); + $definition['relIrReferencedField'] = $relation->getIntermediateReferencedFields(); + + // PHQL has problems with this slash + if ($definition['relReferencedModel'][0] === '\\') { + $definition['relReferencedModel'] = ltrim($definition['relReferencedModel'], '\\'); + } + + return array_values($definition); + } + /** * Executes each db query needed * @@ -65,201 +91,170 @@ public function getSubject() */ public function load() { - $parentSubject = $this->parent->getSubject(); + $relation = $this->relation; + $parentSubject = $this->parent->getSubject(); if (empty($parentSubject)) { return $this; } - - $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(); + $builder = new QueryBuilder; + $builder->setCurrentEagerLoad($this, $this->aliasName, $this->loader); + if ($this->constraints) { + call_user_func($this->constraints, $builder); + } - // PHQL has problems with this slash - if ($relReferencedModel[0] === '\\') { - $relReferencedModel = ltrim($relReferencedModel, '\\'); + $records = $this->getEagerLoadData($parentSubject, $builder, $relation); + + $this->subject = $records; + if (isset($this->delayLoads[$this->aliasName])) { + foreach ($this->delayLoads[$this->aliasName] as $delayLoad) { + $delayLoad->load(); + } + $this->delayLoads = []; } - $bindValues = []; + return $this; + } - foreach ($parentSubject as $record) { - $bindValues[$record->readAttribute($relField)] = true; + private function getEagerLoadData($parentSubject, QueryBuilder $builder, RelationInterface $relation) + { + if ($relation->getType() === Relation::HAS_ONE || $relation->getType() === Relation::BELONGS_TO) { + return $this->parseOneToOne($parentSubject, $builder, $relation); } - $bindValues = array_keys($bindValues); + if ($relation->getType() === Relation::HAS_MANY) { + return $this->parseOneToMany($parentSubject, $builder, $relation); + } - $subjectSize = count($parentSubject); - $isManyToManyForMany = false; + if ($relation->isThrough()) { + return $this->parseManyToMany($parentSubject, $builder, $relation); + } - $builder = new QueryBuilder; - $builder->from($relReferencedModel); - - // many-to-many - 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 = []; - - 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); + return []; + } + + /** + * @param Collection|ModelInterface $parentSubject + * @param QueryBuilder $builder + * @param RelationInterface $relation + */ + private function parseOneToOne($parentSubject, QueryBuilder $builder, RelationInterface $relation) + { + list($alias, $relField, $relReferencedModel, $relReferencedField) = $this->getRelationDefinition($relation); + + if ($relation->getType() !== Relation::HAS_ONE && $relation->getType() !== Relation::BELONGS_TO) { + return $parentSubject; } + $bindValues = $this->getBindValues($parentSubject, $relField); + $builder->inWhere("[{$relReferencedField}]", $bindValues); - $builder->setCurrentEagerLoad($this, $this->aliasName, $this->loader); - if ($this->constraints) { - call_user_func($this->constraints, $builder); + $records = Loader::convertResultSetToCollection($builder->from($relReferencedModel)->getQuery()->execute(), $relReferencedModel); + if ($parentSubject instanceof ModelInterface) { + $parentSubject->$alias = $records->first(); + return $records; } - $records = []; + if ($parentSubject instanceof Collection) { + $indexedRecordsCollections = $records->keyBy($relReferencedField); + $parentSubject->map(function($item) use ($relField, $alias, $indexedRecordsCollections) { + // fix phalcon bug https://github.com/phalcon/cphalcon/issues/10556 + $item->$alias = null; + $item->$alias = $indexedRecordsCollections->get($item->$relField); + return $item; + }); + } - if ($isManyToManyForMany) { - foreach ($builder->getQuery()->execute() as $record) { - $records[$record->readAttribute($relReferencedField)] = $record; - } + return $records; + } - foreach ($parentSubject as $record) { - $referencedFieldValue = $record->readAttribute($relField); + /** + * @param Collection|ModelInterface $parentSubject + * @param QueryBuilder $builder + * @param RelationInterface $relation + */ + private function parseOneToMany($parentSubject, QueryBuilder $builder, RelationInterface $relation) + { + list($alias, $relField, $relReferencedModel, $relReferencedField) = $this->getRelationDefinition($relation); - if (isset($modelReferencedModelValues[$referencedFieldValue])) { - $referencedModels = []; + if ($relation->getType() !== Relation::HAS_MANY) { + return $parentSubject; + } + $bindValues = $this->getBindValues($parentSubject, $relField); + $builder->inWhere("[{$relReferencedField}]", $bindValues); - foreach ($modelReferencedModelValues[$referencedFieldValue] as $idx => $_) { - $referencedModels[] = $records[$idx]; - } + $records = Loader::convertResultSetToCollection($builder->from($relReferencedModel)->getQuery()->execute(), $relReferencedModel); - $record->{$alias} = $referencedModels; + if ($parentSubject instanceof ModelInterface) { + $parentSubject->$alias = null; + $parentSubject->$alias = $records->all(); + return $records; + } - if (static::$isPhalcon2) { - $record->{$alias} = null; - $record->{$alias} = $referencedModels; - } - } else { - $record->{$alias} = null; - $record->{$alias} = []; - } - } + if ($parentSubject instanceof Collection) { + $indexedRecordsCollections = $records->groupBy($relReferencedField); + $parentSubject->transform(function($item) use ($relField, $alias, $indexedRecordsCollections, $relReferencedField) { + // fix phalcon bug https://github.com/phalcon/cphalcon/issues/10556 + $item->$alias = null; + $item->$alias[] = $indexedRecordsCollections->get($item->$relField); + return $item; + }); + } - $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 = $parentSubject[0]; - - if ($isSingle) { - $record->{$alias} = empty($records) ? null : $records[0]; - } else { - if (empty($records)) { - $record->{$alias} = null; - $record->{$alias} = []; - } else { - $record->{$alias} = $records; - - if (static::$isPhalcon2) { - $record->{$alias} = null; - $record->{$alias} = $records; - } - } - } - } else { - $indexedRecords = []; - - // Keep all records in memory - foreach ($builder->getQuery()->execute() as $record) { - $records[] = $record; - - $foreignIdValue = $record->readAttribute($relReferencedField); - if (!$foreignIdValue) { - throw new \RuntimeException(sprintf('%s may lack the %s foreign key column', $alias, $relReferencedField)); - } - if ($isSingle) { - $indexedRecords[$foreignIdValue] = $record; - } else { - $indexedRecords[$foreignIdValue][] = $record; - } - } - - foreach ($parentSubject 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} = []; - } - } - } - } + return $records; + } + + /** + * @param Collection|ModelInterface $parentSubject + * @param QueryBuilder $builder + * @param RelationInterface $relation + */ + private function parseManyToMany($parentSubject, QueryBuilder $builder, RelationInterface $relation) + { + list($alias, $relField, $relReferencedModel, $relReferencedField, $relIrModel, $relIrField, $relIrReferencedField) = $this->getRelationDefinition($relation); + + if (!$relation->isThrough()) { + return $parentSubject; } - $this->subject = $records; - if (isset($this->delayLoads[$this->aliasName])) { - foreach ($this->delayLoads[$this->aliasName] as $delayLoad) { - $delayLoad->load(); - } - $this->delayLoads = []; + $bindValues = $this->getBindValues($parentSubject, $relField); + + $indexedRecordsRelIrData = (new QueryBuilder)->from($relIrModel) + ->inWhere("[{$relIrField}]", $bindValues) + ->getQuery() + ->execute(); + $intermediateCollection = collect($indexedRecordsRelIrData); + + $intermediateBindValues = $intermediateCollection->pluck($relIrReferencedField) + ->all(); + + $records = Loader::convertResultSetToCollection( + $builder->from($relReferencedModel) + ->inWhere("[{$relReferencedField}]", $intermediateBindValues) + ->getQuery() + ->execute() + , $relReferencedModel); + + $indexedRecordsCollections = $intermediateCollection->groupBy($relIrField)->transform(function($item) use ($records, $relIrReferencedField, $relReferencedField) { + return $records->whereIn($relReferencedField, $item->pluck($relIrReferencedField)); + }); + + if ($parentSubject instanceof ModelInterface) { + $parentSubject->$alias = null; + $parentSubject->$alias = $indexedRecordsCollections->all(); + return $records; } - return $this; + if ($parentSubject instanceof Collection) { + $parentSubject->transform(function($item) use ($relField, $alias, $indexedRecordsCollections) { + // fix phalcon bug https://github.com/phalcon/cphalcon/issues/10556 + $item->$alias = null; + $item->$alias[] = $indexedRecordsCollections->get($item->$relField); + return $item; + }); + } + + return $records; } public function delayLoad(EagerLoad $load, $withAliasName) { diff --git a/src/Model/EagerLoading/Loader.php b/src/Model/EagerLoading/Loader.php index b0d9705..8595fcf 100644 --- a/src/Model/EagerLoading/Loader.php +++ b/src/Model/EagerLoading/Loader.php @@ -4,12 +4,14 @@ use Phalcon\Di; use Phalcon\Mvc\Model\Relation; use Phalcon\Mvc\Model\Resultset\Simple; +use Phalcon\Mvc\Model\ResultsetInterface; use Phalcon\Mvc\ModelInterface; +use Tightenco\Collect\Support\Collection; final class Loader { const E_INVALID_SUBJECT = <<<'MSG' -Expected value of `subject` is either a ModelInterface object, a Simple object or an array of ModelInterface objects +Expected value of `subject` is either a ModelInterface object、 a Simple object or Collection object of ModelInterface objects MSG; /** @var ModelInterface[] */ @@ -28,86 +30,26 @@ final class Loader protected $resolvedRelations; /** - * @param ModelInterface|ModelInterface[]|Simple $from + * @param ModelInterface|ResultsetInterface $from * @param string $className - * @param ...$arguments * @throws \InvalidArgumentException */ public function __construct($from, $className) { - $error = false; - $arguments = array_slice(func_get_args(), 2); - - if (!$from instanceof ModelInterface) { - if (!$from instanceof Simple) { - if (($fromType = gettype($from)) !== 'array') { - if (null !== $from && $fromType !== 'boolean') { - $error = true; - } else { - $from = null; - } - } else { - $from = array_filter($from); - - if (empty($from)) { - $from = null; - } else { - 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 = []; - - foreach ($prev as $record) { - $from[] = $record; - } - - if (empty($from)) { - $from = null; - } -// else { -// $className = get_class($record); -// } - } - - $this->mustReturnAModel = false; + $this->subjectClassName = $className; + if ($from instanceof ResultsetInterface) { + $this->subject = self::convertResultSetToCollection($from, $this->subjectClassName); + } else if ($from instanceof ModelInterface || $from instanceof Collection) { + $this->subject = $from; } else { -// $className = get_class($from); - $from = [$from]; - - $this->mustReturnAModel = true; - } - - if ($error) { throw new \InvalidArgumentException(static::E_INVALID_SUBJECT); } - - $this->subject = $from; - $this->subjectClassName = $className; - $this->eagerLoads = ($from === null || empty($arguments)) ? [] : static::parseArguments($arguments); } /** * Create and get from a mixed $subject * - * @param ModelInterface|ModelInterface[]|Simple $subject - * @param mixed ...$arguments + * @param ModelInterface|ResultsetInterface $subject * @throws \InvalidArgumentException * @return mixed */ @@ -115,10 +57,8 @@ public static function from($subject) { if ($subject instanceof ModelInterface) { $ret = call_user_func_array('static::fromModel', func_get_args()); - } elseif ($subject instanceof Simple) { + } elseif ($subject instanceof ResultsetInterface) { $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); } @@ -129,7 +69,6 @@ public static function from($subject) /** * Create and get from a Model * @param ModelInterface $subject - * @param mixed ...$arguments * @return ModelInterface * @throws \ReflectionException */ @@ -141,26 +80,10 @@ public static function fromModel(ModelInterface $subject) return $instance->execute()->get(); } - /** - * Create and get from an array - * @param ModelInterface[] $subject - * @param mixed ...$arguments - * @return array - * @throws \ReflectionException - */ - 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 ModelInterface|ModelInterface[]|Simple $subject - * @param mixed ...$arguments - * @return ModelInterface|ModelInterface[]|Simple + * @param ResultsetInterface $subject + * @return Collection * @throws \ReflectionException */ public static function fromResultset($subject) @@ -176,17 +99,11 @@ public static function fromResultset($subject) */ public function get() { - $ret = $this->subject; - - if (null !== $ret && $this->mustReturnAModel) { - $ret = $ret[0]; - } - - return $ret; + return $this->subject; } /** - * @return null|ModelInterface[] + * @return Simple|ModelInterface */ public function getSubject() { @@ -202,31 +119,21 @@ public function getSubject() */ public static function parseArguments(array $arguments) { -// dump($arguments); if (empty($arguments)) { throw new \InvalidArgumentException('Arguments can not be empty'); } $relations = []; - if (count($arguments) === 1 && !empty(current($arguments))) { - foreach ($arguments 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; + foreach ($arguments as $relationAlias => $queryConstraints) { + if (is_string($relationAlias)) { + $relations[$relationAlias] = is_callable($queryConstraints) ? $queryConstraints : null; + } else { + if (is_string($queryConstraints)) { + $relations[$queryConstraints] = null; } } } - if (empty($relations)) { return []; } @@ -332,10 +239,7 @@ public function buildLoad($eagerDatas, $isNestedLoader = false, $nestedLevel = 0 $this->eagerRelations[$name] = new EagerLoad($relation, $constraints, $parent, $name, $this); } while (++$nestingLevel < $nestingLevels); } -// var_dump(array_keys($this->eagerRelations)); -// var_dump(array_keys($this->oldEagerRelations)); -// -// var_dump(array_keys(array_diff_key($this->eagerRelations, $this->oldEagerRelations))); + return array_diff_key($this->eagerRelations, $this->oldEagerRelations); } @@ -378,4 +282,16 @@ public function load($eagerTrees = []) return $this; } + + public function getSubjectClassName() + { + return $this->subjectClassName; + } + + public static function convertResultSetToCollection(ResultsetInterface $resultset, string $modelClass) + { + return collect($resultset)->map(function($item) use ($modelClass) { + return new $modelClass($item); + }); + } } diff --git a/src/Model/EagerLoading/QueryBuilder.php b/src/Model/EagerLoading/QueryBuilder.php index d05741d..2f87631 100644 --- a/src/Model/EagerLoading/QueryBuilder.php +++ b/src/Model/EagerLoading/QueryBuilder.php @@ -1,7 +1,6 @@ loader) { $isNestedLoader = false; @@ -51,12 +48,10 @@ public function with($relations) unset($arguments[0]); unset($arguments[1]); } -// dump(($arguments)); if (!$arguments) { return $this; } $relations = $this->loader->parseArguments($arguments); -// dump($this->currentAliasName); if ($this->currentAliasName && $isNestedLoader) { $nestedRelations = []; foreach ($relations as $key => $relation) { diff --git a/src/Traits/EagerLoadingTrait.php b/src/Traits/EagerLoadingTrait.php index 1fc03f6..20ed425 100644 --- a/src/Traits/EagerLoadingTrait.php +++ b/src/Traits/EagerLoadingTrait.php @@ -1,6 +1,8 @@ with( + array_merge([$this, get_class($this)], is_string($relations) ? func_get_args() : $relations) + ); + } }