diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 98efbcb..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,7 +0,0 @@ -tools: - php_sim: true - php_pdepend: true - php_analyzer: true -filter: - excluded_paths: - - 'tests/*' diff --git a/.travis.yml b/.travis.yml index efacb1e..c8f71de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,17 +4,9 @@ php: - 7.0 - 7.1 -install: - - travis_retry composer require satooshi/php-coveralls:~0.6@stable - before_script: - - mkdir -p build/logs - travis_retry composer self-update - travis_retry composer install --prefer-source --no-interaction --dev script: - - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml - - ./vendor/bin/phpcs src --standard=psr2 - -after_success: - - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then php vendor/bin/coveralls -v; fi;' + - ./vendor/bin/phpunit diff --git a/README.md b/README.md index 1277ea6..bd5f672 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,34 @@ # Sofa/Eloquence -[![Build Status](https://travis-ci.org/jarektkaczyk/eloquence.svg)](https://travis-ci.org/jarektkaczyk/eloquence) [![Coverage Status](https://coveralls.io/repos/jarektkaczyk/eloquence/badge.svg)](https://coveralls.io/r/jarektkaczyk/eloquence) [![Code Quality](https://scrutinizer-ci.com/g/jarektkaczyk/eloquence/badges/quality-score.png)](https://scrutinizer-ci.com/g/jarektkaczyk/eloquence) [![Downloads](https://poser.pugx.org/sofa/eloquence/downloads)](https://packagist.org/packages/sofa/eloquence) [![stable](https://poser.pugx.org/sofa/eloquence/v/stable.svg)](https://packagist.org/packages/sofa/eloquence) +[![Build Status](https://travis-ci.org/jarektkaczyk/eloquence.svg)](https://travis-ci.org/jarektkaczyk/eloquence) [![Downloads](https://poser.pugx.org/sofa/eloquence/downloads)](https://packagist.org/packages/sofa/eloquence) [![stable](https://poser.pugx.org/sofa/eloquence/v/stable.svg)](https://packagist.org/packages/sofa/eloquence) -Easy and flexible extensions for the [Eloquent ORM](https://laravel.com/docs/5.4/eloquent). +Easy and flexible extensions for the [Eloquent ORM](https://laravel.com/docs/eloquent). **If I'm saving you some time with my work, you can back me up on [Patreon page](https://patreon.com/jarektkaczyk).** -For older versions of Illuminate/Laravel please use: -- 5.3.* -> [5.3](https://github.com/jarektkaczyk/eloquence/tree/5.3) branch. -- 5.2.* -> [5.2](https://github.com/jarektkaczyk/eloquence/tree/5.2) branch. -- 5.1.* -> [5.1](https://github.com/jarektkaczyk/eloquence/tree/5.1) branch. -- 5.0.* -> [0.4](https://github.com/jarektkaczyk/eloquence/tree/0.4) branch. - Currently available extensions: -1. `Searchable` query - crazy-simple fulltext search through any related model (based on https://github.com/nicolaslopezj/searchable only written from scratch & greatly improved) -1. `Validable` - self-validating models -2. `Mappable` -map attributes to table fields and/or related models -3. `Metable` - meta attributes made easy -4. `Mutable` - flexible attribute get/set mutators with quick setup (with help of [Romain Lanz](https://github.com/RomainLanz)) -5. `Mutator` - pipe-based mutating +1. [Searchable](https://github.com/jarektkaczyk/eloquence-base) query - crazy-simple fulltext search through any related model +1. [Validable](https://github.com/jarektkaczyk/eloquence-validable) - self-validating models +2. [Mappable](https://github.com/jarektkaczyk/eloquence-mappable) -map attributes to table fields and/or related models +3. [Metable](https://github.com/jarektkaczyk/eloquence-metable) - meta attributes made easy +4. [Mutable](https://github.com/jarektkaczyk/eloquence-mutable) - flexible attribute get/set mutators with quick setup +5. [Mutator](https://github.com/jarektkaczyk/eloquence-mutable) - pipe-based mutating + +By installing this package you get aforementioned extensions. Alternatively you can pull just single extension: + +```bash +# get all extensions +composer require sofa/eloquence + +# get single extension, eg. Metable +composer require sofa/eloquence-metable +``` **Check the [documentation](https://github.com/jarektkaczyk/eloquence/wiki) for installation and usage info, [website](http://softonsofa.com/tag/eloquence/) for examples and [API reference](http://jarektkaczyk.github.io/eloquence-api)** ## Contribution -All contributions are welcome, PRs must be **tested** and **PSR-2 compliant**. +Shout out to all the Contributors! -To validate your builds before committing use the following composer command: -```bash -composer test -``` +All contributions are welcome, PRs must be **tested** and **PSR-2 compliant** - refer to particular extension repository. diff --git a/composer.json b/composer.json index 958e6c4..a13a8ff 100644 --- a/composer.json +++ b/composer.json @@ -24,37 +24,24 @@ ], "require": { "php": ">=7.0.0", - "sofa/hookable": "5.5.*", - "illuminate/database": "5.5.*" + "sofa/eloquence-base": "5.5.*", + "sofa/eloquence-metable": "5.5.*", + "sofa/eloquence-mutable": "5.5.*", + "sofa/eloquence-mappable": "5.5.*", + "sofa/eloquence-validable": "5.5.*" }, "require-dev": { "phpunit/phpunit": "4.5.0", "squizlabs/php_codesniffer": "2.3.3", "mockery/mockery": "0.9.4" }, - "autoload": { - "psr-4": { - "Sofa\\Eloquence\\": "src" - }, - "files": [ - "src/helpers.php" - ] - }, "autoload-dev": { "psr-4": { "Sofa\\Eloquence\\Tests\\": "tests" } }, - "extra": { - "laravel": { - "providers": [ - "Sofa\\Eloquence\\ServiceProvider" - ] - } - }, "minimum-stability": "stable", "scripts": { - "test": "phpunit && ./vendor/bin/phpcs src --standard=psr2 --report=diff --colors", - "phpcs": "./vendor/bin/phpcs src --standard=psr2 --report=diff --colors" + "test": "phpunit" } } diff --git a/src/AttributeCleaner/Observer.php b/src/AttributeCleaner/Observer.php deleted file mode 100644 index 379e67e..0000000 --- a/src/AttributeCleaner/Observer.php +++ /dev/null @@ -1,38 +0,0 @@ -cleanAttributes($model); - } - } - - /** - * Get rid of attributes that are not correct columns on this model's table. - * - * @param \Sofa\Eloquence\Contracts\CleansAttributes $model - * @return void - */ - protected function cleanAttributes(CleansAttributes $model) - { - $dirty = array_keys($model->getDirty()); - - $invalidColumns = array_diff($dirty, $model->getColumnListing()); - - foreach ($invalidColumns as $column) { - unset($model->{$column}); - } - } -} diff --git a/src/Builder.php b/src/Builder.php deleted file mode 100644 index cc600eb..0000000 --- a/src/Builder.php +++ /dev/null @@ -1,556 +0,0 @@ -query->from instanceof Subquery) { - $this->wheresToSubquery($this->query->from); - } - - return parent::get($columns); - } - - /** - * Search through any columns on current table or any defined relations - * and return results ordered by search relevance. - * - * @param array|string $query - * @param array $columns - * @param boolean $fulltext - * @param float $threshold - * @return $this - */ - public function search($query, $columns = null, $fulltext = true, $threshold = null) - { - if (is_bool($columns)) { - list($fulltext, $columns) = [$columns, []]; - } - - $parser = static::$parser->make(); - - $words = is_array($query) ? $query : $parser->parseQuery($query, $fulltext); - - $columns = $parser->parseWeights($columns ?: $this->model->getSearchableColumns()); - - if (count($words) && count($columns)) { - $this->query->from($this->buildSubquery($words, $columns, $threshold)); - } - - return $this; - } - - /** - * Build the search subquery. - * - * @param array $words - * @param array $mappings - * @param float $threshold - * @return \Sofa\Eloquence\Searchable\Subquery - */ - protected function buildSubquery(array $words, array $mappings, $threshold) - { - $subquery = new SearchableSubquery($this->query->newQuery(), $this->model->getTable()); - - $columns = $this->joinForSearch($mappings, $subquery); - - $threshold = (is_null($threshold)) - ? array_sum($columns->getWeights()) / 4 - : (float) $threshold; - - $subquery->select($this->model->getTable() . '.*') - ->from($this->model->getTable()) - ->groupBy($this->model->getQualifiedKeyName()); - - $this->addSearchClauses($subquery, $columns, $words, $threshold); - - return $subquery; - } - - /** - * Add select and where clauses on the subquery. - * - * @param \Sofa\Eloquence\Searchable\Subquery $subquery - * @param \Sofa\Eloquence\Searchable\ColumnCollection $columns - * @param array $words - * @param float $threshold - * @return void - */ - protected function addSearchClauses( - SearchableSubquery $subquery, - ColumnCollection $columns, - array $words, - $threshold - ) { - $whereBindings = $this->searchSelect($subquery, $columns, $words, $threshold); - - // For morphOne/morphMany support we need to port the bindings from JoinClauses. - $joinBindings = collect($subquery->getQuery()->joins)->flatMap(function ($join) { - return $join->getBindings(); - })->all(); - - $this->addBinding($joinBindings, 'select'); - - // Developer may want to skip the score threshold filtering by passing zero - // value as threshold in order to simply order full result by relevance. - // Otherwise we are going to add where clauses for speed improvement. - if ($threshold > 0) { - $this->searchWhere($subquery, $columns, $words, $whereBindings); - } - - $this->query->where('relevance', '>=', new Expression($threshold)); - - $this->query->orders = array_merge( - [['column' => 'relevance', 'direction' => 'desc']], - (array) $this->query->orders - ); - } - - /** - * Apply relevance select on the subquery. - * - * @param \Sofa\Eloquence\Searchable\Subquery $subquery - * @param \Sofa\Eloquence\Searchable\ColumnCollection $columns - * @param array $words - * @return array - */ - protected function searchSelect(SearchableSubquery $subquery, ColumnCollection $columns, array $words) - { - $cases = $bindings = []; - - foreach ($columns as $column) { - list($cases[], $binding) = $this->buildCase($column, $words); - - $bindings = array_merge_recursive($bindings, $binding); - } - - $select = implode(' + ', $cases); - - $subquery->selectRaw("max({$select}) as relevance"); - - $this->addBinding($bindings['select'], 'select'); - - return $bindings['where']; - } - - /** - * Apply where clauses on the subquery. - * - * @param \Sofa\Eloquence\Searchable\Subquery $subquery - * @param \Sofa\Eloquence\Searchable\ColumnCollection $columns - * @param array $words - * @return void - */ - protected function searchWhere( - SearchableSubquery $subquery, - ColumnCollection $columns, - array $words, - array $bindings - ) { - $operator = $this->getLikeOperator(); - - $wheres = []; - - foreach ($columns as $column) { - $wheres[] = implode( - ' or ', - array_fill(0, count($words), sprintf('%s %s ?', $column->getWrapped(), $operator)) - ); - } - - $where = implode(' or ', $wheres); - - $subquery->whereRaw("({$where})"); - - $this->addBinding($bindings, 'select'); - } - - /** - * Move where clauses to subquery to improve performance. - * - * @param \Sofa\Eloquence\Searchable\Subquery $subquery - * @return void - */ - protected function wheresToSubquery(SearchableSubquery $subquery) - { - $bindingKey = 0; - - $typesToMove = [ - 'basic', 'in', 'notin', 'between', 'null', - 'notnull', 'date', 'day', 'month', 'year', - ]; - - // Here we are going to move all the where clauses that we might apply - // on the subquery in order to improve performance, since this way - // we can drastically reduce number of joined rows on subquery. - foreach ((array) $this->query->wheres as $key => $where) { - $type = strtolower($where['type']); - - $bindingsCount = $this->countBindings($where, $type); - - if (in_array($type, $typesToMove) && $this->model->hasColumn($where['column'])) { - unset($this->query->wheres[$key]); - - $where['column'] = $this->model->getTable() . '.' . $where['column']; - - $subquery->getQuery()->wheres[] = $where; - - $whereBindings = $this->query->getRawBindings()['where']; - - $bindings = array_splice($whereBindings, $bindingKey, $bindingsCount); - - $this->query->setBindings($whereBindings, 'where'); - - $this->query->addBinding($bindings, 'select'); - - // if where is not to be moved onto the subquery, let's increment - // binding key appropriately, so we can reliably move binding - // for the next where clauses in the loop that is running. - } else { - $bindingKey += $bindingsCount; - } - } - } - - /** - * Get number of bindings provided for a where clause. - * - * @param array $where - * @param string $type - * @return integer - */ - protected function countBindings(array $where, $type) - { - if ($this->isHasWhere($where, $type)) { - return substr_count($where['column'] . $where['value'], '?'); - } elseif ($type === 'basic') { - return (int) !$where['value'] instanceof Expression; - } elseif (in_array($type, ['basic', 'date', 'year', 'month', 'day'])) { - return (int) !$where['value'] instanceof Expression; - } elseif (in_array($type, ['null', 'notnull'])) { - return 0; - } elseif ($type === 'between') { - return 2; - } elseif (in_array($type, ['in', 'notin'])) { - return count($where['values']); - } elseif ($type === 'raw') { - return substr_count($where['sql'], '?'); - } elseif (in_array($type, ['nested', 'sub', 'exists', 'notexists', 'insub', 'notinsub'])) { - return count($where['query']->getBindings()); - } - } - - /** - * Determine whether where clause is eloquent has subquery. - * - * @param array $where - * @param string $type - * @return boolean - */ - protected function isHasWhere($where, $type) - { - return $type === 'basic' - && $where['column'] instanceof Expression - && $where['value'] instanceof Expression; - } - - /** - * Build case clause from all words for a single column. - * - * @param \Sofa\Eloquence\Searchable\Column $column - * @param array $words - * @return array - */ - protected function buildCase(Column $column, array $words) - { - // THIS IS BAD - // @todo refactor - - $operator = $this->getLikeOperator(); - - $bindings['select'] = $bindings['where'] = array_map(function ($word) { - return $this->caseBinding($word); - }, $words); - - $case = $this->buildEqualsCase($column, $words); - - if (strpos(implode('', $words), '*') !== false) { - $leftMatching = []; - - foreach ($words as $key => $word) { - if ($this->isLeftMatching($word)) { - $leftMatching[] = sprintf('%s %s ?', $column->getWrapped(), $operator); - $bindings['select'][] = $bindings['where'][$key] = $this->caseBinding($word) . '%'; - } - } - - if (count($leftMatching)) { - $leftMatching = implode(' or ', $leftMatching); - $score = 5 * $column->getWeight(); - $case .= " + case when {$leftMatching} then {$score} else 0 end"; - } - - $wildcards = []; - - foreach ($words as $key => $word) { - if ($this->isWildcard($word)) { - $wildcards[] = sprintf('%s %s ?', $column->getWrapped(), $operator); - $bindings['select'][] = $bindings['where'][$key] = '%'.$this->caseBinding($word) . '%'; - } - } - - if (count($wildcards)) { - $wildcards = implode(' or ', $wildcards); - $score = 1 * $column->getWeight(); - $case .= " + case when {$wildcards} then {$score} else 0 end"; - } - } - - return [$case, $bindings]; - } - - /** - * Replace '?' with single character SQL wildcards. - * - * @param string $word - * @return string - */ - protected function caseBinding($word) - { - $parser = static::$parser->make(); - - return str_replace('?', '_', $parser->stripWildcards($word)); - } - - /** - * Build basic search case for 'equals' comparison. - * - * @param \Sofa\Eloquence\Searchable\Column $column - * @param array $words - * @return string - */ - protected function buildEqualsCase(Column $column, array $words) - { - $equals = implode(' or ', array_fill(0, count($words), sprintf('%s = ?', $column->getWrapped()))); - - $score = 15 * $column->getWeight(); - - return "case when {$equals} then {$score} else 0 end"; - } - - /** - * Determine whether word ends with wildcard. - * - * @param string $word - * @return boolean - */ - protected function isLeftMatching($word) - { - return ends_with($word, '*'); - } - - /** - * Determine whether word starts and ends with wildcards. - * - * @param string $word - * @return boolean - */ - protected function isWildcard($word) - { - return ends_with($word, '*') && starts_with($word, '*'); - } - - /** - * Get driver-specific case insensitive like operator. - * - * @return string - */ - public function getLikeOperator() - { - $grammar = $this->query->getGrammar(); - - if ($grammar instanceof PostgresGrammar) { - return 'ilike'; - } - - return 'like'; - } - - /** - * Join related tables on the search subquery. - * - * @param array $mappings - * @param \Sofa\Eloquence\Searchable\Subquery $subquery - * @return \Sofa\Eloquence\Searchable\ColumnCollection - */ - protected function joinForSearch($mappings, $subquery) - { - $mappings = is_array($mappings) ? $mappings : (array) $mappings; - - $columns = new ColumnCollection; - - $grammar = $this->query->getGrammar(); - - $joiner = static::$joinerFactory->make($subquery->getQuery(), $this->model); - - // Here we loop through the search mappings in order to join related tables - // appropriately and build a searchable column collection, which we will - // use to build select and where clauses with correct table prefixes. - foreach ($mappings as $mapping => $weight) { - if (strpos($mapping, '.') !== false) { - list($relation, $column) = $this->model->parseMappedColumn($mapping); - - $related = $joiner->leftJoin($relation); - - $columns->add( - new Column($grammar, $related->getTable(), $column, $mapping, $weight) - ); - } else { - $columns->add( - new Column($grammar, $this->model->getTable(), $mapping, $mapping, $weight) - ); - } - } - - return $columns; - } - - /** - * Prefix selected columns with table name in order to avoid collisions. - * - * @return $this - */ - public function prefixColumnsForJoin() - { - if (!$columns = $this->query->columns) { - return $this->select($this->model->getTable() . '.*'); - } - - foreach ($columns as $key => $column) { - if ($this->model->hasColumn($column)) { - $columns[$key] = $this->model->getTable() . '.' . $column; - } - } - - $this->query->columns = $columns; - - return $this; - } - - /** - * Join related tables. - * - * @param array|string $relations - * @param string $type - * @return $this - */ - public function joinRelations($relations, $type = 'inner') - { - if (is_null($this->joiner)) { - $this->joiner = static::$joinerFactory->make($this); - } - - if (!is_array($relations)) { - list($relations, $type) = [func_get_args(), 'inner']; - } - - foreach ($relations as $relation) { - $this->joiner->join($relation, $type); - } - - return $this; - } - - /** - * Left join related tables. - * - * @param array|string $relations - * @return $this - */ - public function leftJoinRelations($relations) - { - $relations = is_array($relations) ? $relations : func_get_args(); - - return $this->joinRelations($relations, 'left'); - } - - /** - * Right join related tables. - * - * @param array|string $relations - * @return $this - */ - public function rightJoinRelations($relations) - { - $relations = is_array($relations) ? $relations : func_get_args(); - - return $this->joinRelations($relations, 'right'); - } - - /** - * Set search query parser factory instance. - * - * @param \Sofa\Eloquence\Contracts\Searchable\ParserFactory $factory - */ - public static function setParserFactory(ParserFactory $factory) - { - static::$parser = $factory; - } - - /** - * Set the relations joiner factory instance. - * - * @param \Sofa\Eloquence\Contracts\Relations\JoinerFactory $factory - */ - public static function setJoinerFactory(JoinerFactory $factory) - { - static::$joinerFactory = $factory; - } -} diff --git a/src/Contracts/Attribute.php b/src/Contracts/Attribute.php deleted file mode 100644 index c92f18a..0000000 --- a/src/Contracts/Attribute.php +++ /dev/null @@ -1,49 +0,0 @@ -bound('eloquence.mutator')) { - static::setAttributeMutator(app('eloquence.mutator')); - } else { - static::setAttributeMutator(new Mutator); - } - } - } - - /** - * Determine whether where should be treated as whereNull. - * - * @param string $method - * @param ArgumentBag $args - * @return boolean - */ - protected function isWhereNull($method, ArgumentBag $args) - { - return $method === 'whereNull' || $method === 'where' && $this->isWhereNullByArgs($args); - } - - /** - * Determine whether where is a whereNull by the arguments passed to where method. - * - * @param ArgumentBag $args - * @return boolean - */ - protected function isWhereNullByArgs(ArgumentBag $args) - { - return is_null($args->get('operator')) - || is_null($args->get('value')) && !in_array($args->get('operator'), ['<>', '!=']); - } - - /** - * Extract real name and alias from the sql select clause. - * - * @param string $column - * @return array - */ - protected function extractColumnAlias($column) - { - $alias = $column; - - if (strpos($column, ' as ') !== false) { - list($column, $alias) = explode(' as ', $column); - } - - return [$column, $alias]; - } - - /** - * Get the target relation and column from the mapping. - * - * @param string $mapping - * @return array - */ - public function parseMappedColumn($mapping) - { - $segments = explode('.', $mapping); - - $column = array_pop($segments); - - $target = implode('.', $segments); - - return [$target, $column]; - } - - /** - * Determine whether the key is meta attribute or actual table field. - * - * @param string $key - * @return boolean - */ - public static function hasColumn($key) - { - static::loadColumnListing(); - - return in_array((string) $key, static::$columnListing); - } - - /** - * Get searchable columns defined on the model. - * - * @return array - */ - public function getSearchableColumns() - { - return (property_exists($this, 'searchableColumns')) ? $this->searchableColumns : []; - } - - /** - * Get model table columns. - * - * @return array - */ - public static function getColumnListing() - { - static::loadColumnListing(); - - return static::$columnListing; - } - - /** - * Fetch model table columns. - * - * @return void - */ - protected static function loadColumnListing() - { - if (empty(static::$columnListing)) { - $instance = new static; - - static::$columnListing = $instance->getConnection() - ->getSchemaBuilder() - ->getColumnListing($instance->getTable()); - } - } - - /** - * Create new Eloquence query builder for the instance. - * - * @param \Sofa\Eloquence\Query\Builder $query - * @return \Sofa\Eloquence\Builder - */ - public function newEloquentBuilder($query) - { - return new Builder($query); - } - - /** - * Get a new query builder instance for the connection. - * - * @return \Sofa\Eloquence\Query\Builder - */ - protected function newBaseQueryBuilder() - { - $conn = $this->getConnection(); - - $grammar = $conn->getQueryGrammar(); - - return new QueryBuilder($conn, $grammar, $conn->getPostProcessor()); - } - - /** - * Set attribute mutator instance. - * - * @codeCoverageIgnore - * - * @param \Sofa\Eloquence\Contracts\Mutator $mutator - * @return void - */ - public static function setAttributeMutator(MutatorContract $mutator) - { - static::$attributeMutator = $mutator; - } - - /** - * Get attribute mutator instance. - * - * @codeCoverageIgnore - * - * @return \Sofa\Eloquence\Contracts\Mutator - */ - public static function getAttributeMutator() - { - return static::$attributeMutator; - } -} diff --git a/src/Mappable.php b/src/Mappable.php deleted file mode 100644 index 2d18e7a..0000000 --- a/src/Mappable.php +++ /dev/null @@ -1,610 +0,0 @@ -{$method}()); - } - } - - /** - * Custom query handler for querying mapped attributes. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return mixed - */ - protected function mappedQuery(Builder $query, $method, ArgumentBag $args) - { - $mapping = $this->getMappingForAttribute($args->get('column')); - - if ($this->relationMapping($mapping)) { - return $this->mappedRelationQuery($query, $method, $args, $mapping); - } - - $args->set('column', $mapping); - - return $query->callParent($method, $args->all()); - } - - /** - * Adjust mapped columns for select statement. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return void - */ - protected function mappedSelect(Builder $query, ArgumentBag $args) - { - $columns = $args->get('columns'); - - foreach ($columns as $key => $column) { - list($column, $as) = $this->extractColumnAlias($column); - - // Each mapped column will be selected appropriately. If it's alias - // then prefix it with current table and use original field name - // otherwise join required mapped tables and select the field. - if ($this->hasMapping($column)) { - $mapping = $this->getMappingForAttribute($column); - - if ($this->relationMapping($mapping)) { - list($target, $mapped) = $this->parseMappedColumn($mapping); - - $table = $this->joinMapped($query, $target); - } else { - list($table, $mapped) = [$this->getTable(), $mapping]; - } - - $columns[$key] = "{$table}.{$mapped}"; - - if ($as !== $column) { - $columns[$key] .= " as {$as}"; - } - - // For non mapped columns present on this table we will simply - // add the prefix, in order to avoid any column collisions, - // that are likely to happen when we are joining tables. - } elseif ($this->hasColumn($column)) { - $columns[$key] = "{$this->getTable()}.{$column}"; - } - } - - $args->set('columns', $columns); - } - - /** - * Handle querying relational mappings. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $mapping - * @return mixed - */ - protected function mappedRelationQuery($query, $method, ArgumentBag $args, $mapping) - { - list($target, $column) = $this->parseMappedColumn($mapping); - - if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) { - return $this->mappedJoinQuery($query, $method, $args, $target, $column); - } - - return $this->mappedHasQuery($query, $method, $args, $target, $column); - } - - /** - * Join mapped table(s) in order to call given method. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $target - * @param string $column - * @return mixed - */ - protected function mappedJoinQuery($query, $method, ArgumentBag $args, $target, $column) - { - $table = $this->joinMapped($query, $target); - - // For aggregates we need the actual function name - // so it can be called directly on the builder. - $method = $args->get('function') ?: $method; - - return (in_array($method, ['orderBy', 'lists', 'pluck'])) - ? $this->{"{$method}Mapped"}($query, $args, $table, $column, $target) - : $this->mappedSingleResult($query, $method, "{$table}.{$column}"); - } - - /** - * Order query by mapped attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $table - * @param string $column - * @param string $target - * @return \Sofa\Eloquence\Builder - */ - protected function orderByMapped(Builder $query, ArgumentBag $args, $table, $column, $target) - { - $query->with($target)->getQuery()->orderBy("{$table}.{$column}", $args->get('direction')); - - return $query; - } - - /** - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $table - * @param string $column - * - * @return array - */ - protected function listsMapped(Builder $query, ArgumentBag $args, $table, $column) - { - return $this->pluckMapped($query, $args, $table, $column); - } - - /** - * Get an array with the values of given mapped attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $table - * @param string $column - * @return array - */ - protected function pluckMapped(Builder $query, ArgumentBag $args, $table, $column) - { - $query->select("{$table}.{$column}"); - - if (!is_null($args->get('key'))) { - $this->mappedSelectListsKey($query, $args->get('key')); - } - - $args->set('column', $column); - - return $query->callParent('pluck', $args->all()); - } - - /** - * Add select clause for key of the list array. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $key - * @return \Sofa\Eloquence\Builder - */ - protected function mappedSelectListsKey(Builder $query, $key) - { - if ($this->hasColumn($key)) { - return $query->addSelect($this->getTable() . '.' . $key); - } - - return $query->addSelect($key); - } - - /** - * Join mapped table(s). - * - * @param \Sofa\Eloquence\Builder $query - * @param string $target - * @return string - */ - protected function joinMapped(Builder $query, $target) - { - $query->prefixColumnsForJoin(); - - $parent = $this; - - foreach (explode('.', $target) as $segment) { - list($table, $parent) = $this->joinSegment($query, $segment, $parent); - } - - return $table; - } - - /** - * Join relation's table accordingly. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $segment - * @param \Illuminate\Database\Eloquent\Model $parent - * @return array - */ - protected function joinSegment(Builder $query, $segment, EloquentModel $parent) - { - $relation = $parent->{$segment}(); - $related = $relation->getRelated(); - $table = $related->getTable(); - - // If the table has been already joined let's skip it. Otherwise we will left join - // it in order to allow using some query methods on mapped columns. Polymorphic - // relations require also additional constraints, so let's handle it as well. - if (!$this->alreadyJoined($query, $table)) { - list($fk, $pk) = $this->getJoinKeys($relation); - - $query->leftJoin($table, function ($join) use ($fk, $pk, $relation, $parent, $related) { - $join->on($fk, '=', $pk); - - if ($relation instanceof MorphOne || $relation instanceof MorphTo) { - $morphClass = ($relation instanceof MorphOne) - ? $parent->getMorphClass() - : $related->getMorphClass(); - - $join->where($relation->getQualifiedMorphType(), '=', $morphClass); - } - }); - } - - return [$table, $related]; - } - - /** - * Determine whether given table has been already joined. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $table - * @return boolean - */ - protected function alreadyJoined(Builder $query, $table) - { - $joined = Arr::pluck((array) $query->getQuery()->joins, 'table'); - - return in_array($table, $joined); - } - - /** - * Get the keys from relation in order to join the table. - * - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @return array - * - * @throws \LogicException - */ - protected function getJoinKeys(Relation $relation) - { - if ($relation instanceof HasOne || $relation instanceof MorphOne) { - return [$relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName()]; - } - - if ($relation instanceof BelongsTo && !$relation instanceof MorphTo) { - return [$relation->getQualifiedForeignKey(), $relation->getQualifiedOwnerKeyName()]; - } - - $class = get_class($relation); - - throw new LogicException( - "Only HasOne, MorphOne and BelongsTo mappings can be queried. {$class} given." - ); - } - - /** - * Get single value result from the mapped attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param string $qualifiedColumn - * @return mixed - */ - protected function mappedSingleResult(Builder $query, $method, $qualifiedColumn) - { - return $query->getQuery()->select("{$qualifiedColumn}")->{$method}("{$qualifiedColumn}"); - } - - /** - * Add whereHas subquery on the mapped attribute relation. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $target - * @param string $column - * @return \Sofa\Eloquence\Builder - */ - protected function mappedHasQuery(Builder $query, $method, ArgumentBag $args, $target, $column) - { - $boolean = $this->getMappedBoolean($args); - - $operator = $this->getMappedOperator($method, $args); - - $args->set('column', $column); - - return $query - ->has($target, $operator, 1, $boolean, $this->getMappedWhereConstraint($method, $args)) - ->with($target); - } - - /** - * Get the relation constraint closure. - * - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return \Closure - */ - protected function getMappedWhereConstraint($method, ArgumentBag $args) - { - return function ($query) use ($method, $args) { - call_user_func_array([$query, $method], $args->all()); - }; - } - - /** - * Get boolean called on the original method and set it to default. - * - * @param \Sofa\EloquenceArgumentBag $args - * @return string - */ - protected function getMappedBoolean(ArgumentBag $args) - { - $boolean = $args->get('boolean'); - - $args->set('boolean', 'and'); - - return $boolean; - } - - /** - * Determine the operator for count relation query and set 'not' appropriately. - * - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return string - */ - protected function getMappedOperator($method, ArgumentBag $args) - { - if ($not = $args->get('not')) { - $args->set('not', false); - } - - if ($null = $this->isWhereNull($method, $args)) { - $args->set('not', true); - } - - return ($not ^ $null) ? '<' : '>='; - } - - /** - * Get the mapping key. - * - * @param string $key - * @return string|null - */ - public function getMappingForAttribute($key) - { - if ($this->hasMapping($key)) { - return $this->mappedAttributes[$key]; - } - } - - /** - * Determine whether the mapping points to relation. - * - * @param string $mapping - * @return boolean - */ - protected function relationMapping($mapping) - { - return strpos($mapping, '.') !== false; - } - - /** - * Determine whether a mapping exists for an attribute. - * - * @param string $key - * @return boolean - */ - public function hasMapping($key) - { - if (is_null($this->mappedAttributes)) { - $this->parseMappings(); - } - - return array_key_exists((string) $key, $this->mappedAttributes); - } - - /** - * Parse defined mappings into flat array. - * - * @return void - */ - protected function parseMappings() - { - $this->mappedAttributes = []; - - foreach ($this->getMaps() as $attribute => $mapping) { - if (is_array($mapping)) { - $this->parseImplicitMapping($mapping, $attribute); - } else { - $this->mappedAttributes[$attribute] = $mapping; - } - } - } - - /** - * Parse implicit mappings. - * - * @param array $attributes - * @param string $target - * @return void - */ - protected function parseImplicitMapping($attributes, $target) - { - foreach ($attributes as $attribute) { - $this->mappedAttributes[$attribute] = "{$target}.{$attribute}"; - } - } - - /** - * Map an attribute to a value. - * - * @param string $key - * @return mixed - */ - protected function mapAttribute($key) - { - $segments = explode('.', $this->getMappingForAttribute($key)); - - return $this->getTarget($this, $segments); - } - - /** - * Get mapped value. - * - * @param \Illuminate\Database\Eloquent\Model $target - * @param array $segments - * @return mixed - */ - protected function getTarget($target, array $segments) - { - foreach ($segments as $segment) { - if (!$target) { - return; - } - - $target = $target->{$segment}; - } - - return $target; - } - - /** - * Set value of a mapped attribute. - * - * @param string $key - * @param mixed $value - */ - protected function setMappedAttribute($key, $value) - { - $segments = explode('.', $this->getMappingForAttribute($key)); - - $attribute = array_pop($segments); - - if ($target = $this->getTarget($this, $segments)) { - $this->addTargetToSave($target); - - $target->{$attribute} = $value; - } - } - - /** - * Flag mapped model to be saved along with this model. - * - * @param \Illuminate\Database\Eloquent\Model $target - */ - protected function addTargetToSave($target) - { - if ($this !== $target) { - $this->targetsToSave[] = $target; - } - } - - /** - * Save mapped relations. - * - * @return void - */ - protected function saveMapped() - { - foreach (array_unique($this->targetsToSave) as $target) { - $target->save(); - } - - $this->targetsToSave = []; - } - - /** - * Unset mapped attribute. - * - * @param string $key - * @return void - */ - protected function forget($key) - { - $mapping = $this->getMappingForAttribute($key); - - list($target, $attribute) = $this->parseMappedColumn($mapping); - - $target = $target ? $this->getTarget($this, explode('.', $target)) : $this; - - unset($target->{$attribute}); - } - - /** - * @codeCoverageIgnore - * - * @param string $key - * @param mixed $value - * - * @inheritdoc - */ - protected function mutateAttributeForArray($key, $value) - { - if ($this->hasMapping($key)) { - $value = $this->mapAttribute($key); - - return $value instanceof Arrayable ? $value->toArray() : $value; - } - - return parent::mutateAttributeForArray($key, $value); - } - - /** - * Get the array of attribute mappings. - * - * @return array - */ - public function getMaps() - { - return (property_exists($this, 'maps')) ? $this->maps : []; - } -} diff --git a/src/Mappable/Hooks.php b/src/Mappable/Hooks.php deleted file mode 100644 index 071c309..0000000 --- a/src/Mappable/Hooks.php +++ /dev/null @@ -1,143 +0,0 @@ -get('method'); - $args = $bag->get('args'); - $column = $args->get('column'); - - if ($this->hasMapping($column)) { - return call_user_func_array([$this, 'mappedQuery'], [$query, $method, $args]); - } - - if (in_array($method, ['select', 'addSelect'])) { - call_user_func_array([$this, 'mappedSelect'], [$query, $args]); - } - - return $next($query, $bag); - }; - } - - /** - * Register hook on save method. - * - * @codeCoverageIgnore - * - * @return \Closure - */ - public function save() - { - return function ($next, $value, $args) { - $this->saveMapped(); - - return $next($value, $args); - }; - } - - /** - * Register hook on isset call. - * - * @codeCoverageIgnore - * - * @return \Closure - */ - public function __issetHook() - { - return function ($next, $isset, $args) { - $key = $args->get('key'); - - if (!$isset && $this->hasMapping($key)) { - return (bool) $this->mapAttribute($key); - } - - return $next($isset, $args); - }; - } - - /** - * Register hook on unset call. - * - * @codeCoverageIgnore - * - * @return \Closure - */ - public function __unsetHook() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if ($this->hasMapping($key)) { - return $this->forget($key); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on getAttribute method. - * - * @codeCoverageIgnore - * - * @return \Closure - */ - public function getAttribute() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if ($this->hasMapping($key)) { - $value = $this->mapAttribute($key); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on setAttribute method. - * - * @codeCoverageIgnore - * - * @return \Closure - */ - public function setAttribute() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if ($this->hasMapping($key)) { - return $this->setMappedAttribute($key, $value); - } - - return $next($value, $args); - }; - } - - public function __call($method, $params) - { - if (strpos($method, '__') === 0 && method_exists($this, $method.'Hook')) { - return call_user_func_array([$this, $method.'Hook'], $params); - } - - throw new BadMethodCallException("Method [{$method}] doesn't exist on this object."); - } -} diff --git a/src/Metable.php b/src/Metable.php deleted file mode 100644 index d67101a..0000000 --- a/src/Metable.php +++ /dev/null @@ -1,512 +0,0 @@ -{$method}()); - } - } - - /** - * Determine wheter method called on the query is customizable by this trait. - * - * @param string $method - * @return boolean - */ - protected function isMetaQueryable($method) - { - return in_array($method, $this->metaQueryable); - } - - /** - * Custom query handler for querying meta attributes. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return mixed - */ - protected function metaQuery(Builder $query, $method, ArgumentBag $args) - { - if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) { - return $this->metaJoinQuery($query, $method, $args); - } - - return $this->metaHasQuery($query, $method, $args); - } - - /** - * Adjust meta columns for select statement. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return void - */ - protected function metaSelect(Builder $query, ArgumentBag $args) - { - $columns = $args->get('columns'); - - foreach ($columns as $key => $column) { - list($column, $alias) = $this->extractColumnAlias($column); - - if ($this->hasColumn($column)) { - $select = "{$this->getTable()}.{$column}"; - - if ($column !== $alias) { - $select .= " as {$alias}"; - } - - $columns[$key] = $select; - } elseif (is_string($column) && $column != '*' && strpos($column, '.') === false) { - $table = $this->joinMeta($query, $column); - - $columns[$key] = "{$table}.meta_value as {$alias}"; - } - } - - $args->set('columns', $columns); - } - - /** - * Join meta attributes table in order to call provided method. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return mixed - */ - protected function metaJoinQuery(Builder $query, $method, ArgumentBag $args) - { - $alias = $this->joinMeta($query, $args->get('column')); - - // For aggregates we need the actual function name - // so it can be called directly on the builder. - $method = $args->get('function') ?: $method; - - return (in_array($method, ['orderBy', 'lists', 'pluck'])) - ? $this->{"{$method}Meta"}($query, $args, $alias) - : $this->metaSingleResult($query, $method, $alias); - } - - /** - * Order query by meta attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $alias - * @return \Sofa\Eloquence\Builder - */ - protected function orderByMeta(Builder $query, $args, $alias) - { - $query->with('metaAttributes')->getQuery()->orderBy("{$alias}.meta_value", $args->get('direction')); - - return $query; - } - - /** - * Get an array with the values of given meta attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @param string $alias - * @return array - */ - protected function pluckMeta(Builder $query, ArgumentBag $args, $alias) - { - list($column, $key) = [$args->get('column'), $args->get('key')]; - - $query->select("{$alias}.meta_value as {$column}"); - - if (!is_null($key)) { - $this->metaSelectListsKey($query, $key); - } - - return $query->callParent('pluck', $args->all()); - } - - /** - * Add select clause for key of the list array. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $key - * @return \Sofa\Eloquence\Builder - */ - protected function metaSelectListsKey(Builder $query, $key) - { - if (strpos($key, '.') !== false) { - return $query->addSelect($key); - } elseif ($this->hasColumn($key)) { - return $query->addSelect($this->getTable() . '.' . $key); - } - - $alias = $this->joinMeta($query, $key); - - return $query->addSelect("{$alias}.meta_value as {$key}"); - } - - /** - * Get single value result from the meta attribute. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param string $alias - * @return mixed - */ - protected function metaSingleResult(Builder $query, $method, $alias) - { - return $query->getQuery()->select("{$alias}.meta_value")->{$method}("{$alias}.meta_value"); - } - - - /** - * Join meta attributes table. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $column - * @return string - */ - protected function joinMeta(Builder $query, $column) - { - $query->prefixColumnsForJoin(); - - $alias = $this->generateMetaAlias(); - - $table = (new Attribute)->getTable(); - - $query->leftJoin("{$table} as {$alias}", function ($join) use ($alias, $column) { - $join->on("{$alias}.metable_id", '=', $this->getQualifiedKeyName()) - ->where("{$alias}.metable_type", '=', $this->getMorphClass()) - ->where("{$alias}.meta_key", '=', $column); - }); - - return $alias; - } - - /** - * Generate unique alias for meta attributes table. - * - * @return string - */ - protected function generateMetaAlias() - { - return md5(microtime(true)) . '_meta_alias'; - } - - /** - * Add whereHas subquery on the meta attributes relation. - * - * @param \Sofa\Eloquence\Builder $query - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return \Sofa\Eloquence\Builder - */ - protected function metaHasQuery(Builder $query, $method, ArgumentBag $args) - { - $boolean = $this->getMetaBoolean($args); - - $operator = $this->getMetaOperator($method, $args); - - if (in_array($method, ['whereBetween', 'where'])) { - $this->unbindNumerics($args); - } - - return $query - ->has('metaAttributes', $operator, 1, $boolean, $this->getMetaWhereConstraint($method, $args)) - ->with('metaAttributes'); - } - - /** - * Get boolean called on the original method and set it to default. - * - * @param \Sofa\EloquenceArgumentBag $args - * @return string - */ - protected function getMetaBoolean(ArgumentBag $args) - { - $boolean = $args->get('boolean'); - - $args->set('boolean', 'and'); - - return $boolean; - } - - /** - * Determine the operator for count relation query. - * - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return string - */ - protected function getMetaOperator($method, ArgumentBag $args) - { - if ($not = $args->get('not')) { - $args->set('not', false); - } - - return ($not ^ $this->isWhereNull($method, $args)) ? '<' : '>='; - } - - /** - * Integers and floats must be passed in raw form in order to avoid string - * comparison, due to the fact that all meta values are stored as strings. - * - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return void - */ - protected function unbindNumerics(ArgumentBag $args) - { - if (($value = $args->get('value')) && (is_int($value) || is_float($value))) { - $args->set('value', $this->raw($value)); - } elseif ($values = $args->get('values')) { - foreach ($values as $key => $value) { - if (is_int($value) || is_float($value)) { - $values[$key] = $this->raw($value); - } - } - - $args->set('values', $values); - } - } - - /** - * Get the relation constraint closure. - * - * @param string $method - * @param \Sofa\Hookable\Contracts\ArgumentBag $args - * @return \Closure - */ - protected function getMetaWhereConstraint($method, ArgumentBag $args) - { - $column = $args->get('column'); - - $args->set('column', 'meta_value'); - - if ($method === 'whereBetween') { - return $this->getMetaBetweenConstraint($column, $args->get('values')); - } - - return function ($query) use ($column, $method, $args) { - $query->where('meta_key', $column); - - if ($args->get('value') || $args->get('values')) { - call_user_func_array([$query, $method], $args->all()); - } - }; - } - - /** - * Query Builder whereBetween override required to pass raw numeric values. - * - * @param string $column - * @param array $values - * @return \Closure - */ - protected function getMetaBetweenConstraint($column, array $values) - { - $min = $values[0]; - $max = $values[1]; - - return function ($query) use ($column, $min, $max) { - $query->where('meta_key', $column) - ->where('meta_value', '>=', $min) - ->where('meta_value', '<=', $max); - }; - } - - /** - * Save new or updated meta attributes and delete the ones that were unset. - * - * @return void - */ - protected function saveMeta() - { - foreach ($this->getMetaAttributes() as $attribute) { - if (is_null($attribute->getValue())) { - $attribute->delete(); - } else { - $this->metaAttributes()->save($attribute); - } - } - } - - /** - * Determine whether meta attribute is allowed for the model. - * - * @param string $key - * @return boolean - */ - public function allowsMeta($key) - { - $allowed = $this->getAllowedMeta(); - - return empty($allowed) || in_array($key, $allowed); - } - - /** - * Determine whether meta attribute exists on the model. - * - * @param string $key - * @return boolean - */ - public function hasMeta($key) - { - return array_key_exists($key, $this->getMetaAttributesArray()); - } - - /** - * Get meta attribute value. - * - * @param string $key - * @return mixed - */ - public function getMeta($key) - { - return $this->getMetaAttributes()->getValue($key); - } - /** - * Get meta attribute values by group. - * - * @param string $key - * @return mixed - */ - public function getMetaByGroup($group) - { - return $this->getMetaAttributes()->getMetaByGroup($group); - } - /** - * Set meta attribute. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function setMeta($key, $value, $group = null) - { - $this->getMetaAttributes()->set($key, $value, $group); - } - - /** - * Meta attributes relation. - * - * @codeCoverageIgnore - * - * @return \Illuminate\Database\Eloquent\Relations\MorphMany - */ - public function metaAttributes() - { - return $this->morphMany('Sofa\Eloquence\Metable\Attribute', 'metable'); - } - - /** - * Get meta attributes as collection. - * - * @return \Sofa\Eloquence\Metable\AttributeBag - */ - public function getMetaAttributes() - { - $this->loadMetaAttributes(); - - return $this->getRelation('metaAttributes'); - } - - /** - * Accessor for metaAttributes property - * - * @return \Sofa\Eloquence\Metable\AttributeBag - */ - public function getMetaAttributesAttribute() - { - return $this->getMetaAttributes(); - } - - /** - * Get meta attributes as associative array. - * - * @return array - */ - public function getMetaAttributesArray() - { - return $this->getMetaAttributes()->toArray(); - } - - /** - * Load meta attributes relation. - * - * @return void - */ - protected function loadMetaAttributes() - { - if (!array_key_exists('metaAttributes', $this->relations)) { - $this->reloadMetaAttributes(); - } - - $attributes = $this->getRelation('metaAttributes'); - - if (!$attributes instanceof AttributeBag) { - $this->setRelation('metaAttributes', (new Attribute)->newBag($attributes->all())); - } - } - - /** - * Reload meta attributes from db or set empty bag for newly created model. - * - * @return $this - */ - protected function reloadMetaAttributes() - { - return ($this->exists) - ? $this->load('metaAttributes') - : $this->setRelation('metaAttributes', (new Attribute)->newBag()); - } - - /** - * Get allowed meta attributes array. - * - * @return array - */ - public function getAllowedMeta() - { - return (property_exists($this, 'allowedMeta')) ? $this->allowedMeta : []; - } -} diff --git a/src/Metable/Attribute.php b/src/Metable/Attribute.php deleted file mode 100644 index a74ccea..0000000 --- a/src/Metable/Attribute.php +++ /dev/null @@ -1,399 +0,0 @@ - 'json_decode', - 'StdClass' => 'json_decode', - 'DateTime' => 'asDateTime', - Model::class => 'unserialize', - ]; - - /** - * @var array - */ - protected $setterMutators = [ - 'array' => 'json_encode', - 'StdClass' => 'json_encode', - 'DateTime' => 'fromDateTime', - Model::class => 'serialize', - ]; - - /** - * The attributes included in the model's JSON and array form. - * - * @var array - */ - protected $visible = ['meta_key', 'meta_value', 'meta_type', 'meta_group']; - - /** - * Create new attribute instance. - * - * @param string|array $key - * @param mixed $value - */ - public function __construct($key = null, $value = null, $group = null) - { - // default behaviour - if (is_array($key)) { - parent::__construct($key); - } else { - parent::__construct(); - - if (is_string($key)) { - $this->set($key, $value, $group); - } - } - } - - /** - * Boot this model. - * - * @codeCoverageIgnore - * - * @return void - */ - protected static function boot() - { - parent::boot(); - - if (!isset(static::$attributeMutator)) { - if (function_exists('app') && app()->bound('eloquence.mutator')) { - static::$attributeMutator = app('eloquence.mutator'); - } else { - static::$attributeMutator = new Mutator; - } - } - } - - /** - * Set the meta attribute. - * - * @param string $key - * @param mixed $value - * @param string $group - */ - protected function set($key, $value, $group = 'default') - { - $this->setMetaKey($key); - $this->setValue($value); - $this->setMetaGroup($group); - } - - /** - * Create new AttributeBag. - * - * @param array $models - * @return \Sofa\Eloquence\Metable\AttributeBag - */ - public function newBag(array $models = []) - { - return new AttributeBag($models); - } - - /** - * Get the meta attribute value. - * - * @return mixed - */ - public function getValue() - { - if ($this->hasMutator($this->attributes['meta_value'], 'getter', $this->attributes['meta_type'])) { - return $this->mutateValue($this->attributes['meta_value'], 'getter'); - } - - return $this->castValue(); - } - - /** - * Get the meta attribute key. - * - * @return string - */ - public function getMetaKey() - { - return $this->attributes['meta_key']; - } - - /** - * Get the meta attribute group. - * - * @return string - */ - public function getMetaGroup() - { - return $this->attributes['meta_group']; - } - /** - * Cast value to proper type. - * - * @return mixed - */ - protected function castValue() - { - $value = $this->attributes['meta_value']; - - $validTypes = ['boolean', 'integer', 'float', 'double', 'array', 'object', 'null']; - - if (in_array($this->attributes['meta_type'], $validTypes)) { - settype($value, $this->attributes['meta_type']); - } - - return $value; - } - - /** - * Set key of the meta attribute. - * - * @param string $key - * - * @throws \InvalidArgumentException - */ - protected function setMetaKey($key) - { - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $key)) { - throw new InvalidArgumentException("Provided key [{$key}] is not valid variable name."); - } - - $this->attributes['meta_key'] = $key; - } - /** - * Set group of the meta attribute. - * - * @param string $group - * - * @throws \InvalidArgumentException - */ - public function setMetaGroup($group = null) - { - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $group) && $group !== null) { - throw new InvalidArgumentException("Provided group [{$group}] is not valid variable name."); - } - - $this->attributes['meta_group'] = $group; - } - /** - * Set type of the meta attribute. - * - * @param mixed $value - */ - protected function setType($value) - { - $this->attributes['meta_type'] = $this->hasMutator($value, 'setter') - ? $this->getMutatedType($value, 'setter') - : $this->getValueType($value); - } - - /** - * Set value of the meta attribute. - * - * @param mixed $value - * - * @throws \Sofa\Eloquence\Metable\InvalidTypeException - */ - public function setValue($value) - { - $this->setType($value); - - if ($this->hasMutator($value, 'setter')) { - $value = $this->mutateValue($value, 'setter'); - } elseif (!$this->isStringable($value) && !is_null($value)) { - throw new InvalidTypeException( - "Unsupported meta value type [{$this->getValueType($value)}]." - ); - } - - $this->attributes['meta_value'] = $value; - } - - /** - * Mutate attribute value. - * - * @param mixed $value - * @param string $dir - * @return mixed - */ - protected function mutateValue($value, $dir = 'setter') - { - $mutator = $this->getMutator($value, $dir, $this->attributes['meta_type']); - - if (method_exists($this, $mutator)) { - return $this->{$mutator}($value); - } - - return static::$attributeMutator->mutate($value, $mutator); - } - - /** - * Determine whether the value type can be set to string. - * - * @param mixed $value - * @return boolean - */ - protected function isStringable($value) - { - return is_scalar($value); - } - - /** - * Get the value type. - * - * @param mixed $value - * @return string - */ - protected function getValueType($value) - { - $type = is_object($value) ? get_class($value) : gettype($value); - - // use float instead of deprecated double - return ($type == 'double') ? 'float' : $type; - } - - /** - * Get the mutated type. - * - * @param mixed $value - * @param string $dir - * @return string - */ - protected function getMutatedType($value, $dir = 'setter') - { - foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) { - if ($this->getValueType($value) == $mutated || $value instanceof $mutated) { - return $mutated; - } - } - } - - /** - * Determine whether a mutator exists for the value type. - * - * @param mixed $value - * @param string $dir - * @return boolean - */ - protected function hasMutator($value, $dir = 'setter', $type = null) - { - return (bool) $this->getMutator($value, $dir, $type); - } - - /** - * Get mutator for the type. - * - * @param mixed $value - * @param string $dir - * @return string - */ - protected function getMutator($value, $dir = 'setter', $type = null) - { - $type = $type ?: $this->getValueType($value); - - foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) { - if ($type == $mutated || $value instanceof $mutated) { - return $mutator; - } - } - } - - /** - * Allow custom table name for meta attributes via config. - * - * @return string - */ - public function getTable() - { - return isset(static::$customTable) ? static::$customTable : parent::getTable(); - } - - /** - * Set custom table for the meta attributes. Allows doing it only once - * in order to mimic protected behaviour, most likely in the service - * provider, which in turn gets the table name from configuration. - * - * @param string $table - */ - public static function setCustomTable($table) - { - if (!isset(static::$customTable)) { - static::$customTable = $table; - } - } - - /** - * Handle casting value to string. - * - * @return string - */ - public function castToString() - { - if ($this->attributes['meta_type'] == 'array') { - return $this->attributes['meta_value']; - } - - $value = $this->getValue(); - - if ($this->isStringable($value) || is_object($value) && method_exists($value, '__toString')) { - return (string) $value; - } - - return ''; - } - - /** - * Handle dynamic casting to string. - * - * @return string - */ - public function __toString() - { - return $this->castToString(); - } -} diff --git a/src/Metable/AttributeBag.php b/src/Metable/AttributeBag.php deleted file mode 100644 index 9981599..0000000 --- a/src/Metable/AttributeBag.php +++ /dev/null @@ -1,247 +0,0 @@ -add($attribute); - } - } - - /** - * Add or update attribute. - * - * @param \Sofa\Eloquence\Metable\Attribute|string $key - * @param mixed $value - * @return $this - */ - public function set($key, $value = null, $group = null) - { - if ($key instanceof Attribute) { - return $this->setInstance($key); - } - - if ($this->has($key)) { - $this->update($key, $value, $group); - } else { - $this->items[$key] = $this->newAttribute($key, $value, $group); - } - - return $this; - } - - /** - * Set attribute. - * - * @param \Sofa\Eloquence\Metable\Attribute $attribute - */ - protected function setInstance(Attribute $attribute) - { - if ($this->has($attribute->getMetaKey())) { - $this->update($attribute); - } else { - $this->items[$attribute->getMetaKey()] = $attribute; - } - - return $this; - } - - /** - * Set attribute. - * - * @param \Sofa\Eloquence\Metable\Attribute $attribute - */ - public function add($attribute) - { - return $this->addInstance($attribute); - } - - /** - * Set attribute. - * - * @param \Sofa\Eloquence\Metable\Attribute $attribute - */ - protected function addInstance(Attribute $attribute) - { - return $this->set($attribute); - } - - /** - * Update existing attribute. - * - * @param \Sofa\Eloquence\Metable\Attribute|string $key - * @param mixed $value - * @return $this - */ - protected function update($key, $value = null, $group = null) - { - if ($key instanceof Attribute) { - $value = $key->getValue(); - $group = $key->getMetaGroup(); - $key = $key->getMetaKey(); - } - - $this->get($key)->setValue($value); - $this->get($key)->setMetaGroup($group); - - return $this; - } - - /** - * New attribute instance. - * - * @param string $key - * @param mixed $value - * @return \Sofa\Eloquence\Metable\Attribute - */ - protected function newAttribute($key, $value, $group = null) - { - return new Attribute($key, $value, $group); - } - - /** - * Get attribute value. - * - * @param string $key - * @return mixed - */ - public function getValue($key) - { - if ($attribute = $this->get($key)) { - return $attribute->getValue(); - } - } - /** - * Get attribute values by group. - * - * @param string $group - * @return mixed - */ - public function getMetaByGroup($group) - { - return $this->where('meta_group', $group); - } - /** - * Get collection as key-value array. - * - * @return array - */ - public function toArray() - { - return array_filter(array_map(function ($attribute) { - return $attribute->getValue(); - }, $this->items)); - } - - /** - * Unset attribute. - * - * @param string $key - * @return $this - */ - public function forget($key) - { - if ($attribute = $this->get($key)) { - $attribute->setValue(null); - } - - return $this; - } - - /** - * Set attribute. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function offsetSet($key, $value) - { - $this->set($key, $value); - } - - /** - * Set attribute to null. - * - * @param string $key - * @return void - */ - public function offsetUnset($key) - { - $this->forget($key); - } - - /** - * Handle dynamic properties. - * - * @param string $key - * @param mixed $value - */ - public function __set($key, $value) - { - $this->set($key, $value); - } - - /** - * Handle dynamic properties. - * - * @param string $key - * @return mixed - */ - public function __get($key) - { - return $this->getValue($key); - } - - /** - * Handle isset calls. - * - * @param string $key - * @return boolean - */ - public function __isset($key) - { - return (bool) $this->get($key); - } - - /** - * Handle unset calls. - * - * @param string $key - * @return void - */ - public function __unset($key) - { - $this->forget($key); - } - - /** - * Create copy of the attribute bag. - * - * @return static - */ - public function replicate($except = null) - { - $except = $except ? array_combine($except, $except) : []; - - $attributes = []; - - foreach (array_diff_key($this->items, $except) as $attribute) { - $attributes[] = $attribute->replicate(); - } - - return new static($attributes); - } -} diff --git a/src/Metable/CreateMetaAttributesTable.php b/src/Metable/CreateMetaAttributesTable.php deleted file mode 100644 index 6c498ee..0000000 --- a/src/Metable/CreateMetaAttributesTable.php +++ /dev/null @@ -1,55 +0,0 @@ -table, function (Blueprint $table) { - $table->increments('meta_id'); - $table->string('meta_key'); - $table->longText('meta_value'); - $table->string('meta_type')->default('string'); - $table->string('meta_group')->nullable(); - $table->morphs('metable'); - - $table->index('meta_key'); - - // Laravel doesn't handle index length, so we need raw statement for this one - \Schema::getConnection()->statement( - 'create index meta_attributes_index_value on meta_attributes (meta_key, meta_value(20))' - ); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - \Schema::drop($this->table); - } -} diff --git a/src/Metable/Hooks.php b/src/Metable/Hooks.php deleted file mode 100644 index 594a213..0000000 --- a/src/Metable/Hooks.php +++ /dev/null @@ -1,165 +0,0 @@ -get('key'); - - if (is_null($value)) { - $value = $this->getMeta($key); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on setAttribute method. - * - * @return \Closure - */ - public function setAttribute() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if (!$this->hasColumn($key) && $this->allowsMeta($key) && !$this->hasSetMutator($key)) { - return $this->setMeta($key, $value); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on toArray method. - * - * @return \Closure - */ - public function toArray() - { - return function ($next, $attributes) { - unset($attributes['meta_attributes'], $attributes['metaAttributes']); - - $attributes = array_merge($attributes, $this->getMetaAttributesArray()); - - return $next($attributes); - }; - } - - /** - * Register hook on replicate method. - * - * @return \Closure - */ - public function replicate() - { - return function ($next, $copy, $args) { - $metaAttributes = $args->get('original') - ->getMetaAttributes() - ->replicate($args->get('except')); - - $copy->setRelation('metaAttributes', $metaAttributes); - - return $next($copy, $args); - }; - } - - /** - * Register hook on save method. - * - * @return \Closure - */ - public function save() - { - return function ($next, $value, $args) { - $this->saveMeta(); - - return $next($value, $args); - }; - } - - /** - * Register hook on isset call. - * - * @return \Closure - */ - public function __issetHook() - { - return function ($next, $isset, $args) { - $key = $args->get('key'); - - if (!$isset) { - $isset = (bool) $this->hasMeta($key); - } - - return $next($isset, $args); - }; - } - - /** - * Register hook on unset call. - * - * @return \Closure - */ - public function __unsetHook() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if ($this->hasMeta($key)) { - return $this->setMeta($key, null); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on queryHook method. - * - * @return \Closure - */ - public function queryHook() - { - return function ($next, $query, $bag) { - $method = $bag->get('method'); - $args = $bag->get('args'); - $column = $args->get('column'); - - if (!$this->hasColumn($column) && $this->allowsMeta($column) && $this->isMetaQueryable($method)) { - return call_user_func_array([$this, 'metaQuery'], [$query, $method, $args]); - } - - if (in_array($method, ['select', 'addSelect'])) { - call_user_func_array([$this, 'metaSelect'], [$query, $args]); - } - - return $next($query, $bag); - }; - } - - public function __call($method, $params) - { - if (strpos($method, '__') === 0 && method_exists($this, $method.'Hook')) { - return call_user_func_array([$this, $method.'Hook'], $params); - } - - throw new BadMethodCallException("Method [{$method}] doesn't exist on this object."); - } -} diff --git a/src/Metable/InvalidMutatorException.php b/src/Metable/InvalidMutatorException.php deleted file mode 100644 index 5c5633b..0000000 --- a/src/Metable/InvalidMutatorException.php +++ /dev/null @@ -1,8 +0,0 @@ -{$method}()); - } - } - - /** - * Mutate mutable attributes for array conversion. - * - * @param array $attributes - * @return array - */ - protected function mutableAttributesToArray(array $attributes) - { - foreach ($attributes as $key => $value) { - if ($this->hasGetterMutator($key)) { - $attributes[$key] = $this->mutableMutate($key, $value, 'getter'); - } - } - - return $attributes; - } - - /** - * Determine whether an attribute has getter mutators defined. - * - * @param string $key - * @return boolean - */ - public function hasGetterMutator($key) - { - return array_key_exists($key, $this->getMutators('getter')); - } - - /** - * Determine whether an attribute has setter mutators defined. - * - * @param string $key - * @return boolean - */ - public function hasSetterMutator($key) - { - return array_key_exists($key, $this->getMutators('setter')); - } - - /** - * Mutate the attribute. - * - * @param string $key - * @param string $value - * @param string $dir - * @return mixed - */ - protected function mutableMutate($key, $value, $dir) - { - $mutators = $this->getMutatorsForAttribute($key, $dir); - - return static::$attributeMutator->mutate($value, $mutators); - } - - /** - * Get the mutators for an attribute. - * - * @param string $key - * @return string - */ - protected function getMutatorsForAttribute($key, $dir) - { - return $this->getMutators($dir)[$key]; - } - - /** - * Get the array of attribute mutators. - * - * @param string $dir - * @return array - */ - public function getMutators($dir) - { - $property = ($dir === 'setter') ? 'setterMutators' : 'getterMutators'; - - return (property_exists($this, $property)) ? $this->{$property} : []; - } -} diff --git a/src/Mutable/Hooks.php b/src/Mutable/Hooks.php deleted file mode 100644 index f9c8534..0000000 --- a/src/Mutable/Hooks.php +++ /dev/null @@ -1,60 +0,0 @@ -get('key'); - - if ($this->hasGetterMutator($key)) { - $value = $this->mutableMutate($key, $value, 'getter'); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on setAttribute method. - * - * @return \Closure - */ - public function setAttribute() - { - return function ($next, $value, $args) { - $key = $args->get('key'); - - if ($this->hasSetterMutator($key)) { - $value = $this->mutableMutate($key, $value, 'setter'); - } - - return $next($value, $args); - }; - } - - /** - * Register hook on toArray method. - * - * @return \Closure - */ - public function toArray() - { - return function ($next, $attributes) { - $attributes = $this->mutableAttributesToArray($attributes); - - return $next($attributes); - }; - } -} diff --git a/src/Mutator/InvalidCallableException.php b/src/Mutator/InvalidCallableException.php deleted file mode 100644 index a8cfcf7..0000000 --- a/src/Mutator/InvalidCallableException.php +++ /dev/null @@ -1,8 +0,0 @@ -parse(trim($callable)); - - $value = call_user_func_array($callable, array_merge([$value], $args)); - } - - return $value; - } - - /** - * Parse provided mutator functions. - * - * @param string $callable - * @return array - * - * @throws \Sofa\Eloquence\Mutator\InvalidCallableException - */ - protected function parse($callable) - { - list($callable, $args) = $this->parseArgs($callable); - - if ($this->isClassMethod($callable)) { - $callable = $this->parseClassMethod($callable); - } elseif ($this->isMutatorMethod($callable)) { - $callable = [$this, $callable]; - } elseif (!function_exists($callable)) { - throw new InvalidCallableException("Function [{$callable}] not found."); - } - - return [$callable, $args]; - } - - /** - * Determine whether callable is a class method. - * - * @param string $callable - * @return boolean - */ - protected function isClassMethod($callable) - { - return strpos($callable, '@') !== false; - } - - /** - * Determine whether callable is available on this instance. - * - * @param string $callable - * @return boolean - */ - protected function isMutatorMethod($callable) - { - return method_exists($this, $callable) || static::hasMacro($callable); - } - - /** - * Split provided string into callable and arguments. - * - * @param string $callable - * @return array - */ - protected function parseArgs($callable) - { - $args = []; - - if (strpos($callable, ':') !== false) { - list($callable, $argsString) = explode(':', $callable); - - $args = explode(',', $argsString); - } - - return [$callable, $args]; - } - - /** - * Extract and validate class method. - * - * @param string $userCallable - * @return callable - * - * @throws \Sofa\Eloquence\Mutator\InvalidCallableException - */ - protected function parseClassMethod($userCallable) - { - list($class) = explode('@', $userCallable); - - $callable = str_replace('@', '::', $userCallable); - - try { - $method = new ReflectionMethod($callable); - - $class = new ReflectionClass($class); - } catch (ReflectionException $e) { - throw new InvalidCallableException($e->getMessage()); - } - - return ($method->isStatic()) ? $callable : $this->getInstanceMethod($class, $method); - } - - /** - * Get instance callable. - * - * @param \ReflectionMethod $method - * @return callable - * - * @throws \Sofa\Eloquence\Mutator\InvalidCallableException - */ - protected function getInstanceMethod(ReflectionClass $class, ReflectionMethod $method) - { - if (!$method->isPublic()) { - throw new InvalidCallableException("Instance method [{$class}@{$method->getName()}] is not public."); - } - - if (!$this->canInstantiate($class)) { - throw new InvalidCallableException("Can't instantiate class [{$class->getName()}]."); - } - - return [$class->newInstance(), $method->getName()]; - } - - /** - * Determine whether instance can be instantiated. - * - * @param \ReflectionClass $class - * @return boolean - */ - protected function canInstantiate(ReflectionClass $class) - { - if (!$class->isInstantiable()) { - return false; - } - - $constructor = $class->getConstructor(); - - return is_null($constructor) || 0 === $constructor->getNumberOfRequiredParameters(); - } -} diff --git a/src/Query/Builder.php b/src/Query/Builder.php deleted file mode 100644 index f461e48..0000000 --- a/src/Query/Builder.php +++ /dev/null @@ -1,106 +0,0 @@ -aggregate = compact('function', 'columns'); - - $previousColumns = $this->columns; - - if (!$this->from instanceof Subquery) { - // We will also back up the select bindings since the select clause will be - // removed when performing the aggregate function. Once the query is run - // we will add the bindings back onto this query so they can get used. - $previousSelectBindings = $this->bindings['select']; - - $this->bindings['select'] = []; - } - - $results = $this->get($columns); - - // Once we have executed the query, we will reset the aggregate property so - // that more select queries can be executed against the database without - // the aggregate value getting in the way when the grammar builds it. - $this->aggregate = null; - - $this->columns = $previousColumns; - - if (!$this->from instanceof Subquery) { - $this->bindings['select'] = $previousSelectBindings; - } - - if (isset($results[0])) { - $result = array_change_key_case((array) $results[0]); - - return $result['aggregate']; - } - } - - /** - * Backup some fields for the pagination count. - * - * @return void - */ - protected function backupFieldsForCount() - { - foreach (['orders', 'limit', 'offset', 'columns'] as $field) { - $this->backups[$field] = $this->{$field}; - - $this->{$field} = null; - } - - $bindings = ($this->from instanceof Subquery) ? ['order'] : ['order', 'select']; - - foreach ($bindings as $key) { - $this->bindingBackups[$key] = $this->bindings[$key]; - - $this->bindings[$key] = []; - } - } - - /** - * Restore some fields after the pagination count. - * - * @return void - */ - protected function restoreFieldsForCount() - { - foreach ($this->backups as $field => $value) { - $this->{$field} = $value; - } - - foreach ($this->bindingBackups as $key => $value) { - $this->bindings[$key] = $value; - } - - $this->backups = $this->bindingBackups = []; - } - - /** - * Run a pagination count query. - * - * @param array $columns - * @return array - */ - protected function runPaginationCountQuery($columns = ['*']) - { - $bindings = $this->from instanceof Subquery ? ['order'] : ['select', 'order']; - - return $this->cloneWithout(['columns', 'orders', 'limit', 'offset']) - ->cloneWithoutBindings($bindings) - ->setAggregate('count', $this->withoutSelectAliases($columns)) - ->get()->all(); - } -} diff --git a/src/Relations/Joiner.php b/src/Relations/Joiner.php deleted file mode 100644 index 7d5501e..0000000 --- a/src/Relations/Joiner.php +++ /dev/null @@ -1,201 +0,0 @@ -query = $query; - $this->model = $model; - } - - /** - * Join related tables. - * - * @param string $target - * @param string $type - * @return \Illuminate\Database\Eloquent\Model - */ - public function join($target, $type = 'inner') - { - $related = $this->model; - - foreach (explode('.', $target) as $segment) { - $related = $this->joinSegment($related, $segment, $type); - } - - return $related; - } - - /** - * Left join related tables. - * - * @param string $target - * @return \Illuminate\Database\Eloquent\Model - */ - public function leftJoin($target) - { - return $this->join($target, 'left'); - } - - /** - * Right join related tables. - * - * @param string $target - * @return \Illuminate\Database\Eloquent\Model - */ - public function rightJoin($target) - { - return $this->join($target, 'right'); - } - - /** - * Join relation's table accordingly. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @param string $segment - * @param string $type - * @return \Illuminate\Database\Eloquent\Model - */ - protected function joinSegment(Model $parent, $segment, $type) - { - $relation = $parent->{$segment}(); - $related = $relation->getRelated(); - $table = $related->getTable(); - - if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) { - $this->joinIntermediate($parent, $relation, $type); - } - - if (!$this->alreadyJoined($join = $this->getJoinClause($parent, $relation, $table, $type))) { - $this->query->joins[] = $join; - } - - return $related; - } - - /** - * Determine whether the related table has been already joined. - * - * @param \Illuminate\Database\Query\JoinClause $join - * @return boolean - */ - protected function alreadyJoined(Join $join) - { - return in_array($join, (array) $this->query->joins); - } - - /** - * Get the join clause for related table. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @param string $type - * @param string $table - * @return \Illuminate\Database\Query\JoinClause - */ - protected function getJoinClause(Model $parent, Relation $relation, $table, $type) - { - list($fk, $pk) = $this->getJoinKeys($relation); - - $join = (new Join($this->query, $type, $table))->on($fk, '=', $pk); - - if ($relation instanceof MorphOneOrMany) { - $join->where($relation->getQualifiedMorphType(), '=', $parent->getMorphClass()); - } - - return $join; - } - - /** - * Join pivot or 'through' table. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @param string $type - * @return void - */ - protected function joinIntermediate(Model $parent, Relation $relation, $type) - { - if ($relation instanceof BelongsToMany) { - $table = $relation->getTable(); - $fk = $relation->getQualifiedForeignPivotKeyName(); - } else { - $table = $relation->getParent()->getTable(); - $fk = $relation->getQualifiedFirstKeyName(); - } - - $pk = $parent->getQualifiedKeyName(); - - if (!$this->alreadyJoined($join = (new Join($this->query, $type, $table))->on($fk, '=', $pk))) { - $this->query->joins[] = $join; - } - } - - /** - * Get pair of the keys from relation in order to join the table. - * - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @return array - * - * @throws \LogicException - */ - protected function getJoinKeys(Relation $relation) - { - if ($relation instanceof MorphTo) { - throw new LogicException("MorphTo relation cannot be joined."); - } - - if ($relation instanceof HasOneOrMany) { - return [$relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName()]; - } - - if ($relation instanceof BelongsTo) { - return [$relation->getQualifiedForeignKey(), $relation->getQualifiedOwnerKeyName()]; - } - - if ($relation instanceof BelongsToMany) { - return [$relation->getQualifiedRelatedPivotKeyName(), $relation->getRelated()->getQualifiedKeyName()]; - } - - if ($relation instanceof HasManyThrough) { - $fk = $relation->getQualifiedFarKeyName(); - - return [$fk, $relation->getParent()->getQualifiedKeyName()]; - } - } -} diff --git a/src/Relations/JoinerFactory.php b/src/Relations/JoinerFactory.php deleted file mode 100644 index 019b8ee..0000000 --- a/src/Relations/JoinerFactory.php +++ /dev/null @@ -1,27 +0,0 @@ -getModel(); - $query = $query->getQuery(); - } - - return new Joiner($query, $model); - } -} diff --git a/src/Searchable/Column.php b/src/Searchable/Column.php deleted file mode 100644 index 08b966b..0000000 --- a/src/Searchable/Column.php +++ /dev/null @@ -1,92 +0,0 @@ -grammar = $grammar; - $this->table = $table; - $this->name = $name; - $this->mapping = $mapping; - $this->weight = $weight; - } - - /** - * Get qualified name wrapped by the grammar. - * - * @return string - */ - public function getWrapped() - { - return $this->grammar->wrap($this->getQualifiedName()); - } - - /** - * Get column name with table prefix. - * - * @return string - */ - public function getQualifiedName() - { - return $this->getTable().'.'.$this->getName(); - } - - /** - * @return string - */ - public function getTable() - { - return $this->table; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return string - */ - public function getMapping() - { - return $this->mapping; - } - - /** - * @return integer - */ - public function getWeight() - { - return $this->weight; - } -} diff --git a/src/Searchable/ColumnCollection.php b/src/Searchable/ColumnCollection.php deleted file mode 100644 index e81386f..0000000 --- a/src/Searchable/ColumnCollection.php +++ /dev/null @@ -1,152 +0,0 @@ -add($column); - } - } - - /** - * Get columns as plain array. - * - * @return array - */ - public function getColumns() - { - return $this->columns; - } - - /** - * Add column to the collection. - * - * @param \Sofa\Eloquence\Searchable\Column $column - */ - public function add(Column $column) - { - $this->columns[$column->getMapping()] = $column; - } - - /** - * Get array of qualified columns names. - * - * @return array - */ - public function getQualifiedNames() - { - return array_map(function ($column) { - return $column->getQualifiedName(); - }, $this->columns); - } - - /** - * Get array of tables names. - * - * @return array - */ - public function getTables() - { - return array_unique(array_map(function ($column) { - return $column->getTable(); - }, $this->columns)); - } - - /** - * Get array of columns mappings and weights. - * - * @return array - */ - public function getWeights() - { - $weights = []; - - foreach ($this->columns as $column) { - $weights[$column->getMapping()] = $column->getWeight(); - } - - return $weights; - } - - /** - * Get array of columns mappings. - * - * @return array - */ - public function getMappings() - { - return array_map(function ($column) { - return $column->getMapping(); - }, $this->columns); - } - - /** - * Check if element exists at given offset. - * - * @param string $key - * @return boolean - */ - public function offsetExists($key) - { - return array_key_exists($key, $this->columns); - } - - /** - * Get element at given offset. - * - * @param string $key - * @return \Sofa\Eloquence\Searchable\Column - */ - public function offsetGet($key) - { - return $this->columns[$key]; - } - - /** - * Set element at given offset. - * - * @param string $key [description] - * @param \Sofa\Eloquence\Searchable\Column $column - * @return void - */ - public function offsetSet($key, $column) - { - $this->add($column); - } - - /** - * Unset element at given offset. - * - * @param string $key - * @return \Sofa\Eloquence\Searchable\Column - */ - public function offsetUnset($key) - { - unset($this->columns[$key]); - } - - /** - * Get an iterator for the columns. - * - * @return \ArrayIterator - */ - public function getIterator() - { - return new ArrayIterator($this->columns); - } -} diff --git a/src/Searchable/Parser.php b/src/Searchable/Parser.php deleted file mode 100644 index 1d47a8e..0000000 --- a/src/Searchable/Parser.php +++ /dev/null @@ -1,124 +0,0 @@ -weight = $weight; - $this->wildcard = $wildcard; - } - - /** - * Parse searchable columns. - * - * @param array|string $columns - * @return array - */ - public function parseWeights($columns) - { - if (is_string($columns)) { - $columns = func_get_args(); - } - - return $this->addMissingWeights($columns); - } - - /** - * Add search weight to the columns if missing. - * - * @param array $columns - */ - protected function addMissingWeights(array $columns) - { - $parsed = []; - - foreach ($columns as $column => $weight) { - if (is_numeric($column)) { - list($column, $weight) = [$weight, $this->weight]; - } - - $parsed[$column] = $weight; - } - - return $parsed; - } - - /** - * Strip wildcard tokens from the word. - * - * @param string $word - * @return string - */ - public function stripWildcards($word) - { - return str_replace($this->wildcard, '%', trim($word, $this->wildcard)); - } - - /** - * Parse query string into separate words with wildcards if applicable. - * - * @param string $query - * @param boolean $fulltext - * @return array - */ - public function parseQuery($query, $fulltext = true) - { - $words = $this->splitString($query); - - if ($fulltext) { - $words = $this->addWildcards($words); - } - - return $words; - } - - /** - * Split query string into words/phrases to be searched. - * - * @param string $query - * @return array - */ - protected function splitString($query) - { - preg_match_all('/(?<=")[\w ][^"]+(?=")|(?<=\s|^)[^\s"]+(?=\s|$)/u', $query, $matches); - - return reset($matches); - } - - /** - * Add wildcard tokens to the words. - * - * @param array $words - */ - protected function addWildcards(array $words) - { - $token = $this->wildcard; - - return array_map(function ($word) use ($token) { - return preg_replace('/\*+/', '*', "{$token}{$word}{$token}"); - }, $words); - } -} diff --git a/src/Searchable/ParserFactory.php b/src/Searchable/ParserFactory.php deleted file mode 100644 index bdbf736..0000000 --- a/src/Searchable/ParserFactory.php +++ /dev/null @@ -1,20 +0,0 @@ -registerMutator(); - $this->registerJoiner(); - $this->registerParser(); - } - - /** - * Register attribute mutator service. - * - * @return void - */ - protected function registerMutator() - { - $this->app->singleton('eloquence.mutator', function () { - return new Mutator; - }); - - $this->app->alias('eloquence.mutator', 'Sofa\Eloquence\Contracts\Mutator'); - } - - /** - * Register relation joiner factory. - * - * @return void - */ - protected function registerJoiner() - { - $this->app->singleton('eloquence.joiner', function () { - return new JoinerFactory; - }); - - $this->app->alias('eloquence.joiner', 'Sofa\Eloquence\Contracts\Relations\JoinerFactory'); - } - - /** - * Register serachable parser factory. - * - * @return void - */ - protected function registerParser() - { - $this->app->singleton('eloquence.parser', function () { - return new ParserFactory; - }); - - $this->app->alias('eloquence.parser', 'Sofa\Eloquence\Contracts\Relations\ParserFactory'); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return ['eloquence.mutator', 'eloquence.joiner', 'eloquence.parser']; - } -} diff --git a/src/Subquery.php b/src/Subquery.php deleted file mode 100644 index 872d74f..0000000 --- a/src/Subquery.php +++ /dev/null @@ -1,137 +0,0 @@ -getQuery(); - } - - $this->setQuery($query); - - $this->alias = $alias; - } - - /** - * Set underlying query builder. - * - * @param \Illuminate\Database\Query\Builder $query - */ - public function setQuery(QueryBuilder $query) - { - $this->query = $query; - } - - /** - * Get underlying query builder. - * - * @return \Illuminate\Database\Query\Builder - */ - public function getQuery() - { - return $this->query; - } - - /** - * Evaluate query as string. - * - * @return string - */ - public function getValue() - { - $sql = '('.$this->query->toSql().')'; - - if ($this->alias) { - $alias = $this->query->getGrammar()->wrapTable($this->alias); - - $sql .= ' as '.$alias; - } - - return $sql; - } - - /** - * Get subquery alias. - * - * @return string - */ - public function getAlias() - { - return $this->alias; - } - - /** - * Set subquery alias. - * - * @param string $alias - * @return $this - */ - public function setAlias($alias) - { - $this->alias = $alias; - - return $this; - } - - /** - * Pass property calls to the underlying builder. - * - * @param string $property - * @param mixed $value - * @return mixed - */ - public function __set($property, $value) - { - return $this->query->{$property} = $value; - } - - /** - * Pass property calls to the underlying builder. - * - * @param string $property - * @return mixed - */ - public function __get($property) - { - return $this->query->{$property}; - } - - /** - * Pass method calls to the underlying builder. - * - * @param string $method - * @param array $params - * @return mixed - */ - public function __call($method, $params) - { - return call_user_func_array([$this->query, $method], $params); - } -} diff --git a/src/Validable.php b/src/Validable.php deleted file mode 100644 index c72a730..0000000 --- a/src/Validable.php +++ /dev/null @@ -1,330 +0,0 @@ -bound('validator')) { - static::setValidatorFactory(app('validator')); - } - } - } - - /** - * Determine whether all the attributes on this instance pass validation. - * - * @return boolean - */ - public function isValid() - { - $this->getValidator()->setData($this->getAttributes()); - - return $this->getValidator()->passes(); - } - - /** - * Skip validation on the next saving attempt. - * - * @return $this - */ - public function skipValidation() - { - return $this->disableValidation($once = true); - } - - /** - * Disable validation for this instance. - * - * @return $this - */ - public function disableValidation($once = false) - { - $this->skipValidation = ($once) ? Observer::SKIP_ONCE : Observer::SKIP_ALWAYS; - - return $this; - } - - /** - * Enable validation for this instance. - * - * @return $this - */ - public function enableValidation() - { - $this->skipValidation = false; - - return $this; - } - - /** - * Get current validation flag. - * - * @return integer|false - */ - public function skipsValidation() - { - return $this->skipValidation; - } - - /** - * Determine whether validation is enabled for this instance. - * - * @return boolean - */ - public function validationEnabled() - { - return !$this->skipsValidation(); - } - - /** - * Retrieve validation error messages. - * - * @return \Illuminate\Support\MessageBag - */ - public function getValidationErrors() - { - return $this->getMessageBag(); - } - - /** - * Retrieve validation error messages. - * - * @return \Illuminate\Support\MessageBag - */ - public function getMessageBag() - { - return $this->getValidator()->getMessageBag(); - } - - /** - * Get names of the attributes that didn't pass validation. - * - * @return array - */ - public function getInvalidAttributes() - { - return array_keys($this->getValidationErrors()->toArray()); - } - - /** - * Get the validator instance. - * - * @return \Illuminate\Contracts\Validation\Validator - */ - public function getValidator() - { - if (!$this->validator) { - $this->validator = static::$validatorFactory->make( - [], - static::getCreateRules(), - static::getValidationMessages(), - static::getValidationAttributes() - ); - } - - return $this->validator; - } - - /** - * Get custom validation messages. - * - * @return array - */ - public static function getValidationMessages() - { - return (property_exists(get_called_class(), 'validationMessages')) - ? static::$validationMessages - : []; - } - - /** - * Get custom validation attribute names. - * - * @return array - */ - public static function getValidationAttributes() - { - return (property_exists(get_called_class(), 'validationAttributes')) - ? static::$validationAttributes - : []; - } - - /** - * Get all the validation rules for this model. - * - * @return array - */ - public static function getCreateRules() - { - if (!static::$rulesMerged) { - static::$rulesMerged = static::gatherRules(); - } - - return static::$rulesMerged; - } - - /** - * Gather all the rules for the model and store it for easier use. - * - * @return array - */ - protected static function gatherRules() - { - // This rather gnarly looking logic is just for developer convenience - // so he can define multiple rule groups on the model for clarity - // and now we simply gather all rules and merge them together. - $keys = static::getValidatedFields(); - - $result = array_fill_keys($keys, []); - - foreach ($keys as $key) { - foreach (static::getRulesGroups() as $groupName) { - $group = static::getRulesGroup($groupName); - - if (isset($group[$key])) { - $rules = is_array($group[$key]) - ? $group[$key] - : explode('|', $group[$key]); - - foreach ($rules as &$rule) { - if ($rule === 'unique') { - $table = (new static)->getTable(); - - $rule .= ":{$table}"; - } - } - - unset($rule); - - $result[$key] = array_unique(array_merge($result[$key], $rules)); - } - } - } - - return $result; - } - - /** - * Get all validation rules for update on this model. - * - * @return array - */ - public function getUpdateRules() - { - return ($this->getKey()) ? static::getUpdateRulesForId($this) : static::getCreateRules(); - } - - /** - * Get all validation rules for update for given id. - * - * @param \Illuminate\Database\Eloquent\Model|integer|string $id - * @return array - */ - public static function getUpdateRulesForId($id) - { - return rules_for_update(static::getCreateRules(), $id, (new static)->getKeyName()); - } - - /** - * Get array of attributes that have validation rules defined. - * - * @return array - */ - public static function getValidatedFields() - { - $fields = []; - - foreach (static::getRulesGroups() as $groupName) { - $fields = array_merge($fields, array_keys(static::getRulesGroup($groupName))); - } - - return array_values(array_unique($fields)); - } - - /** - * Get all the rules groups defined on this model. - * - * @return array - */ - protected static function getRulesGroups() - { - $groups = []; - - foreach (get_class_vars(get_called_class()) as $property => $val) { - if (preg_match('/^.*rules$/i', $property)) { - $groups[] = $property; - } - } - - return $groups; - } - - /** - * Get rules from given group. - * - * @param string $group - * @return array - */ - protected static function getRulesGroup($group) - { - return static::$$group; - } - - /** - * Set validation factory instance for this model. - * - * @param \Illuminate\Contracts\Validation\Factory - * @return void - */ - public static function setValidatorFactory(Factory $factory) - { - static::$validatorFactory = $factory; - } -} diff --git a/src/Validable/Observer.php b/src/Validable/Observer.php deleted file mode 100644 index 8eef4ab..0000000 --- a/src/Validable/Observer.php +++ /dev/null @@ -1,83 +0,0 @@ -validationEnabled() && !$model->isValid()) { - return false; - } - } - - /** - * Updating event handler. - * - * @param \Sofa\Eloquence\Contracts\Validable $model - * @return void|false - */ - public function updating(Validable $model) - { - if ($model->validationEnabled()) { - return $this->validateUpdate($model); - } - } - - /** - * Halt updating if model doesn't pass validation. - * - * @param \Sofa\Eloquence\Contracts\Validable $model - * @return void|false - */ - protected function validateUpdate(Validable $model) - { - // When we are trying to update this model we need to set the update rules - // on the validator first, next we can determine if the model is valid, - // finally we restore original rules and notify in case of failure. - $model->getValidator()->setRules($model->getUpdateRules()); - - $valid = $model->isValid(); - - $model->getValidator()->setRules($model->getCreateRules()); - - if (!$valid) { - return false; - } - } - - /** - * Enable validation for the model if skipped only once. - * - * @param \Sofa\Eloquence\Contracts\Validable $model - * @return void - */ - public function saved(Validable $model) - { - if ($model->skipsValidation() === static::SKIP_ONCE) { - $model->enableValidation(); - } - } -} diff --git a/src/helpers.php b/src/helpers.php deleted file mode 100644 index 0bf6caf..0000000 --- a/src/helpers.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ - -use Illuminate\Database\Eloquent\Model; - -if (!function_exists('rules_for_update')) { - /** - * Adjust unique rules for update so it doesn't treat updated model's row as duplicate. - * - * @link http://laravel.com/docs/5.0/validation#rule-unique - * - * @param array $rules - * @param \Illuminate\Database\Eloquent\Model|integer|string $id - * @param string $primaryKey - * @return array - */ - function rules_for_update(array $rules, $id, $primaryKey = 'id') - { - if ($id instanceof Model) { - list($primaryKey, $id) = [$id->getKeyName(), $id->getKey()]; - } - - // We want to update each unique rule so it ignores this model's row - // during unique check in order to avoid faulty non-unique errors - // in accordance to the linked Laravel Validator documentation. - array_walk($rules, function (&$fieldRules, $field) use ($id, $primaryKey) { - if (is_string($fieldRules)) { - $fieldRules = explode('|', $fieldRules); - } - - array_walk($fieldRules, function (&$rule) use ($field, $id, $primaryKey) { - if (strpos($rule, 'unique') === false) { - return; - } - - list(,$argsString) = explode(':', $rule); - - $args = explode(',', $argsString); - - $args[1] = isset($args[1]) ? $args[1] : $field; - $args[2] = $id; - $args[3] = $primaryKey; - - $rule = 'unique:'.implode(',', $args); - }); - }); - - return $rules; - } -}