File "Builder.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/fluentform/vendor/wpfluent/framework/src/WPFluent/Database/Orm/Builder.php
File size: 40.36 KB
MIME-type: text/x-php
Charset: utf-8
<?php
namespace FluentForm\Framework\Database\Orm;
use Closure;
use FluentForm\Framework\Support\Arr;
use FluentForm\Framework\Support\Str;
use FluentForm\Framework\Foundation\App;
use FluentForm\Framework\Pagination\Paginator;
use FluentForm\Framework\Database\Query\Expression;
use FluentForm\Framework\Pagination\LengthAwarePaginator;
use FluentForm\Framework\Database\Orm\Relations\Relation;
use FluentForm\Framework\Database\Orm\ModelNotFoundException;
use FluentForm\Framework\Database\Query\Builder as QueryBuilder;
class Builder
{
/**
* The base query builder instance.
*
* @var \FluentForm\Framework\Database\Query\Builder
*/
protected $query;
/**
* The model being queried.
*
* @var \FluentForm\Framework\Database\Orm\Model
*/
protected $model;
/**
* The relationships that should be eager loaded.
*
* @var array
*/
protected $eagerLoad = [];
/**
* All of the registered builder macros.
*
* @var array
*/
protected $macros = [];
/**
* A replacement for the typical delete function.
*
* @var \Closure
*/
protected $onDelete;
/**
* The methods that should be returned from query builder.
*
* @var array
*/
protected $passthru = [
'insert', 'insertGetId', 'getBindings', 'toSql',
'exists', 'count', 'min', 'max', 'avg', 'sum', 'getConnection',
];
/**
* Applied global scopes.
*
* @var array
*/
protected $scopes = [];
/**
* Removed global scopes.
*
* @var array
*/
protected $removedScopes = [];
/**
* Create a new Eloquent query builder instance.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @return void
*/
public function __construct(QueryBuilder $query)
{
$this->query = $query;
}
/**
* Register a new global scope.
*
* @param string $identifier
* @param \FluentForm\Framework\Database\Orm\Scope|\Closure $scope
* @return $this
*/
public function withGlobalScope($identifier, $scope)
{
$this->scopes[$identifier] = $scope;
if (method_exists($scope, 'extend')) {
$scope->extend($this);
}
return $this;
}
/**
* Remove a registered global scope.
*
* @param \FluentForm\Framework\Database\Orm\Scope|string $scope
* @return $this
*/
public function withoutGlobalScope($scope)
{
if (! is_string($scope)) {
$scope = get_class($scope);
}
unset($this->scopes[$scope]);
$this->removedScopes[] = $scope;
return $this;
}
/**
* Remove all or passed registered global scopes.
*
* @param array|null $scopes
* @return $this
*/
public function withoutGlobalScopes(array $scopes = null)
{
if (is_array($scopes)) {
foreach ($scopes as $scope) {
$this->withoutGlobalScope($scope);
}
} else {
$this->scopes = [];
}
return $this;
}
/**
* Get an array of global scopes that were removed from the query.
*
* @return array
*/
public function removedScopes()
{
return $this->removedScopes;
}
/**
* Find a model by its primary key.
*
* @param mixed $id
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model|\FluentForm\Framework\Database\Orm\Collection|static[]|static|null
*/
public function find($id, $columns = ['*'])
{
if (is_array($id)) {
return $this->findMany($id, $columns);
}
$this->query->where($this->model->getQualifiedKeyName(), '=', $id);
return $this->first($columns);
}
/**
* Find multiple models by their primary keys.
*
* @param array $ids
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Collection
*/
public function findMany($ids, $columns = ['*'])
{
if (empty($ids)) {
return $this->model->newCollection();
}
$this->query->whereIn($this->model->getQualifiedKeyName(), $ids);
return $this->get($columns);
}
/**
* Find a model by its primary key or throw an exception.
*
* @param mixed $id
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model|\FluentForm\Framework\Database\Orm\Collection
*
* @throws \FluentForm\Framework\Database\Orm\ModelNotFoundException
*/
public function findOrFail($id, $columns = ['*'])
{
$result = $this->find($id, $columns);
if (is_array($id)) {
if (count($result) == count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
throw (new ModelNotFoundException)->setModel(get_class($this->model));
}
/**
* Find a model by its primary key or return fresh model instance.
*
* @param mixed $id
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model
*/
public function findOrNew($id, $columns = ['*'])
{
if (! is_null($model = $this->find($id, $columns))) {
return $model;
}
return $this->model->newInstance();
}
/**
* Get the first record matching the attributes or instantiate it.
*
* @param array $attributes
* @return \FluentForm\Framework\Database\Orm\Model
*/
public function firstOrNew(array $attributes)
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return $this->model->newInstance($attributes);
}
/**
* Get the first record matching the attributes or create it.
*
* @param array $attributes
* @return \FluentForm\Framework\Database\Orm\Model
*/
public function firstOrCreate(array $attributes)
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
$instance = $this->model->newInstance($attributes);
$instance->save();
return $instance;
}
/**
* Create or update a record matching the attributes, and fill it with values.
*
* @param array $attributes
* @param array $values
* @return \FluentForm\Framework\Database\Orm\Model
*/
public function updateOrCreate(array $attributes, array $values = [])
{
$instance = $this->firstOrNew($attributes);
$instance->fill($values)->save();
return $instance;
}
/**
* Execute the query and get the first result.
*
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model|static|null
*/
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
/**
* Execute the query and get the first result or throw an exception.
*
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model|static
*
* @throws \FluentForm\Framework\Database\Orm\ModelNotFoundException
*/
public function firstOrFail($columns = ['*'])
{
if (! is_null($model = $this->first($columns))) {
return $model;
}
throw (new ModelNotFoundException)->setModel(get_class($this->model));
}
/**
* Execute the query as a "select" statement.
*
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Collection|static[]
*/
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
$models = $builder->getModels($columns);
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded, which will solve the
// n+1 query issue for the developers to avoid running a lot of queries.
if (count($models) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
/**
* Get a single column's value from the first result of a query.
*
* @param string $column
* @return mixed
*/
public function value($column)
{
$result = $this->first([$column]);
if ($result) {
return $result->{$column};
}
}
/**
* Get a generator for the given query.
*
* @return \Generator
*/
public function cursor()
{
$builder = $this->applyScopes();
foreach ($builder->query->cursor() as $record) {
yield $this->model->newFromBuilder($record);
}
}
/**
* Chunk the results of the query.
*
* @param int $count
* @param callable $callback
* @return bool
*/
public function chunk($count, callable $callback)
{
$results = $this->forPage($page = 1, $count)->get();
while (count($results) > 0) {
// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if (call_user_func($callback, $results) === false) {
return false;
}
$page++;
$results = $this->forPage($page, $count)->get();
}
return true;
}
/**
* Chunk the results of a query by comparing numeric IDs.
*
* @param int $count
* @param callable $callback
* @param string $column
* @return bool
*/
public function chunkById($count, callable $callback, $column = 'id')
{
$lastId = null;
$results = $this->forPageAfterId($count, 0, $column)->get();
while (! $results->isEmpty()) {
if (call_user_func($callback, $results) === false) {
return false;
}
$lastId = $results->last()->{$column};
$results = $this->forPageAfterId($count, $lastId, $column)->get();
}
return true;
}
/**
* Execute a callback over each item while chunking.
*
* @param callable $callback
* @param int $count
* @return bool
*/
public function each(callable $callback, $count = 1000)
{
if (is_null($this->query->orders) && is_null($this->query->unionOrders)) {
$this->orderBy($this->model->getQualifiedKeyName(), 'asc');
}
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
return false;
}
}
});
}
/**
* Get an array with the values of a given column.
*
* @param string $column
* @param string|null $key
* @return \Illuminate\Support\Collection
*/
public function pluck($column, $key = null)
{
$results = $this->toBase()->pluck($column, $key);
// If the model has a mutator for the requested column, we will spin through
// the results and mutate the values so that the mutated version of these
// columns are returned as you would expect from these Eloquent models.
if ($this->model->hasGetMutator($column)) {
foreach ($results as $key => &$value) {
$fill = [$column => $value];
$value = $this->model->newFromBuilder($fill)->$column;
}
}
return new Collection($results);
}
/**
* Alias for the "pluck" method.
*
* @param string $column
* @param string $key
* @return \Illuminate\Support\Collection
*
* @deprecated since version 5.2. Use the "pluck" method directly.
*/
public function lists($column, $key = null)
{
return $this->pluck($column, $key);
}
/**
* Paginate the given query.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*
* @throws \InvalidArgumentException
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$query = $this->toBase();
$total = $query->getCountForPagination();
$results = $total ? $this->forPage($page, $perPage)->get($columns) : new Collection;
return new LengthAwarePaginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
/**
* Paginate the given query into a simple paginator.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$this->skip(($page - 1) * $perPage)->take($perPage + 1);
return new Paginator($this->get($columns), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
/**
* Update a record in the database.
*
* @param array $values
* @return int
*/
public function update(array $values)
{
return $this->toBase()->update($this->addUpdatedAtColumn($values));
}
/**
* Increment a column's value by a given amount.
*
* @param string $column
* @param int $amount
* @param array $extra
* @return int
*/
public function increment($column, $amount = 1, array $extra = [])
{
$extra = $this->addUpdatedAtColumn($extra);
return $this->toBase()->increment($column, $amount, $extra);
}
/**
* Decrement a column's value by a given amount.
*
* @param string $column
* @param int $amount
* @param array $extra
* @return int
*/
public function decrement($column, $amount = 1, array $extra = [])
{
$extra = $this->addUpdatedAtColumn($extra);
return $this->toBase()->decrement($column, $amount, $extra);
}
/**
* Add the "updated at" column to an array of values.
*
* @param array $values
* @return array
*/
protected function addUpdatedAtColumn(array $values)
{
if (! $this->model->usesTimestamps()) {
return $values;
}
$column = $this->model->getUpdatedAtColumn();
return Arr::add($values, $column, $this->model->freshTimestampString());
}
/**
* Delete a record from the database.
*
* @return mixed
*/
public function delete()
{
if (isset($this->onDelete)) {
return call_user_func($this->onDelete, $this);
}
return $this->toBase()->delete();
}
/**
* Run the default delete function on the builder.
*
* @return mixed
*/
public function forceDelete()
{
return $this->query->delete();
}
/**
* Register a replacement for the default delete function.
*
* @param \Closure $callback
* @return void
*/
public function onDelete(Closure $callback)
{
$this->onDelete = $callback;
}
/**
* Get the hydrated models without eager loading.
*
* @param array $columns
* @return \FluentForm\Framework\Database\Orm\Model[]
*/
public function getModels($columns = ['*'])
{
$results = $this->query->get($columns);
$connection = $this->model->getConnectionName();
return $this->model->hydrate($results, $connection)->all();
}
/**
* Eager load the relationships for the models.
*
* @param array $models
* @return array
*/
public function eagerLoadRelations(array $models)
{
foreach ($this->eagerLoad as $name => $constraints) {
// For nested eager loads we'll skip loading them here and they will be set as an
// eager load on the query to retrieve the relation so that they will be eager
// loaded on that query, because that is where they get hydrated as models.
if (strpos($name, '.') === false) {
$models = $this->loadRelation($models, $name, $constraints);
}
}
return $models;
}
/**
* Eagerly load the relationship on a set of models.
*
* @param array $models
* @param string $name
* @param \Closure $constraints
* @return array
*/
protected function loadRelation(array $models, $name, Closure $constraints)
{
// First we will "back up" the existing where conditions on the query so we can
// add our eager constraints. Then we will merge the wheres that were on the
// query back to it in order that any where conditions might be specified.
$relation = $this->getRelation($name);
$relation->addEagerConstraints($models);
call_user_func($constraints, $relation);
$models = $relation->initRelation($models, $name);
// Once we have the results, we just match those back up to their parent models
// using the relationship instance. Then we just return the finished arrays
// of models which have been eagerly hydrated and are readied for return.
$results = $relation->getEager();
return $relation->match($models, $results, $name);
}
/**
* Get the relation instance for the given relation name.
*
* @param string $name
* @return \FluentForm\Framework\Database\Orm\Relations\Relation
*/
public function getRelation($name)
{
// We want to run a relationship query without any constrains so that we will
// not have to remove these where clauses manually which gets really hacky
// and is error prone while we remove the developer's own where clauses.
$relation = Relation::noConstraints(function () use ($name) {
return $this->getModel()->$name();
});
$nested = $this->nestedRelations($name);
// If there are nested relationships set on the query, we will put those onto
// the query instances so that they can be handled after this relationship
// is loaded. In this way they will all trickle down as they are loaded.
if (count($nested) > 0) {
$relation->getQuery()->with($nested);
}
return $relation;
}
/**
* Get the deeply nested relations for a given top-level relation.
*
* @param string $relation
* @return array
*/
protected function nestedRelations($relation)
{
$nested = [];
// We are basically looking for any relationships that are nested deeper than
// the given top-level relationship. We will just check for any relations
// that start with the given top relations and adds them to our arrays.
foreach ($this->eagerLoad as $name => $constraints) {
if ($this->isNested($name, $relation)) {
$nested[substr($name, strlen($relation.'.'))] = $constraints;
}
}
return $nested;
}
/**
* Determine if the relationship is nested.
*
* @param string $name
* @param string $relation
* @return bool
*/
protected function isNested($name, $relation)
{
$dots = Str::contains($name, '.');
return $dots && Str::startsWith($name, $relation.'.');
}
/**
* Apply the callback's query changes if the given "value" is true.
*
* @param bool $value
* @param \Closure $callback
* @return $this
*/
public function when($value, $callback)
{
$builder = $this;
if ($value) {
$builder = call_user_func($callback, $builder);
}
return $builder;
}
/**
* Add a basic where clause to the query.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column instanceof Closure) {
$query = $this->model->newQueryWithoutScopes();
call_user_func($column, $query);
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
call_user_func_array([$this->query, 'where'], func_get_args());
}
return $this;
}
/**
* Add an "or where" clause to the query.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function orWhere($column, $operator = null, $value = null)
{
return $this->where($column, $operator, $value, 'or');
}
/**
* Add a relationship count / exists condition to the query.
*
* @param string $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
if (strpos($relation, '.') !== false) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
}
$relation = $this->getHasRelationQuery($relation);
// If we only need to check for the existence of the relation, then we can
// optimize the subquery to only run a "where exists" clause instead of
// the full "count" clause. This will make the query run much faster.
$queryType = $this->shouldRunExistsQuery($operator, $count)
? 'getRelationQuery' : 'getRelationCountQuery';
$query = $relation->{$queryType}($relation->getRelated()->newQuery(), $this);
if ($callback) {
$query->callScope($callback);
}
return $this->addHasWhere(
$query, $relation, $operator, $count, $boolean
);
}
/**
* Add nested relationship count / exists conditions to the query.
*
* @param string $relations
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
{
$relations = explode('.', $relations);
// In order to nest "has", we need to add count relation constraints on the
// callback Closure. We'll do this by simply passing the Closure its own
// reference to itself so it calls itself recursively on each segment.
$closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) {
if (count($relations) > 1) {
$q->whereHas(array_shift($relations), $closure);
} else {
$q->has(array_shift($relations), $operator, $count, 'and', $callback);
}
};
return $this->has(array_shift($relations), '>=', 1, $boolean, $closure);
}
/**
* Add a relationship count / exists condition to the query.
*
* @param string $relation
* @param string $boolean
* @param \Closure|null $callback
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
{
return $this->has($relation, '<', 1, $boolean, $callback);
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* @param string $relation
* @param \Closure $callback
* @param string $operator
* @param int $count
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'and', $callback);
}
/**
* Add a relationship count / exists condition to the query with where clauses.
*
* @param string $relation
* @param \Closure|null $callback
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function whereDoesntHave($relation, Closure $callback = null)
{
return $this->doesntHave($relation, 'and', $callback);
}
/**
* Add a relationship count / exists condition to the query with an "or".
*
* @param string $relation
* @param string $operator
* @param int $count
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function orHas($relation, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or');
}
/**
* Add a relationship count / exists condition to the query with where clauses and an "or".
*
* @param string $relation
* @param \Closure $callback
* @param string $operator
* @param int $count
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or', $callback);
}
/**
* Add the "has" condition where clause to the query.
*
* @param \FluentForm\Framework\Database\Orm\Builder $hasQuery
* @param \FluentForm\Framework\Database\Orm\Relations\Relation $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
{
$hasQuery->mergeModelDefinedRelationConstraints($relation->getQuery());
if ($this->shouldRunExistsQuery($operator, $count)) {
$not = ($operator === '<' && $count === 1);
return $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $not);
}
return $this->whereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
}
/**
* Check if we can run an "exists" query to optimize performance.
*
* @param string $operator
* @param int $count
* @return bool
*/
protected function shouldRunExistsQuery($operator, $count)
{
return ($operator === '>=' || $operator === '<') && $count === 1;
}
/**
* Add a sub query count clause to the query.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @param string $operator
* @param int $count
* @param string $boolean
* @return $this
*/
protected function whereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
{
if (is_numeric($count)) {
$count = new Expression($count);
}
$this->query->addBinding($query->getBindings(), 'where');
return $this->where(new Expression('('.$query->toSql().')'), $operator, $count, $boolean);
}
/**
* Merge the constraints from a relation query to the current query.
*
* @param \FluentForm\Framework\Database\Orm\Builder $relation
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function mergeModelDefinedRelationConstraints(Builder $relation)
{
$removedScopes = $relation->removedScopes();
$relationQuery = $relation->getQuery();
// Here we have some relation query and the original relation. We need to copy over any
// where clauses that the developer may have put in the relation definition function.
// We need to remove any global scopes that the developer already removed as well.
return $this->withoutGlobalScopes($removedScopes)->mergeWheres(
$relationQuery->wheres, $relationQuery->getBindings()
);
}
/**
* Get the "has relation" base query instance.
*
* @param string $relation
* @return \FluentForm\Framework\Database\Orm\Relations\Relation
*/
protected function getHasRelationQuery($relation)
{
return Relation::noConstraints(function () use ($relation) {
return $this->getModel()->$relation();
});
}
/**
* Set the relationships that should be eager loaded.
*
* @param mixed $relations
* @return $this
*/
public function with($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
$eagers = $this->parseWithRelations($relations);
$this->eagerLoad = array_merge($this->eagerLoad, $eagers);
return $this;
}
/**
* Prevent the specified relations from being eager loaded.
*
* @param mixed $relations
* @return $this
*/
public function without($relations)
{
if (is_string($relations)) {
$relations = func_get_args();
}
$this->eagerLoad = array_diff_key($this->eagerLoad, array_flip($relations));
return $this;
}
/**
* Add subselect queries to count the relations.
*
* @param mixed $relations
* @return $this
*/
public function withCount($relations)
{
if (is_null($this->query->columns)) {
$this->query->select(['*']);
}
$relations = is_array($relations) ? $relations : func_get_args();
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
// Here we will get the relationship count query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the relation
// count query. We will normalize the relation name then append _count as the name.
$relation = $this->getHasRelationQuery($name);
$query = $relation->getRelationCountQuery(
$relation->getRelated()->newQuery(), $this
);
$query->callScope($constraints);
$query->mergeModelDefinedRelationConstraints($relation->getQuery());
$this->selectSub($query->toBase(), snake_case($name).'_count');
}
return $this;
}
/**
* Parse a list of relations into individuals.
*
* @param array $relations
* @return array
*/
protected function parseWithRelations(array $relations)
{
$results = [];
foreach ($relations as $name => $constraints) {
// If the "relation" value is actually a numeric key, we can assume that no
// constraints have been specified for the eager load and we'll just put
// an empty Closure with the loader so that we can treat all the same.
if (is_numeric($name)) {
$f = function () {
//
};
list($name, $constraints) = [$constraints, $f];
}
// We need to separate out any nested includes. Which allows the developers
// to load deep relationships using "dots" without stating each level of
// the relationship with its own key in the array of eager load names.
$results = $this->parseNestedWith($name, $results);
$results[$name] = $constraints;
}
return $results;
}
/**
* Parse the nested relationships in a relation.
*
* @param string $name
* @param array $results
* @return array
*/
protected function parseNestedWith($name, $results)
{
$progress = [];
// If the relation has already been set on the result array, we will not set it
// again, since that would override any constraints that were already placed
// on the relationships. We will only set the ones that are not specified.
foreach (explode('.', $name) as $segment) {
$progress[] = $segment;
if (! isset($results[$last = implode('.', $progress)])) {
$results[$last] = function () {
//
};
}
}
return $results;
}
/**
* Add the given scopes to the current builder instance.
*
* @param array $scopes
* @return mixed
*/
public function scopes(array $scopes)
{
$builder = $this;
foreach ($scopes as $scope => $parameters) {
if (is_int($scope)) {
list($scope, $parameters) = [$parameters, []];
}
$builder = $builder->callScope(
[$this->model, 'scope'.ucfirst($scope)], (array) $parameters
);
}
return $builder;
}
/**
* Apply the given scope on the current builder instance.
*
* @param callable $scope
* @param array $parameters
* @return mixed
*/
protected function callScope(callable $scope, $parameters = [])
{
array_unshift($parameters, $this);
$query = $this->getQuery();
// We will keep track of how many wheres are on the query before running the
// scope so that we can properly group the added scope constraints in the
// query as their own isolated nested where statement and avoid issues.
$originalWhereCount = count($query->wheres);
$result = call_user_func_array($scope, $parameters) ?: $this;
if ($this->shouldNestWheresForScope($query, $originalWhereCount)) {
$this->nestWheresForScope($query, $originalWhereCount);
}
return $result;
}
/**
* Apply the scopes to the Eloquent builder instance and return it.
*
* @return \FluentForm\Framework\Database\Orm\Builder|static
*/
public function applyScopes()
{
if (! $this->scopes) {
return $this;
}
$builder = clone $this;
foreach ($this->scopes as $scope) {
$builder->callScope(function (Builder $builder) use ($scope) {
if ($scope instanceof Closure) {
$scope($builder);
} elseif ($scope instanceof Scope) {
$scope->apply($builder, $this->getModel());
}
});
}
return $builder;
}
/**
* Determine if the scope added after the given offset should be nested.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @param int $originalWhereCount
* @return bool
*/
protected function shouldNestWheresForScope(QueryBuilder $query, $originalWhereCount)
{
return count($query->wheres) > $originalWhereCount;
}
/**
* Nest where conditions by slicing them at the given where count.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @param int $originalWhereCount
* @return void
*/
protected function nestWheresForScope(QueryBuilder $query, $originalWhereCount)
{
// Here, we totally remove all of the where clauses since we are going to
// rebuild them as nested queries by slicing the groups of wheres into
// their own sections. This is to prevent any confusing logic order.
$allWheres = $query->wheres;
$query->wheres = [];
$this->addNestedWhereSlice(
$query, $allWheres, 0, $originalWhereCount
);
$this->addNestedWhereSlice(
$query, $allWheres, $originalWhereCount
);
}
/**
* Slice where conditions at the given offset and add them to the query as a nested condition.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @param array $wheres
* @param int $offset
* @param int $length
* @return void
*/
protected function addNestedWhereSlice(QueryBuilder $query, $wheres, $offset, $length = null)
{
$whereSlice = array_slice($wheres, $offset, $length);
$whereBooleans = (new Collection($whereSlice))->pluck('boolean');
// Here we'll check if the given subset of where clauses contains any "or"
// booleans and in this case create a nested where expression. That way
// we don't add any unnecessary nesting thus keeping the query clean.
if ($whereBooleans->contains('or')) {
$query->wheres[] = $this->nestWhereSlice($whereSlice);
} else {
$query->wheres = array_merge($query->wheres, $whereSlice);
}
}
/**
* Create a where array with nested where conditions.
*
* @param array $whereSlice
* @return array
*/
protected function nestWhereSlice($whereSlice)
{
$whereGroup = $this->getQuery()->forNestedWhere();
$whereGroup->wheres = $whereSlice;
return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => 'and'];
}
/**
* Get the underlying query builder instance.
*
* @return \FluentForm\Framework\Database\Query\Builder
*/
public function getQuery()
{
return $this->query;
}
/**
* Get a base query builder instance.
*
* @return \FluentForm\Framework\Database\Query\Builder
*/
public function toBase()
{
return $this->applyScopes()->getQuery();
}
/**
* Set the underlying query builder instance.
*
* @param \FluentForm\Framework\Database\Query\Builder $query
* @return $this
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* Get the relationships being eagerly loaded.
*
* @return array
*/
public function getEagerLoads()
{
return $this->eagerLoad;
}
/**
* Set the relationships being eagerly loaded.
*
* @param array $eagerLoad
* @return $this
*/
public function setEagerLoads(array $eagerLoad)
{
$this->eagerLoad = $eagerLoad;
return $this;
}
/**
* Get the model instance being queried.
*
* @return \FluentForm\Framework\Database\Orm\Model
*/
public function getModel()
{
return $this->model;
}
/**
* Set a model instance for the model being queried.
*
* @param \FluentForm\Framework\Database\Orm\Model $model
* @return $this
*/
public function setModel(Model $model)
{
$this->model = $model;
$this->query->from($model->getTable());
return $this;
}
/**
* Extend the builder with a given callback.
*
* @param string $name
* @param \Closure $callback
* @return void
*/
public function macro($name, Closure $callback)
{
$this->macros[$name] = $callback;
}
/**
* Get the given macro by name.
*
* @param string $name
* @return \Closure
*/
public function getMacro($name)
{
return Arr::get($this->macros, $name);
}
/**
* Dynamically handle calls into the query instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (isset($this->macros[$method])) {
array_unshift($parameters, $this);
return call_user_func_array($this->macros[$method], $parameters);
}
if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
return $this->callScope([$this->model, $scope], $parameters);
}
if (in_array($method, $this->passthru)) {
return call_user_func_array([$this->toBase(), $method], $parameters);
}
call_user_func_array([$this->query, $method], $parameters);
return $this;
}
/**
* Force a clone of the underlying query builder when cloning.
*
* @return void
*/
public function __clone()
{
$this->query = clone $this->query;
}
}