src/Prettus/Repository/Eloquent/BaseRepository.php
<?php
namespace Prettus\Repository\Eloquent;
use Closure;
use Exception;
use Illuminate\Container\Container as Application;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Prettus\Repository\Contracts\CriteriaInterface;
use Prettus\Repository\Contracts\Presentable;
use Prettus\Repository\Contracts\PresenterInterface;
use Prettus\Repository\Contracts\RepositoryCriteriaInterface;
use Prettus\Repository\Contracts\RepositoryInterface;
use Prettus\Repository\Events\RepositoryEntityCreated;
use Prettus\Repository\Events\RepositoryEntityCreating;
use Prettus\Repository\Events\RepositoryEntityDeleted;
use Prettus\Repository\Events\RepositoryEntityDeleting;
use Prettus\Repository\Events\RepositoryEntityUpdated;
use Prettus\Repository\Events\RepositoryEntityUpdating;
use Prettus\Repository\Exceptions\RepositoryException;
use Prettus\Repository\Traits\ComparesVersionsTrait;
use Prettus\Validator\Contracts\ValidatorInterface;
use Prettus\Validator\Exceptions\ValidatorException;
/**
* Class BaseRepository
*
* @package Prettus\Repository\Eloquent
* @author Anderson Andrade <contato@andersonandra.de>
*/
abstract class BaseRepository implements RepositoryInterface, RepositoryCriteriaInterface
{
use ComparesVersionsTrait;
/**
* @var Application
*/
protected $app;
/**
* @var Model
*/
protected $model;
/**
* @var array
*/
protected $fieldSearchable = [];
/**
* @var PresenterInterface
*/
protected $presenter;
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* Validation Rules
*
* @var array
*/
protected $rules = null;
/**
* Collection of Criteria
*
* @var Collection
*/
protected $criteria;
/**
* @var bool
*/
protected $skipCriteria = false;
/**
* @var bool
*/
protected $skipPresenter = false;
/**
* @var \Closure
*/
protected $scopeQuery = null;
/**
* @param Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
$this->criteria = new Collection();
$this->makeModel();
$this->makePresenter();
$this->makeValidator();
$this->boot();
}
/**
*
*/
public function boot()
{
//
}
/**
* Returns the current Model instance
*
* @return Model
*/
public function getModel()
{
return $this->model;
}
/**
* @throws RepositoryException
*/
public function resetModel()
{
$this->makeModel();
}
/**
* Specify Model class name
*
* @return string
*/
abstract public function model();
/**
* Specify Presenter class name
*
* @return string
*/
public function presenter()
{
return null;
}
/**
* Specify Validator class name of Prettus\Validator\Contracts\ValidatorInterface
*
* @return null
* @throws Exception
*/
public function validator()
{
if (isset($this->rules) && !is_null($this->rules) && is_array($this->rules) && !empty($this->rules)) {
if (class_exists('Prettus\Validator\LaravelValidator')) {
$validator = app('Prettus\Validator\LaravelValidator');
if ($validator instanceof ValidatorInterface) {
$validator->setRules($this->rules);
return $validator;
}
} else {
throw new Exception(trans('repository::packages.prettus_laravel_validation_required'));
}
}
return null;
}
/**
* Set Presenter
*
* @param $presenter
*
* @return $this
*/
public function setPresenter($presenter)
{
$this->makePresenter($presenter);
return $this;
}
/**
* @return Model
* @throws RepositoryException
*/
public function makeModel()
{
$model = $this->app->make($this->model());
if (!$model instanceof Model) {
throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
}
return $this->model = $model;
}
/**
* @param null $presenter
*
* @return PresenterInterface
* @throws RepositoryException
*/
public function makePresenter($presenter = null)
{
$presenter = !is_null($presenter) ? $presenter : $this->presenter();
if (!is_null($presenter)) {
$this->presenter = is_string($presenter) ? $this->app->make($presenter) : $presenter;
if (!$this->presenter instanceof PresenterInterface) {
throw new RepositoryException("Class {$presenter} must be an instance of Prettus\\Repository\\Contracts\\PresenterInterface");
}
return $this->presenter;
}
return null;
}
/**
* @param null $validator
*
* @return null|ValidatorInterface
* @throws RepositoryException
*/
public function makeValidator($validator = null)
{
$validator = !is_null($validator) ? $validator : $this->validator();
if (!is_null($validator)) {
$this->validator = is_string($validator) ? $this->app->make($validator) : $validator;
if (!$this->validator instanceof ValidatorInterface) {
throw new RepositoryException("Class {$validator} must be an instance of Prettus\\Validator\\Contracts\\ValidatorInterface");
}
return $this->validator;
}
return null;
}
/**
* Get Searchable Fields
*
* @return array
*/
public function getFieldsSearchable()
{
return $this->fieldSearchable;
}
/**
* Query Scope
*
* @param \Closure $scope
*
* @return $this
*/
public function scopeQuery(\Closure $scope)
{
$this->scopeQuery = $scope;
return $this;
}
/**
* Retrieve data array for populate field select
*
* @param string $column
* @param string|null $key
*
* @return \Illuminate\Support\Collection|array
*/
public function lists($column, $key = null)
{
$this->applyCriteria();
return $this->model->lists($column, $key);
}
/**
* Retrieve data array for populate field select
* Compatible with Laravel 5.3
*
* @param string $column
* @param string|null $key
*
* @return \Illuminate\Support\Collection|array
*/
public function pluck($column, $key = null)
{
$this->applyCriteria();
return $this->model->pluck($column, $key);
}
/**
* Sync relations
*
* @param $id
* @param $relation
* @param $attributes
* @param bool $detaching
*
* @return mixed
*/
public function sync($id, $relation, $attributes, $detaching = true)
{
return $this->find($id)->{$relation}()->sync($attributes, $detaching);
}
/**
* SyncWithoutDetaching
*
* @param $id
* @param $relation
* @param $attributes
*
* @return mixed
*/
public function syncWithoutDetaching($id, $relation, $attributes)
{
return $this->sync($id, $relation, $attributes, false);
}
/**
* Retrieve all data of repository
*
* @param array $columns
*
* @return mixed
*/
public function all($columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
if ($this->model instanceof Builder) {
$results = $this->model->get($columns);
} else {
$results = $this->model->all($columns);
}
$this->resetModel();
$this->resetScope();
return $this->parserResult($results);
}
/**
* Count results of repository
*
* @param array $where
* @param string $columns
*
* @return int
*/
public function count(array $where = [], $columns = '*')
{
$this->applyCriteria();
$this->applyScope();
if ($where) {
$this->applyConditions($where);
}
$result = $this->model->count($columns);
$this->resetModel();
$this->resetScope();
return $result;
}
/**
* Alias of All method
*
* @param array $columns
*
* @return mixed
*/
public function get($columns = ['*'])
{
return $this->all($columns);
}
/**
* Retrieve first data of repository
*
* @param array $columns
*
* @return mixed
*/
public function first($columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$results = $this->model->first($columns);
$this->resetModel();
return $this->parserResult($results);
}
/**
* Retrieve first data of repository, or return new Entity
*
* @param array $attributes
*
* @return mixed
*/
public function firstOrNew(array $attributes = [])
{
$this->applyCriteria();
$this->applyScope();
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
$model = $this->model->firstOrNew($attributes);
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Retrieve first data of repository, or create new Entity
*
* @param array $attributes
*
* @return mixed
*/
public function firstOrCreate(array $attributes = [])
{
$this->applyCriteria();
$this->applyScope();
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
$model = $this->model->firstOrCreate($attributes);
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Retrieve data of repository with limit applied
*
* @param int $limit
* @param array $columns
*
* @return mixed
*/
public function limit($limit, $columns = ['*'])
{
// Shortcut to all with `limit` applied on query via `take`
$this->take($limit);
return $this->all($columns);
}
/**
* Retrieve all data of repository, paginated
*
* @param null|int $limit
* @param array $columns
* @param string $method
*
* @return mixed
*/
public function paginate($limit = null, $columns = ['*'], $method = "paginate")
{
$this->applyCriteria();
$this->applyScope();
$limit = is_null($limit) ? config('repository.pagination.limit', 15) : $limit;
$results = $this->model->{$method}($limit, $columns);
$results->appends(app('request')->query());
$this->resetModel();
return $this->parserResult($results);
}
/**
* Retrieve all data of repository, simple paginated
*
* @param null|int $limit
* @param array $columns
*
* @return mixed
*/
public function simplePaginate($limit = null, $columns = ['*'])
{
return $this->paginate($limit, $columns, "simplePaginate");
}
/**
* Find data by id
*
* @param $id
* @param array $columns
*
* @return mixed
*/
public function find($id, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$model = $this->model->findOrFail($id, $columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Find data by field and value
*
* @param $field
* @param $value
* @param array $columns
*
* @return mixed
*/
public function findByField($field, $value = null, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$model = $this->model->where($field, '=', $value)->get($columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Find data by multiple fields
*
* @param array $where
* @param array $columns
*
* @return mixed
*/
public function findWhere(array $where, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$this->applyConditions($where);
$model = $this->model->get($columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Find data by multiple values in one field
*
* @param $field
* @param array $values
* @param array $columns
*
* @return mixed
*/
public function findWhereIn($field, array $values, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$model = $this->model->whereIn($field, $values)->get($columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Find data by excluding multiple values in one field
*
* @param $field
* @param array $values
* @param array $columns
*
* @return mixed
*/
public function findWhereNotIn($field, array $values, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$model = $this->model->whereNotIn($field, $values)->get($columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Find data by between values in one field
*
* @param $field
* @param array $values
* @param array $columns
*
* @return mixed
*/
public function findWhereBetween($field, array $values, $columns = ['*'])
{
$this->applyCriteria();
$this->applyScope();
$model = $this->model->whereBetween($field, $values)->get($columns);
$this->resetModel();
return $this->parserResult($model);
}
/**
* Save a new entity in repository
*
* @param array $attributes
*
* @return mixed
* @throws ValidatorException
*
*/
public function create(array $attributes)
{
if (!is_null($this->validator)) {
// we should pass data that has been casts by the model
// to make sure data type are same because validator may need to use
// this data to compare with data that fetch from database.
if ($this->versionCompare($this->app->version(), "5.2.*", ">")) {
$attributes = $this->model->newInstance()->forceFill($attributes)->makeVisible($this->model->getHidden())->toArray();
} else {
$model = $this->model->newInstance()->forceFill($attributes);
$model->makeVisible($this->model->getHidden());
$attributes = $model->toArray();
}
$this->validator->with($attributes)->passesOrFail(ValidatorInterface::RULE_CREATE);
}
event(new RepositoryEntityCreating($this, $attributes));
$model = $this->model->newInstance($attributes);
$model->save();
$this->resetModel();
event(new RepositoryEntityCreated($this, $model));
return $this->parserResult($model);
}
/**
* Update a entity in repository by id
*
* @param array $attributes
* @param $id
*
* @return mixed
* @throws ValidatorException
*
*/
public function update(array $attributes, $id)
{
$this->applyScope();
if (!is_null($this->validator)) {
// we should pass data that has been casts by the model
// to make sure data type are same because validator may need to use
// this data to compare with data that fetch from database.
$model = $this->model->newInstance();
$model->setRawAttributes([]);
$model->setAppends([]);
if ($this->versionCompare($this->app->version(), "5.2.*", ">")) {
$attributes = $model->forceFill($attributes)->makeVisible($this->model->getHidden())->toArray();
} else {
$model->forceFill($attributes);
$model->makeVisible($this->model->getHidden());
$attributes = $model->toArray();
}
$this->validator->with($attributes)->setId($id)->passesOrFail(ValidatorInterface::RULE_UPDATE);
}
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
$model = $this->model->findOrFail($id);
event(new RepositoryEntityUpdating($this, $model));
$model->fill($attributes);
$model->save();
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
event(new RepositoryEntityUpdated($this, $model));
return $this->parserResult($model);
}
/**
* Update or Create an entity in repository
*
* @param array $attributes
* @param array $values
*
* @return mixed
* @throws ValidatorException
*
*/
public function updateOrCreate(array $attributes, array $values = [])
{
$this->applyScope();
if (!is_null($this->validator)) {
$this->validator->with(array_merge($attributes, $values))->passesOrFail(ValidatorInterface::RULE_CREATE);
}
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
event(new RepositoryEntityCreating($this, $attributes));
$model = $this->model->updateOrCreate($attributes, $values);
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
event(new RepositoryEntityUpdated($this, $model));
return $this->parserResult($model);
}
/**
* Delete a entity in repository by id
*
* @param $id
*
* @return int
*/
public function delete($id)
{
$this->applyScope();
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
$model = $this->find($id);
$originalModel = clone $model;
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
event(new RepositoryEntityDeleting($this, $model));
$deleted = $model->delete();
event(new RepositoryEntityDeleted($this, $originalModel));
return $deleted;
}
/**
* Delete multiple entities by given criteria.
*
* @param array $where
*
* @return int
*/
public function deleteWhere(array $where)
{
$this->applyScope();
$temporarySkipPresenter = $this->skipPresenter;
$this->skipPresenter(true);
$this->applyConditions($where);
event(new RepositoryEntityDeleting($this, $this->model->getModel()));
$deleted = $this->model->delete();
event(new RepositoryEntityDeleted($this, $this->model->getModel()));
$this->skipPresenter($temporarySkipPresenter);
$this->resetModel();
return $deleted;
}
/**
* Check if entity has relation
*
* @param string $relation
*
* @return $this
*/
public function has($relation)
{
$this->model = $this->model->has($relation);
return $this;
}
/**
* Load relations
*
* @param array|string $relations
*
* @return $this
*/
public function with($relations)
{
$this->model = $this->model->with($relations);
return $this;
}
/**
* Add subselect queries to count the relations.
*
* @param mixed $relations
*
* @return $this
*/
public function withCount($relations)
{
$this->model = $this->model->withCount($relations);
return $this;
}
/**
* Load relation with closure
*
* @param string $relation
* @param closure $closure
*
* @return $this
*/
public function whereHas($relation, $closure)
{
$this->model = $this->model->whereHas($relation, $closure);
return $this;
}
/**
* Set hidden fields
*
* @param array $fields
*
* @return $this
*/
public function hidden(array $fields)
{
$this->model->setHidden($fields);
return $this;
}
/**
* Set the "orderBy" value of the query.
*
* @param mixed $column
* @param string $direction
*
* @return $this
*/
public function orderBy($column, $direction = 'asc')
{
$this->model = $this->model->orderBy($column, $direction);
return $this;
}
/**
* Set the "limit" value of the query.
*
* @param int $limit
*
* @return $this
*/
public function take($limit)
{
// Internally `take` is an alias to `limit`
$this->model = $this->model->limit($limit);
return $this;
}
/**
* Set visible fields
*
* @param array $fields
*
* @return $this
*/
public function visible(array $fields)
{
$this->model->setVisible($fields);
return $this;
}
/**
* Push Criteria for filter the query
*
* @param $criteria
*
* @return $this
* @throws \Prettus\Repository\Exceptions\RepositoryException
*/
public function pushCriteria($criteria)
{
if (is_string($criteria)) {
$criteria = new $criteria;
}
if (!$criteria instanceof CriteriaInterface) {
throw new RepositoryException("Class " . get_class($criteria) . " must be an instance of Prettus\\Repository\\Contracts\\CriteriaInterface");
}
$this->criteria->push($criteria);
return $this;
}
/**
* Pop Criteria
*
* @param $criteria
*
* @return $this
*/
public function popCriteria($criteria)
{
$this->criteria = $this->criteria->reject(function ($item) use ($criteria) {
if (is_object($item) && is_string($criteria)) {
return get_class($item) === $criteria;
}
if (is_string($item) && is_object($criteria)) {
return $item === get_class($criteria);
}
return get_class($item) === get_class($criteria);
});
return $this;
}
/**
* Get Collection of Criteria
*
* @return Collection
*/
public function getCriteria()
{
return $this->criteria;
}
/**
* Find data by Criteria
*
* @param CriteriaInterface $criteria
*
* @return mixed
*/
public function getByCriteria(CriteriaInterface $criteria)
{
$this->model = $criteria->apply($this->model, $this);
$results = $this->model->get();
$this->resetModel();
return $this->parserResult($results);
}
/**
* Skip Criteria
*
* @param bool $status
*
* @return $this
*/
public function skipCriteria($status = true)
{
$this->skipCriteria = $status;
return $this;
}
/**
* Reset all Criterias
*
* @return $this
*/
public function resetCriteria()
{
$this->criteria = new Collection();
return $this;
}
/**
* Reset Query Scope
*
* @return $this
*/
public function resetScope()
{
$this->scopeQuery = null;
return $this;
}
/**
* Apply scope in current Query
*
* @return $this
*/
protected function applyScope()
{
if (isset($this->scopeQuery) && is_callable($this->scopeQuery)) {
$callback = $this->scopeQuery;
$this->model = $callback($this->model);
}
return $this;
}
/**
* Apply criteria in current Query
*
* @return $this
*/
protected function applyCriteria()
{
if ($this->skipCriteria === true) {
return $this;
}
$criteria = $this->getCriteria();
if ($criteria) {
foreach ($criteria as $c) {
if ($c instanceof CriteriaInterface) {
$this->model = $c->apply($this->model, $this);
}
}
}
return $this;
}
/**
* Applies the given where conditions to the model.
*
* @param array $where
*
* @return void
*/
protected function applyConditions(array $where)
{
foreach ($where as $field => $value) {
if (is_array($value)) {
list($field, $condition, $val) = $value;
//smooth input
$condition = preg_replace('/\s\s+/', ' ', trim($condition));
//split to get operator, syntax: "DATE >", "DATE =", "DAY <"
$operator = explode(' ', $condition);
if (count($operator) > 1) {
$condition = $operator[0];
$operator = $operator[1];
} else $operator = null;
switch (strtoupper($condition)) {
case 'IN':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereIn($field, $val);
break;
case 'NOTIN':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereNotIn($field, $val);
break;
case 'DATE':
if (!$operator) $operator = '=';
$this->model = $this->model->whereDate($field, $operator, $val);
break;
case 'DAY':
if (!$operator) $operator = '=';
$this->model = $this->model->whereDay($field, $operator, $val);
break;
case 'MONTH':
if (!$operator) $operator = '=';
$this->model = $this->model->whereMonth($field, $operator, $val);
break;
case 'YEAR':
if (!$operator) $operator = '=';
$this->model = $this->model->whereYear($field, $operator, $val);
break;
case 'EXISTS':
if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
$this->model = $this->model->whereExists($val);
break;
case 'HAS':
if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
$this->model = $this->model->whereHas($field, $val);
break;
case 'HASMORPH':
if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
$this->model = $this->model->whereHasMorph($field, $val);
break;
case 'DOESNTHAVE':
if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
$this->model = $this->model->whereDoesntHave($field, $val);
break;
case 'DOESNTHAVEMORPH':
if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
$this->model = $this->model->whereDoesntHaveMorph($field, $val);
break;
case 'BETWEEN':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereBetween($field, $val);
break;
case 'BETWEENCOLUMNS':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereBetweenColumns($field, $val);
break;
case 'NOTBETWEEN':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereNotBetween($field, $val);
break;
case 'NOTBETWEENCOLUMNS':
if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
$this->model = $this->model->whereNotBetweenColumns($field, $val);
break;
case 'RAW':
$this->model = $this->model->whereRaw($val);
break;
default:
$this->model = $this->model->where($field, $condition, $val);
}
} else {
$this->model = $this->model->where($field, '=', $value);
}
}
}
/**
* Skip Presenter Wrapper
*
* @param bool $status
*
* @return $this
*/
public function skipPresenter($status = true)
{
$this->skipPresenter = $status;
return $this;
}
/**
* Wrapper result data
*
* @param mixed $result
*
* @return mixed
*/
public function parserResult($result)
{
if ($this->presenter instanceof PresenterInterface) {
if ($result instanceof Collection || $result instanceof LengthAwarePaginator) {
$result->each(function ($model) {
if ($model instanceof Presentable) {
$model->setPresenter($this->presenter);
}
return $model;
});
} else if ($result instanceof Presentable) {
$result = $result->setPresenter($this->presenter);
}
if (!$this->skipPresenter) {
return $this->presenter->present($result);
}
}
return $result;
}
/**
* Trigger static method calls to the model
*
* @param $method
* @param $arguments
*
* @return mixed
*/
public static function __callStatic($method, $arguments)
{
return call_user_func_array([new static(), $method], $arguments);
}
/**
* Trigger method calls to the model
*
* @param string $method
* @param array $arguments
*
* @return mixed
*/
public function __call($method, $arguments)
{
$this->applyCriteria();
$this->applyScope();
return call_user_func_array([$this->model, $method], $arguments);
}
}