├── src ├── Housekeeper │ ├── Exceptions │ │ └── RepositoryException.php │ ├── Abilities │ │ ├── Cache │ │ │ ├── Contracts │ │ │ │ └── CacheAdapter.php │ │ │ ├── Statically.php │ │ │ ├── Statically │ │ │ │ ├── Injections │ │ │ │ │ ├── AbstractBase.php │ │ │ │ │ ├── GetCachedIfExistsBefore.php │ │ │ │ │ └── CacheResultOrClearCacheAfter.php │ │ │ │ └── CacheAdapter.php │ │ │ └── Foundation │ │ │ │ └── Base.php │ │ ├── Adjustable │ │ │ ├── Contracts │ │ │ │ └── Criteria.php │ │ │ └── Injections │ │ │ │ └── ApplyCriteriasBefore.php │ │ ├── Metadata.php │ │ ├── Vintage.php │ │ ├── Metadata │ │ │ └── Injections │ │ │ │ └── ToMetadataAfter.php │ │ ├── CacheStatically.php │ │ ├── Adjustable.php │ │ ├── Guardable.php │ │ ├── SoftDeletes.php │ │ └── Eloquently.php │ ├── Contracts │ │ ├── Injection │ │ │ ├── Basic.php │ │ │ ├── Reset.php │ │ │ ├── After.php │ │ │ └── Before.php │ │ ├── Flow │ │ │ ├── Basic.php │ │ │ ├── Reset.php │ │ │ ├── After.php │ │ │ └── Before.php │ │ ├── Plan.php │ │ ├── Action.php │ │ └── Repository.php │ ├── Console │ │ └── Generators │ │ │ ├── stubs │ │ │ └── repository.stub │ │ │ └── MakeRepositoryCommand.php │ ├── Support │ │ ├── SmartHasher.php │ │ └── InjectionContainer.php │ ├── Providers │ │ └── HousekeeperServiceProvider.php │ ├── Flows │ │ ├── Reset.php │ │ ├── After.php │ │ └── Before.php │ ├── Action.php │ ├── Plan.php │ └── Repository.php └── config │ └── housekeeper.php ├── composer.json ├── phpunit.xml ├── tests ├── Housekeeper │ ├── Abilities │ │ └── AdjustableTest.php │ ├── Support │ │ └── InjectionContainerTest.php │ └── RepositoryTest.php └── bootstrap.php ├── .gitignore ├── LICENSE └── README.md /src/Housekeeper/Exceptions/RepositoryException.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Housekeeper\Exceptions 10 | */ 11 | class RepositoryException extends \Exception 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Contracts/CacheAdapter.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Housekeeper\Contracts\Injection 10 | */ 11 | interface Basic 12 | { 13 | /** 14 | * Lower is higher. 15 | * 16 | * @return integer 17 | */ 18 | public function priority(); 19 | } 20 | -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Injection/Reset.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Contracts 12 | */ 13 | interface Reset extends Basic 14 | { 15 | /** 16 | * @param \Housekeeper\Contracts\Flow\Reset $resetFlow 17 | * @return void 18 | */ 19 | public function handle(ResetFlow $resetFlow); 20 | } 21 | -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Injection/After.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Contracts\Injection 12 | */ 13 | interface After extends Basic 14 | { 15 | /** 16 | * @param \Housekeeper\Contracts\Flow\After $afterFlow 17 | * @return void 18 | */ 19 | public function handle(AfterFlow $afterFlow); 20 | } 21 | -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Injection/Before.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Contracts\Injection 12 | */ 13 | interface Before extends Basic 14 | { 15 | /** 16 | * @param \Housekeeper\Contracts\Flow\Before $beforeFlow 17 | * @return void 18 | */ 19 | public function handle(BeforeFlow $beforeFlow); 20 | } 21 | -------------------------------------------------------------------------------- /src/Housekeeper/Console/Generators/stubs/repository.stub: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Ablities\Adjustable\Contracts 12 | */ 13 | interface Criteria 14 | { 15 | /** 16 | * Apply criteria to repository 17 | * 18 | * @param RepositoryContract $repository 19 | */ 20 | public function apply(RepositoryContract $repository); 21 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Flow/Basic.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Contracts\Flow 12 | */ 13 | interface Basic 14 | { 15 | /** 16 | * @return \Housekeeper\Contracts\Repository 17 | */ 18 | public function getRepository(); 19 | 20 | /** 21 | * @return \Housekeeper\Action 22 | */ 23 | public function getAction(); 24 | 25 | /** 26 | * @return int 27 | */ 28 | public function getIndex(); 29 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Flow/Reset.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Housekeeper\Contracts\Flow 13 | */ 14 | interface Reset extends Basic 15 | { 16 | /** 17 | * @param \Housekeeper\Contracts\Repository $repository 18 | * @param \Housekeeper\Contracts\Action $action 19 | * @param int $index 20 | */ 21 | public function __construct(Repository $repository, Action $action, $index); 22 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Metadata.php: -------------------------------------------------------------------------------- 1 | 14 | * @package Housekeeper\Traits\Repository 15 | */ 16 | trait Metadata 17 | { 18 | /** 19 | * Binding injection. 20 | */ 21 | public function bootMetadata() 22 | { 23 | $this->injectIntoAfter(new ToMetadataAfter()); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Vintage.php: -------------------------------------------------------------------------------- 1 | 14 | * @package Housekeeper\Traits\Repository 15 | */ 16 | trait Vintage 17 | { 18 | /** 19 | * @param array $wheres 20 | * @return $this 21 | */ 22 | public function applyWhere(array $wheres) 23 | { 24 | return $this->whereAre($wheres); 25 | } 26 | 27 | /** 28 | * @param $column 29 | * @param string $direction 30 | * @return mixed 31 | */ 32 | public function applyOrder($column, $direction = 'asc') 33 | { 34 | return $this->applyOrderBy($column, $direction); 35 | } 36 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aaronjan/housekeeper", 3 | "description": "Powerful, simple Repository-Pattern implementation for Laravel (>=5.1), and it come with tests.", 4 | "keywords": [ 5 | "laravel", 6 | "repository", 7 | "housekeeper" 8 | ], 9 | "license": "Apache-2.0", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Aaron Jan", 14 | "homepage": "https://github.com/AaronJan" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.5.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "~4.0", 22 | "mockery/mockery": "^0.9.4" 23 | }, 24 | "autoload": { 25 | "classmap": [ 26 | ], 27 | "psr-4": { 28 | "Housekeeper\\": "src/Housekeeper/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "classmap": [ 33 | ] 34 | }, 35 | "minimum-stability": "stable" 36 | } 37 | -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Flow/After.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Housekeeper\Contracts\Flow 13 | */ 14 | interface After extends Basic 15 | { 16 | /** 17 | * @param \Housekeeper\Contracts\Repository $repository 18 | * @param \Housekeeper\Contracts\Action $action 19 | * @param int $index 20 | * @param mixed $returnValue 21 | */ 22 | public function __construct(Repository $repository, Action $action, $index, $returnValue); 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getReturnValue(); 28 | 29 | /** 30 | * @param mixed $value 31 | * @return void 32 | */ 33 | public function setReturnValue($value); 34 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Flow/Before.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Housekeeper\Contracts\Flow 13 | */ 14 | interface Before extends Basic 15 | { 16 | /** 17 | * @param \Housekeeper\Contracts\Repository $repository 18 | * @param \Housekeeper\Contracts\Action $action 19 | * @param int $index 20 | */ 21 | public function __construct(Repository $repository, Action $action, $index); 22 | 23 | /** 24 | * @return bool 25 | */ 26 | public function hasReturnValue(); 27 | 28 | /** 29 | * @return mixed 30 | */ 31 | public function getReturnValue(); 32 | 33 | /** 34 | * @param mixed $value 35 | * @return void 36 | */ 37 | public function setReturnValue($value); 38 | } -------------------------------------------------------------------------------- /src/Housekeeper/Support/SmartHasher.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Housekeeper\Support 10 | */ 11 | class SmartHasher 12 | { 13 | /** 14 | * Hash anything (except PDO connection stuff, for now). 15 | * 16 | * @param mixed $object 17 | * @return string 18 | */ 19 | static public function hash($object) 20 | { 21 | $object = is_array($object) ? $object : [$object]; 22 | 23 | array_walk_recursive($object, function ($item) { 24 | if ($item instanceof \Closure) { 25 | $reflection = new \ReflectionFunction($item); 26 | 27 | // Unique and fast. 28 | $item = serialize($reflection->getClosureScopeClass()) . 29 | $reflection->getNumberOfParameters() . 30 | $reflection->getNamespaceName() . 31 | $reflection->getStartLine() . 32 | $reflection->getEndLine(); 33 | } 34 | }); 35 | 36 | return md5(serialize($object)); 37 | } 38 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/Housekeeper/ 17 | 18 | 19 | 20 | 21 | 22 | ./vendor/ 23 | 24 | 25 | ./src/Housekeeper/ 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Metadata/Injections/ToMetadataAfter.php: -------------------------------------------------------------------------------- 1 | 16 | * @package Housekeeper\Injections\Cacheable 17 | */ 18 | class ToMetadataAfter implements BasicInjectionContract, 19 | AfterInjectionContract 20 | { 21 | const PRIORITY = 10; 22 | 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function priority() 28 | { 29 | return static::PRIORITY; 30 | } 31 | 32 | /** 33 | * @param \Housekeeper\Contracts\Flow\After $afterFlow 34 | */ 35 | public function handle(AfterFlowContract $afterFlow) 36 | { 37 | $returnResult = $afterFlow->getReturnValue(); 38 | 39 | if ($returnResult instanceof Arrayable) { 40 | $afterFlow->setReturnValue($returnResult->toArray()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Housekeeper/Abilities/AdjustableTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, 1); 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/Housekeeper/Providers/HousekeeperServiceProvider.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Providers 12 | */ 13 | class HousekeeperServiceProvider extends ServiceProvider 14 | { 15 | /** 16 | * Indicates if loading of the provider is deferred. 17 | * 18 | * @var bool 19 | */ 20 | protected $defer = false; 21 | 22 | /** 23 | * 24 | * @return void 25 | */ 26 | public function boot() 27 | { 28 | $this->publishes([ 29 | __DIR__ . '/../../config/housekeeper.php' => config_path('housekeeper.php') 30 | ], 'config'); 31 | 32 | $this->mergeConfigFrom( 33 | __DIR__ . '/../../config/housekeeper.php', 'housekeeper' 34 | ); 35 | } 36 | 37 | /** 38 | * Register the service provider. 39 | * 40 | * @return void 41 | */ 42 | public function register() 43 | { 44 | $this->commands(\Housekeeper\Console\Generators\MakeRepositoryCommand::class); 45 | } 46 | 47 | /** 48 | * Get the services provided by the provider. 49 | * 50 | * @return array 51 | */ 52 | public function provides() 53 | { 54 | return []; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Statically.php: -------------------------------------------------------------------------------- 1 | getRedis(); 32 | $configs = $this->getCacheConfigs([ 33 | 'prefix' => 'housekeeper_', 34 | ]); 35 | 36 | /** 37 | * @var $this \Housekeeper\Contracts\Repository|$this 38 | */ 39 | $this->cacheAdapter = new CacheAdapter($this, $redis, $configs); 40 | 41 | $this->inject(new GetCachedIfExistsBefore($this->cacheAdapter), false); 42 | $this->inject(new CacheResultOrClearCacheAfter($this->cacheAdapter)); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/CacheStatically.php: -------------------------------------------------------------------------------- 1 | getRedis(); 29 | $configs = $this->getCacheConfigs([ 30 | 'prefix' => 'housekeeper_', 31 | ]); 32 | 33 | /** 34 | * @var $this CacheStatically|\Housekeeper\Contracts\Repository 35 | */ 36 | $this->cacheAdapter = new CacheAdapter($this, $redis, $configs); 37 | 38 | $this->injectIntoBefore(new GetCachedIfExistsBefore($this->cacheAdapter), false); 39 | $this->injectIntoAfter(new CacheResultOrClearCacheAfter($this->cacheAdapter)); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | getProperty($property); 24 | $reflectionProperty->setAccessible(true); 25 | $reflectionProperty->setValue($object, $value); 26 | } 27 | 28 | /** 29 | * @param $object 30 | * @param $property 31 | * @return mixed 32 | */ 33 | function getUnaccessibleObjectPropertyValue($object, $property) 34 | { 35 | $relection = new \ReflectionClass($object); 36 | 37 | $reflectionProperty = $relection->getProperty($property); 38 | $reflectionProperty->setAccessible(true); 39 | 40 | return $reflectionProperty->getValue($object); 41 | } 42 | 43 | /** 44 | * @param $object 45 | * @param $methodName 46 | * @return ReflectionMethod 47 | */ 48 | function getUnaccessibleObjectMethod($object, $methodName) 49 | { 50 | $relection = new \ReflectionClass($object); 51 | $method = $relection->getMethod($methodName); 52 | $method->setAccessible(true); 53 | 54 | return $method; 55 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Plan.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Housekeeper\Flow 14 | */ 15 | class Reset implements ResetContract 16 | { 17 | /** 18 | * @var \Housekeeper\Contracts\Action 19 | */ 20 | protected $action; 21 | 22 | /** 23 | * @var \Housekeeper\Contracts\Repository 24 | */ 25 | protected $repository; 26 | 27 | /** 28 | * @var int 29 | */ 30 | protected $index; 31 | 32 | 33 | /** 34 | * @param \Housekeeper\Contracts\Repository $repository 35 | * @param \Housekeeper\Contracts\Action $action 36 | * @param int $index 37 | */ 38 | public function __construct(Repository $repository, Action $action, $index) 39 | { 40 | $this->repository = $repository; 41 | $this->action = $action; 42 | $this->index = $index; 43 | } 44 | 45 | /** 46 | * @return \Housekeeper\Contracts\Action 47 | */ 48 | public function getAction() 49 | { 50 | return $this->action; 51 | } 52 | 53 | /** 54 | * @return \Housekeeper\Contracts\Repository 55 | */ 56 | public function getRepository() 57 | { 58 | return $this->repository; 59 | } 60 | 61 | /** 62 | * @return int 63 | */ 64 | public function getIndex() 65 | { 66 | return $this->index; 67 | } 68 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Statically/Injections/AbstractBase.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Housekeeper\Abilities\Cache\Statically\Injections 14 | */ 15 | abstract class AbstractBase 16 | { 17 | /** 18 | * @var \Housekeeper\Abilities\Cache\Contracts\CacheAdapter 19 | */ 20 | protected $cacheAdapter; 21 | 22 | 23 | /** 24 | * AbstractBase constructor. 25 | * 26 | * @param \Housekeeper\Abilities\Cache\Statically\CacheAdapter $hashCacheAdapter 27 | */ 28 | public function __construct(CacheAdapter $hashCacheAdapter) 29 | { 30 | $this->cacheAdapter = $hashCacheAdapter; 31 | } 32 | 33 | /** 34 | * @param \Housekeeper\Contracts\Action $action 35 | * @return mixed|null 36 | */ 37 | protected function getCacheForAction(ActionContract $action) 38 | { 39 | return $this->cacheAdapter->getCacheForAction($action); 40 | } 41 | 42 | /** 43 | * @param \Housekeeper\Contracts\Action $action 44 | * @param $value 45 | */ 46 | protected function setCacheForAction(ActionContract $action, $value) 47 | { 48 | $this->cacheAdapter->setCacheForAction($action, $value); 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | protected function clearCache() 55 | { 56 | $this->cacheAdapter->flush(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Adjustable/Injections/ApplyCriteriasBefore.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Housekeeper\Abilities\Adjustable\Injections 14 | */ 15 | class ApplyCriteriasBefore implements BasicInjectionContract, 16 | BeforeInjectionContract 17 | { 18 | /** 19 | * @var int 20 | */ 21 | const PRIORITY = 10; 22 | 23 | /** 24 | * @return int 25 | */ 26 | public function priority() 27 | { 28 | return static::PRIORITY; 29 | } 30 | 31 | /** 32 | * @param \Housekeeper\Contracts\Flow\Before $beforeFlow 33 | */ 34 | public function handle(BeforeFlowContract $beforeFlow) 35 | { 36 | // Only affecting the first wrapped method 37 | if ($beforeFlow->getIndex() != 1) { 38 | return; 39 | } 40 | 41 | /** 42 | * @var \Housekeeper\Contracts\Repository|\Housekeeper\Abilities\Adjustable $repository 43 | */ 44 | $repository = $beforeFlow->getRepository(); 45 | 46 | $criterias = $repository->getCriterias(); 47 | 48 | array_walk($criterias, function ($criteria) use ($repository) { 49 | /** 50 | * @var \Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria 51 | */ 52 | $repository->applyCriteria($criteria); 53 | }); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Action.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Housekeeper\Contracts 10 | */ 11 | interface Action 12 | { 13 | /** 14 | * @var int 15 | */ 16 | const UNKNOW = - 1; 17 | 18 | /** 19 | * @var int 20 | */ 21 | const CREATE = 1; 22 | 23 | /** 24 | * @var int 25 | */ 26 | const UPDATE = 2; 27 | 28 | /** 29 | * @var int 30 | */ 31 | const READ = 3; 32 | 33 | /** 34 | * @var int 35 | */ 36 | const DELETE = 4; 37 | 38 | /** 39 | * @var int 40 | */ 41 | const CREATE_OR_UPDATE = 5; 42 | 43 | /** 44 | * @var int 45 | */ 46 | const INTERNAL = 6; 47 | 48 | /** 49 | * @var int 50 | */ 51 | const IGNORED = 7; 52 | 53 | /** 54 | * @param string $methodName 55 | */ 56 | public function setMethodName($methodName); 57 | 58 | /** 59 | * @return null|string 60 | */ 61 | public function getMethodName(); 62 | 63 | /** 64 | * @param array $arguments 65 | */ 66 | public function setArguments(array $arguments); 67 | 68 | /** 69 | * @return null|array 70 | */ 71 | public function getArguments(); 72 | 73 | /** 74 | * @return int 75 | */ 76 | public function getType(); 77 | 78 | /** 79 | * @param int $type 80 | * @return bool 81 | */ 82 | public function isType($type); 83 | 84 | /** 85 | * @param $discription 86 | */ 87 | public function describeAs($discription); 88 | 89 | /** 90 | * @param $discription 91 | * @return bool 92 | */ 93 | public function isDescribedAs($discription); 94 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Statically/Injections/GetCachedIfExistsBefore.php: -------------------------------------------------------------------------------- 1 | 18 | * @package Housekeeper\Injections\Cacheable 19 | */ 20 | class GetCachedIfExistsBefore extends AbstractBase implements BasicInjectionContract, 21 | BeforeInjectionContract 22 | { 23 | const PRIORITY = 50; 24 | 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function priority() 30 | { 31 | return static::PRIORITY; 32 | } 33 | 34 | /** 35 | * @param \Housekeeper\Contracts\Flow\Before $beforeFlow 36 | */ 37 | public function handle(BeforeFlowContract $beforeFlow) 38 | { 39 | /** 40 | * Skip cache logic if cache has been disabled in the repository. 41 | * 42 | * @var $repository Repository|Statically 43 | */ 44 | $repository = $beforeFlow->getRepository(); 45 | if ($repository->isCacheEnabled() === false) { 46 | return; 47 | } 48 | 49 | /** 50 | * Only get cache when Action is typed as "READ". 51 | */ 52 | if ($beforeFlow->getAction()->isType(Action::READ)) { 53 | $cached = $this->getCacheForAction($beforeFlow->getAction()); 54 | 55 | if ( ! is_null($cached)) { 56 | $beforeFlow->setReturnValue($cached); 57 | } 58 | } 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Adjustable.php: -------------------------------------------------------------------------------- 1 | 14 | * @package Housekeeper\Traits\Repository 15 | */ 16 | trait Adjustable 17 | { 18 | /** 19 | * @var \Housekeeper\Abilities\Adjustable\Contracts\Criteria[] 20 | */ 21 | protected $criterias = []; 22 | 23 | 24 | /** 25 | * Bind injection. 26 | */ 27 | public function bootAdjustable() 28 | { 29 | $this->injectIntoBefore(new ApplyCriteriasBefore()); 30 | } 31 | 32 | /** 33 | * ave this criteria, it would be auto-applied before every method calling. 34 | * 35 | * @param \Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria 36 | * @return $this 37 | */ 38 | public function rememberCriteria(Criteria $criteria) 39 | { 40 | $this->criterias[] = $criteria; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Remove all criterias that this repository remembered. 47 | * 48 | * @return $this 49 | */ 50 | public function forgetCriterias() 51 | { 52 | $this->criterias = []; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Get all criterias that this repository remembered. 59 | * 60 | * @return Criteria[] 61 | */ 62 | public function getCriterias() 63 | { 64 | return $this->criterias; 65 | } 66 | 67 | /** 68 | * @param \Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria 69 | * @return $this 70 | */ 71 | public function applyCriteria(Criteria $criteria) 72 | { 73 | /** 74 | * @var \Housekeeper\Contracts\Repository $this 75 | */ 76 | $criteria->apply($this); 77 | 78 | return $this; 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/Housekeeper/Flows/After.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Housekeeper\Flow 14 | */ 15 | class After implements AfterContract 16 | { 17 | /** 18 | * @var Action 19 | */ 20 | protected $action; 21 | 22 | /** 23 | * @var Repository 24 | */ 25 | protected $repository; 26 | 27 | /** 28 | * @var int 29 | */ 30 | protected $index; 31 | 32 | /** 33 | * @var mixed 34 | */ 35 | protected $returnValue; 36 | 37 | 38 | /** 39 | * @param \Housekeeper\Contracts\Repository $repository 40 | * @param \Housekeeper\Contracts\Action $action 41 | * @param int $index 42 | * @param mixed $returnValue 43 | */ 44 | public function __construct(Repository $repository, Action $action, $index, $returnValue) 45 | { 46 | $this->repository = $repository; 47 | $this->action = $action; 48 | $this->index = $index; 49 | $this->returnValue = $returnValue; 50 | } 51 | 52 | /** 53 | * @return \Housekeeper\Contracts\Action 54 | */ 55 | public function getAction() 56 | { 57 | return $this->action; 58 | } 59 | 60 | /** 61 | * @return \Housekeeper\Contracts\Repository 62 | */ 63 | public function getRepository() 64 | { 65 | return $this->repository; 66 | } 67 | 68 | /** 69 | * @return int 70 | */ 71 | public function getIndex() 72 | { 73 | return $this->index; 74 | } 75 | 76 | /** 77 | * @return mixed 78 | */ 79 | public function getReturnValue() 80 | { 81 | return $this->returnValue; 82 | } 83 | 84 | /** 85 | * @param mixed $value 86 | */ 87 | public function setReturnValue($value) 88 | { 89 | $this->returnValue = $value; 90 | } 91 | } -------------------------------------------------------------------------------- /src/config/housekeeper.php: -------------------------------------------------------------------------------- 1 | 6 | * @package Housekeeper 7 | * @version 2.* 8 | * @license Apache 2.0 9 | * @copyright (c) 2015, AaronJan 10 | * @link https://github.com/AaronJan/Housekeeper 11 | */ 12 | 13 | return [ 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Repository Directory 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Where to put repository class. 21 | | 22 | */ 23 | 24 | 'directory' => '/Repositories/', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Paginate Configures 29 | |-------------------------------------------------------------------------- 30 | */ 31 | 32 | 'paginate' => [ 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Page Size 37 | |-------------------------------------------------------------------------- 38 | | 39 | | How many entries per page for "paginate" method. 40 | | 41 | */ 42 | 43 | 'per_page' => 15, 44 | 45 | ], 46 | 47 | 'abilities' => [ 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Cacheable 52 | |-------------------------------------------------------------------------- 53 | | 54 | | 55 | | 56 | */ 57 | 58 | 'cache' => [ 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Cache Key Prefix 63 | |-------------------------------------------------------------------------- 64 | | 65 | | 66 | | 67 | */ 68 | 69 | 'prefix' => 'housekeeper_', 70 | 71 | ], 72 | 73 | ], 74 | 75 | ]; 76 | -------------------------------------------------------------------------------- /src/Housekeeper/Flows/Before.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Housekeeper\Flow 14 | */ 15 | class Before implements BeforeContract 16 | { 17 | /** 18 | * @var Action 19 | */ 20 | protected $action; 21 | 22 | /** 23 | * @var Repository 24 | */ 25 | protected $repository; 26 | 27 | /** 28 | * @var int 29 | */ 30 | protected $index; 31 | 32 | /** 33 | * @var mixed 34 | */ 35 | protected $returnValue; 36 | 37 | 38 | /** 39 | * @param \Housekeeper\Contracts\Repository $repository 40 | * @param \Housekeeper\Contracts\Action $action 41 | * @param int $index 42 | */ 43 | public function __construct(Repository $repository, Action $action, $index) 44 | { 45 | $this->repository = $repository; 46 | $this->action = $action; 47 | $this->index = $index; 48 | } 49 | 50 | /** 51 | * @return \Housekeeper\Contracts\Action 52 | */ 53 | public function getAction() 54 | { 55 | return $this->action; 56 | } 57 | 58 | /** 59 | * @return \Housekeeper\Contracts\Repository 60 | */ 61 | public function getRepository() 62 | { 63 | return $this->repository; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getIndex() 70 | { 71 | return $this->index; 72 | } 73 | 74 | /** 75 | * @return bool 76 | */ 77 | public function hasReturnValue() 78 | { 79 | return ! is_null($this->returnValue); 80 | } 81 | 82 | /** 83 | * @return mixed 84 | */ 85 | public function getReturnValue() 86 | { 87 | return $this->returnValue; 88 | } 89 | 90 | /** 91 | * @param mixed $value 92 | */ 93 | public function setReturnValue($value) 94 | { 95 | $this->returnValue = $value; 96 | } 97 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Foundation/Base.php: -------------------------------------------------------------------------------- 1 | cacheEnabled = false; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * 44 | */ 45 | public function enableCache() 46 | { 47 | $this->cacheEnabled = true; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function isCacheEnabled() 56 | { 57 | return $this->cacheEnabled; 58 | } 59 | 60 | /** 61 | * 62 | */ 63 | public function clearCache() 64 | { 65 | $this->cacheAdapter->flush(); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return \Illuminate\Contracts\Redis\Database|\Illuminate\Redis\Database 72 | */ 73 | protected function getRedis() 74 | { 75 | if (! $this->cacheRedis) { 76 | try { 77 | $this->cacheRedis = $this->getApp()->make('redis'); 78 | } catch (\Exception $e) { 79 | throw new \RuntimeException('Cacheable trait of Housekeeper requires Redis support.'); 80 | } 81 | } 82 | 83 | return $this->cacheRedis; 84 | } 85 | 86 | /** 87 | * @param array $default 88 | * @return mixed 89 | */ 90 | protected function getCacheConfigs(array $default = []) 91 | { 92 | return $this->getConfig('housekeeper.abilities.cache', $default); 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Statically/Injections/CacheResultOrClearCacheAfter.php: -------------------------------------------------------------------------------- 1 | 18 | * @package Housekeeper\Injections\Cacheable 19 | */ 20 | class CacheResultOrClearCacheAfter extends AbstractBase implements BasicInjectionContract, 21 | AfterInjectionContract 22 | { 23 | const PRIORITY = 50; 24 | 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function priority() 30 | { 31 | return static::PRIORITY; 32 | } 33 | 34 | /** 35 | * @param \Housekeeper\Contracts\Flow\After $afterFlow 36 | */ 37 | public function handle(AfterFlowContract $afterFlow) 38 | { 39 | /** 40 | * Skip cache logic if cache has been disabled in the repository. 41 | * 42 | * @var $repository Repository|Statically 43 | */ 44 | $repository = $afterFlow->getRepository(); 45 | $cacheEnabled = $repository->isCacheEnabled(); 46 | 47 | /** 48 | * Cache result only when action is "Read" type, 49 | * clear cache when action is "Create" or "Update". 50 | */ 51 | switch ($afterFlow->getAction()->getType()) { 52 | case Action::CREATE: 53 | $this->clearCache(); 54 | 55 | break; 56 | case Action::UPDATE: 57 | $this->clearCache(); 58 | 59 | break; 60 | case Action::READ: 61 | $cacheEnabled && $this->remember($afterFlow); 62 | 63 | break; 64 | case Action::DELETE: 65 | $this->clearCache(); 66 | 67 | break; 68 | case Action::CREATE_OR_UPDATE: 69 | $this->clearCache(); 70 | 71 | break; 72 | default: 73 | break; 74 | } 75 | } 76 | 77 | /** 78 | * @param \Housekeeper\Contracts\Flow\After $afterFlow 79 | */ 80 | protected function remember(AfterFlowContract $afterFlow) 81 | { 82 | $this->setCacheForAction( 83 | $afterFlow->getAction(), 84 | $afterFlow->getReturnValue() 85 | ); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/Housekeeper/Action.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper 12 | */ 13 | class Action implements ActionContract 14 | { 15 | /** 16 | * @var integer 17 | */ 18 | protected $type; 19 | 20 | /** 21 | * @var null|array 22 | */ 23 | protected $arguments; 24 | 25 | /** 26 | * @var null|string 27 | */ 28 | protected $methodName; 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected $discriptions; 34 | 35 | /** 36 | * Action constructor. 37 | * 38 | * @param int $type 39 | * @param array $discriptions 40 | * @param null $methodName 41 | * @param array|null $arguments 42 | */ 43 | public function __construct($type = ActionContract::UNKNOW, 44 | array $discriptions = [], 45 | $methodName = null, 46 | array $arguments = null) 47 | { 48 | $this->type = is_null($type) ? static::UNKNOW : $type; 49 | $this->discriptions = $discriptions; 50 | 51 | if ($methodName) { 52 | $this->setMethodName($methodName); 53 | } 54 | 55 | if (! is_null($arguments)) { 56 | $this->setArguments($arguments); 57 | } 58 | } 59 | 60 | /** 61 | * @param string $methodName 62 | */ 63 | public function setMethodName($methodName) 64 | { 65 | $this->methodName = $methodName; 66 | } 67 | 68 | /** 69 | * @return null|string 70 | */ 71 | public function getMethodName() 72 | { 73 | return $this->methodName; 74 | } 75 | 76 | /** 77 | * @param array $arguments 78 | */ 79 | public function setArguments(array $arguments) 80 | { 81 | $this->arguments = $arguments; 82 | } 83 | 84 | /** 85 | * @return array|null 86 | */ 87 | public function getArguments() 88 | { 89 | return $this->arguments; 90 | } 91 | 92 | /** 93 | * @return int 94 | */ 95 | public function getType() 96 | { 97 | return $this->type; 98 | } 99 | 100 | /** 101 | * @param int $type 102 | * @return bool 103 | */ 104 | public function isType($type) 105 | { 106 | return $this->type === $type; 107 | } 108 | 109 | /** 110 | * @param $discription 111 | */ 112 | public function describeAs($discription) 113 | { 114 | $this->discriptions[] = $discription; 115 | } 116 | 117 | /** 118 | * @param $discription 119 | * @return bool 120 | */ 121 | public function isDescribedAs($discription) 122 | { 123 | return in_array($discription, $this->discriptions); 124 | } 125 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Guardable.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper\Abilities 12 | */ 13 | trait Guardable 14 | { 15 | /** 16 | * @var bool 17 | */ 18 | protected $guarded = false; 19 | 20 | /** 21 | * @return $this 22 | */ 23 | public function guardUp() 24 | { 25 | $this->guarded = true; 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * @return $this 32 | */ 33 | public function guardDown() 34 | { 35 | $this->guarded = false; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @return bool 42 | */ 43 | public function isGuarded() 44 | { 45 | return $this->guarded; 46 | } 47 | 48 | /** 49 | * @param array $attributes 50 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|static 51 | */ 52 | protected function _create(array $attributes) 53 | { 54 | $model = $this->newModelInstance(); 55 | 56 | if ($this->isGuarded()) { 57 | $model = $model->fill($attributes); 58 | } else { 59 | $model = $model->forceFill($attributes); 60 | } 61 | 62 | $model->save(); 63 | 64 | return $model; 65 | } 66 | 67 | /** 68 | * @param $id 69 | * @param array $attributes 70 | * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model 71 | */ 72 | protected function _update($id, array $attributes) 73 | { 74 | if ($id instanceof Model) { 75 | $id = $id->getKey(); 76 | } 77 | 78 | $model = $this->getModel()->findOrFail($id); 79 | 80 | if ($this->isGuarded()) { 81 | $model->fill($attributes); 82 | } else { 83 | $model->forceFill($attributes); 84 | } 85 | 86 | $model->save(); 87 | 88 | return $model; 89 | } 90 | 91 | /** 92 | * @see \Housekeeper\Repository::getModel 93 | * 94 | * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder 95 | */ 96 | abstract protected function getModel(); 97 | 98 | /** 99 | * @see \Housekeeper\Repository::newModelInstance 100 | * 101 | * Make a new Model instance. 102 | * 103 | * @return Model 104 | */ 105 | abstract protected function newModelInstance(); 106 | 107 | /** 108 | * @see \Housekeeper\Repository::injectIntoBefore 109 | * 110 | * @param \Housekeeper\Contracts\Injection\Before $injection 111 | * @param bool $sort 112 | * @return void 113 | */ 114 | abstract protected function injectIntoBefore(\Housekeeper\Contracts\Injection\Before $injection, $sort = true); 115 | } -------------------------------------------------------------------------------- /src/Housekeeper/Contracts/Repository.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Housekeeper\Contracts 10 | */ 11 | interface Repository 12 | { 13 | /** 14 | * @return \Housekeeper\Contracts\Plan 15 | */ 16 | public function getCurrentPlan(); 17 | 18 | /** 19 | * This is more semantic than `applyWheres`. 20 | * 21 | * @param array $wheres 22 | * @return $this 23 | */ 24 | public function whereAre(array $wheres); 25 | 26 | /** 27 | * @param array $wheres 28 | * @return mixed 29 | */ 30 | public function applyWheres(array $wheres); 31 | 32 | /** 33 | * @param $column 34 | * @param string $direction 35 | * @return mixed 36 | */ 37 | public function orderBy($column, $direction = 'asc'); 38 | 39 | /** 40 | * @param $column 41 | * @param string $direction 42 | * @return mixed 43 | */ 44 | public function applyOrderBy($column, $direction = 'asc'); 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function with(); 50 | 51 | /** 52 | * @param $value 53 | * @return $this 54 | */ 55 | public function offset($value); 56 | 57 | /** 58 | * @param $value 59 | * @return $this 60 | */ 61 | public function limit($value); 62 | 63 | /** 64 | * @param $id 65 | * @param null $column 66 | * @return mixed 67 | */ 68 | public function exists($id, $column = null); 69 | 70 | /** 71 | * @param $id 72 | * @param array $columns 73 | * @return mixed 74 | */ 75 | public function find($id, $columns = ['*']); 76 | 77 | /** 78 | * @param $ids 79 | * @param array $columns 80 | * @return mixed 81 | */ 82 | public function findMany($ids, $columns = ['*']); 83 | 84 | /** 85 | * @param array $attributes 86 | * @return mixed 87 | */ 88 | public function create(array $attributes); 89 | 90 | /** 91 | * @param $id 92 | * @return mixed 93 | */ 94 | public function delete($id); 95 | 96 | /** 97 | * @param $id 98 | * @param array $attributes 99 | * @return mixed 100 | */ 101 | public function update($id, array $attributes); 102 | 103 | /** 104 | * @param array $columns 105 | * @return mixed 106 | */ 107 | public function all($columns = ['*']); 108 | 109 | /** 110 | * @param null $limit 111 | * @param array $columns 112 | * @param string $pageName 113 | * @param null $page 114 | * @return mixed 115 | */ 116 | public function paginate($limit = null, $columns = ['*'], $pageName = 'page', $page = null); 117 | 118 | /** 119 | * @param $field 120 | * @param null $value 121 | * @param array $columns 122 | * @return mixed 123 | */ 124 | public function getByField($field, $value = null, $columns = ['*']); 125 | 126 | /** 127 | * @param $field 128 | * @param null $value 129 | * @param array $columns 130 | * @return mixed 131 | */ 132 | public function findByField($field, $value = null, $columns = ['*']); 133 | } 134 | -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/SoftDeletes.php: -------------------------------------------------------------------------------- 1 | 15 | * @package Housekeeper\Traits\Repository 16 | */ 17 | trait SoftDeletes 18 | { 19 | /** 20 | * Include soft deletes for following use, will triggering a Reset flow. 21 | * 22 | * @return $this 23 | */ 24 | public function startWithTrashed() 25 | { 26 | $this->reset(new Action(__METHOD__, [], Action::INTERNAL)); 27 | 28 | $this->getCurrentPlan()->startWithTrashed(); 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Only include soft deletes for following use, will triggering a Reset 35 | * flow. 36 | * 37 | * @return $this 38 | */ 39 | public function startWithTrashedOnly() 40 | { 41 | $this->reset(new Action(__METHOD__, [], Action::INTERNAL)); 42 | 43 | $this->getCurrentPlan()->startWithTrashedOnly(); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param $id 50 | * @return bool|null 51 | * @throws ModelNotFoundException 52 | */ 53 | public function forceDelete($id) 54 | { 55 | return $this->simpleWrap(Action::DELETE, [$this, '_forceDelete']); 56 | } 57 | 58 | /** 59 | * @param $id 60 | * @return bool|null 61 | * @throws ModelNotFoundException 62 | */ 63 | protected function _forceDelete($id) 64 | { 65 | $model = $this->getModel()->findOrFail($id); 66 | 67 | $deleted = $model->forceDelete(); 68 | 69 | return $deleted; 70 | } 71 | 72 | /** 73 | * Restore model which is soft-deleted. 74 | * 75 | * @return bool|null 76 | */ 77 | public function restore($id) 78 | { 79 | return $this->simpleWrap(Action::UPDATE, [$this, '_restore']); 80 | } 81 | 82 | /** 83 | * @return bool|null 84 | */ 85 | protected function _restore($id) 86 | { 87 | /** 88 | * @var $model Model|\Illuminate\Database\Eloquent\SoftDeletes 89 | */ 90 | $model = $this->startWithTrashed()->find($id, [$this->getKeyName()]); 91 | 92 | $model->restore(); 93 | 94 | return $model; 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | abstract protected function getKeyName(); 101 | 102 | /** 103 | * @param $id 104 | * @param array $columns 105 | * @return mixed 106 | * @throws \Exception 107 | */ 108 | abstract public function find($id, $columns = ['*']); 109 | 110 | /** 111 | * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder 112 | */ 113 | abstract protected function getModel(); 114 | 115 | /** 116 | * @param $actionType 117 | * @param callable|null $function 118 | * @return mixed 119 | * @throws \Exception 120 | */ 121 | abstract protected function simpleWrap($actionType, callable $function = null); 122 | 123 | /** 124 | * @param \Housekeeper\Contracts\Action $action 125 | * @return $this 126 | */ 127 | abstract protected function reset(ActionContract $action); 128 | 129 | /** 130 | * @return \Housekeeper\Plan 131 | */ 132 | abstract protected function getCurrentPlan(); 133 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/log/ 2 | .settings/ 3 | composer.lock 4 | 5 | 6 | # Created by .ignore support plugin (hsz.mobi) 7 | ### Composer template 8 | composer.phar 9 | vendor/ 10 | 11 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 12 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 13 | # composer.lock 14 | 15 | 16 | ### JetBrains template 17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 18 | 19 | *.iml 20 | 21 | ## Directory-based project format: 22 | .idea/ 23 | # if you remove the above rule, at least ignore the following: 24 | 25 | # User-specific stuff: 26 | # .idea/workspace.xml 27 | # .idea/tasks.xml 28 | # .idea/dictionaries 29 | 30 | # Sensitive or high-churn files: 31 | # .idea/dataSources.ids 32 | # .idea/dataSources.xml 33 | # .idea/sqlDataSources.xml 34 | # .idea/dynamic.xml 35 | # .idea/uiDesigner.xml 36 | 37 | # Gradle: 38 | # .idea/gradle.xml 39 | # .idea/libraries 40 | 41 | # Mongo Explorer plugin: 42 | # .idea/mongoSettings.xml 43 | 44 | ## File-based project format: 45 | *.ipr 46 | *.iws 47 | 48 | ## Plugin-specific files: 49 | 50 | # IntelliJ 51 | /out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | 64 | 65 | ### OSX template 66 | .DS_Store 67 | .AppleDouble 68 | .LSOverride 69 | 70 | # Icon must end with two \r 71 | Icon 72 | 73 | # Thumbnails 74 | ._* 75 | 76 | # Files that might appear in the root of a volume 77 | .DocumentRevisions-V100 78 | .fseventsd 79 | .Spotlight-V100 80 | .TemporaryItems 81 | .Trashes 82 | .VolumeIcon.icns 83 | 84 | # Directories potentially created on remote AFP share 85 | .AppleDB 86 | .AppleDesktop 87 | Network Trash Folder 88 | Temporary Items 89 | .apdisk 90 | 91 | 92 | ### Windows template 93 | # Windows image file caches 94 | Thumbs.db 95 | ehthumbs.db 96 | 97 | # Folder config file 98 | Desktop.ini 99 | 100 | # Recycle Bin used on file shares 101 | $RECYCLE.BIN/ 102 | 103 | # Windows Installer files 104 | *.cab 105 | *.msi 106 | *.msm 107 | *.msp 108 | 109 | # Windows shortcuts 110 | *.lnk 111 | 112 | 113 | 114 | ### JetBrains template 115 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 116 | 117 | *.iml 118 | 119 | ## Directory-based project format: 120 | .idea/ 121 | # if you remove the above rule, at least ignore the following: 122 | 123 | # User-specific stuff: 124 | # .idea/workspace.xml 125 | # .idea/tasks.xml 126 | # .idea/dictionaries 127 | 128 | # Sensitive or high-churn files: 129 | # .idea/dataSources.ids 130 | # .idea/dataSources.xml 131 | # .idea/sqlDataSources.xml 132 | # .idea/dynamic.xml 133 | # .idea/uiDesigner.xml 134 | 135 | # Gradle: 136 | # .idea/gradle.xml 137 | # .idea/libraries 138 | 139 | # Mongo Explorer plugin: 140 | # .idea/mongoSettings.xml 141 | 142 | ## File-based project format: 143 | *.ipr 144 | *.iws 145 | 146 | ## Plugin-specific files: 147 | 148 | # IntelliJ 149 | /out/ 150 | 151 | # mpeltonen/sbt-idea plugin 152 | .idea_modules/ 153 | 154 | # JIRA plugin 155 | atlassian-ide-plugin.xml 156 | 157 | # Crashlytics plugin (for Android Studio and IntelliJ) 158 | com_crashlytics_export_strings.xml 159 | crashlytics.properties 160 | crashlytics-build.properties 161 | 162 | 163 | 164 | ### Composer template 165 | composer.phar 166 | vendor/ 167 | 168 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 169 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 170 | # composer.lock 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Cache/Statically/CacheAdapter.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 51 | $this->redis = $redis; 52 | 53 | $this->prefix = $configs['prefix']; 54 | } 55 | 56 | /** 57 | * @return \Illuminate\Contracts\Redis\Database|\Illuminate\Redis\Database 58 | */ 59 | protected function getRedis() 60 | { 61 | return $this->redis; 62 | } 63 | 64 | /** 65 | * @return \Housekeeper\Contracts\Repository 66 | */ 67 | protected function getRepository() 68 | { 69 | return $this->repository; 70 | } 71 | 72 | /** 73 | * @param \Housekeeper\Contracts\Action $action 74 | * @param $value 75 | */ 76 | public function setCacheForAction(ActionContract $action, $value) 77 | { 78 | $this->getRedis()->hset( 79 | $this->cacheKey(), 80 | $this->getCacheFieldForAction($action), 81 | serialize($value) 82 | ); 83 | } 84 | 85 | /** 86 | * @param \Housekeeper\Contracts\Action $action 87 | * @return mixed|null 88 | */ 89 | public function getCacheForAction(ActionContract $action) 90 | { 91 | $cachedValue = $this->getRedis()->hget( 92 | $this->cacheKey(), 93 | $this->getCacheFieldForAction($action) 94 | ); 95 | 96 | return is_null($cachedValue) ? null : unserialize($cachedValue); 97 | } 98 | 99 | /** 100 | * 101 | */ 102 | public function flush() 103 | { 104 | $this->getRedis()->del($this->cacheKey()); 105 | } 106 | 107 | /** 108 | * @param \Housekeeper\Contracts\Action $action 109 | * @return string 110 | */ 111 | protected function getCacheFieldForAction(ActionContract $action) 112 | { 113 | $id = md5( 114 | SmartHasher::hash($this->getRepository()->getCurrentPlan()) . 115 | $action->getMethodName() . 116 | SmartHasher::hash($action->getArguments()) 117 | ); 118 | 119 | return $id; 120 | } 121 | 122 | /** 123 | * @return string 124 | */ 125 | protected function cacheKey() 126 | { 127 | if ( ! $this->cacheKey) { 128 | $this->cacheKey = $this->prefix . 129 | str_replace( 130 | '\\', '#', 131 | get_class($this->getRepository()) 132 | ); 133 | } 134 | 135 | return $this->cacheKey; 136 | } 137 | } -------------------------------------------------------------------------------- /tests/Housekeeper/Support/InjectionContainerTest.php: -------------------------------------------------------------------------------- 1 | 22 | * @package Housekeeper\Eloquent 23 | */ 24 | class InjectionContainerTest extends \PHPUnit_Framework_TestCase 25 | { 26 | /** 27 | * 28 | */ 29 | protected function setUp() 30 | { 31 | 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | public function tearDown() 38 | { 39 | m::close(); 40 | } 41 | 42 | // 43 | // /** 44 | // * @runInSeparateProcess 45 | // * @covers Housekeeper\Repository::sortAllInjections 46 | // * @covers Housekeeper\Repository::sortInjection 47 | // */ 48 | // public function testSortAllInjections() 49 | // { 50 | // // mock Injections for Repository to sort 51 | // $mockInjections = $this->makeMockInjections(); 52 | // 53 | // $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 54 | // 55 | // // Mock `sortInjection` method (same as the original method) because it's private 56 | // $methodSortInjection = getUnaccessibleObjectMethod($mockRepository, 'sortInjection'); 57 | // $mockRepository->shouldReceive('sortInjection') 58 | // ->andReturnUsing(function ($a, $b) use ($methodSortInjection, $mockRepository) { 59 | // return $methodSortInjection->invoke($mockRepository, $a, $b); 60 | // }); 61 | // 62 | // // Replace `injections` property with mock `injections` 63 | // $mockRepositoryReflection = new \ReflectionClass($mockRepository); 64 | // $propertyInjectionsReflection = $mockRepositoryReflection->getProperty('injections'); 65 | // $propertyInjectionsReflection->setAccessible(true); 66 | // $propertyInjectionsReflection->setValue($mockRepository, $mockInjections); 67 | // 68 | // // Execute `sortAllInjections` method 69 | // $methodSortAllInjections = getUnaccessibleObjectMethod($mockRepository, 'sortAllInjections'); 70 | // $methodSortAllInjections->invoke($mockRepository); 71 | // 72 | // // Verify results 73 | // $sortedInjections = $propertyInjectionsReflection->getValue($mockRepository); 74 | // foreach ($sortedInjections as $group) { 75 | // $lastPriority = - 1; 76 | // 77 | // foreach ($group as $injection) { 78 | // // ascending order 79 | // $this->assertGreaterThanOrEqual($lastPriority, $injection->priority()); 80 | // } 81 | // } 82 | // } 83 | // 84 | // /** 85 | // * @return array 86 | // */ 87 | // protected function makeMockInjections() 88 | // { 89 | // $mockInjections = [ 90 | // 'reset' => [], 91 | // 'before' => [], 92 | // 'after' => [], 93 | // ]; 94 | // $priorities = [ 95 | // 3, 3, 4, 5, 1, 96 | // 2, 1, 5, 4, 6, 97 | // 100, 1, 99, 2, 66, 98 | // ]; 99 | // foreach (['reset', 'before', 'after'] as $group) { 100 | // for ($i = 0; $i < 5; $i ++) { 101 | // $priority = array_pop($priorities); 102 | // 103 | // $mockInjection = m::mock(MockBasicInjection::class); 104 | // $mockInjection->shouldReceive('priority') 105 | // ->andReturnUsing(function () use ($priority) { 106 | // return $priority; 107 | // }); 108 | // 109 | // $mockInjections[$group][] = $mockInjection; 110 | // } 111 | // } 112 | // 113 | // return $mockInjections; 114 | // } 115 | 116 | 117 | } 118 | 119 | // ============================================================================ 120 | -------------------------------------------------------------------------------- /src/Housekeeper/Plan.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Housekeeper 12 | */ 13 | class Plan implements Contracts\Plan 14 | { 15 | /** 16 | * @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder 17 | */ 18 | protected $model; 19 | 20 | /** 21 | * @var array 22 | */ 23 | protected $conditions = []; 24 | 25 | 26 | /** 27 | * Plan constructor. 28 | * 29 | * @param \Illuminate\Database\Eloquent\Model $model 30 | */ 31 | public function __construct(Model $model) 32 | { 33 | $this->model = $model; 34 | } 35 | 36 | /** 37 | * Every single action that would afftected returns, should add a condition. 38 | * 39 | * @param string $type 40 | * @param mixed $condition 41 | */ 42 | protected function addCondition($type, $condition) 43 | { 44 | $this->conditions[] = [ 45 | $type => $condition, 46 | ]; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getConditions() 53 | { 54 | return $this->conditions; 55 | } 56 | 57 | /** 58 | * @return bool 59 | */ 60 | public function isEmpty() 61 | { 62 | return empty($this->conditions); 63 | } 64 | 65 | /** 66 | * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder 67 | */ 68 | public function getModel() 69 | { 70 | return $this->model; 71 | } 72 | 73 | /** 74 | * @param string $column 75 | * @param string $direction 76 | * @return $this 77 | */ 78 | public function applyOrderBy($column, $direction = 'asc') 79 | { 80 | /** 81 | * Save to conditons. 82 | */ 83 | $this->addCondition('order by', [$column, $direction]); 84 | 85 | $this->model = $this->model->orderBy($column, $direction); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @param $value 92 | * @return $this 93 | */ 94 | public function applyOffset($value) 95 | { 96 | /** 97 | * Save to conditons. 98 | */ 99 | $this->addCondition('offset', $value); 100 | 101 | $this->model = $this->model->offset($value); 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param $value 108 | * @return $this 109 | */ 110 | public function applyLimit($value) 111 | { 112 | /** 113 | * Save to conditons. 114 | */ 115 | $this->addCondition('limit', $value); 116 | 117 | $this->model = $this->model->limit($value); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * @param array $wheres 124 | * @return $this 125 | */ 126 | public function applyWheres(array $wheres) 127 | { 128 | /** 129 | * Save to conditons. 130 | */ 131 | $this->addCondition('wheres', $wheres); 132 | 133 | foreach ($wheres as $key => $where) { 134 | $parameters = ($where instanceof \Closure ? [$where] : $where); 135 | $this->model = call_user_func_array([$this->model, 'where'], $parameters); 136 | } 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @param mixed $relations 143 | * @return $this 144 | */ 145 | public function applyWith() 146 | { 147 | $arguments = func_get_args(); 148 | 149 | /** 150 | * Save to conditons. 151 | */ 152 | $this->addCondition('with', $arguments); 153 | 154 | $this->model = call_user_func_array(array($this->model, 'with'), $arguments); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * Reset and includes soft deletes for following queries. 161 | * 162 | * @return $this 163 | */ 164 | public function startWithTrashed() 165 | { 166 | /** 167 | * Save to conditons. 168 | */ 169 | $this->addCondition('withTrashed', 'withTrashed'); 170 | 171 | $this->model = $this->model->withTrashed(); 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Reset and only includes soft deletes for following queries. 178 | * 179 | * @return $this 180 | */ 181 | public function startWithTrashedOnly() 182 | { 183 | /** 184 | * Save to conditons. 185 | */ 186 | $this->addCondition('onlyTrashed', 'onlyTrashed'); 187 | 188 | $this->model = $this->model->onlyTrashed(); 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * @return array 195 | */ 196 | public function __sleep() 197 | { 198 | return [ 199 | 'conditions', 200 | ]; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Housekeeper/Support/InjectionContainer.php: -------------------------------------------------------------------------------- 1 | 18 | * @package Housekeeper\Support 19 | */ 20 | class InjectionContainer 21 | { 22 | /** 23 | * @var string 24 | */ 25 | const GROUP_BEFORE = 'before'; 26 | 27 | /** 28 | * @var string 29 | */ 30 | const GROUP_AFTER = 'after'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | const GROUP_RESET = 'reset'; 36 | 37 | /** 38 | * All injections. 39 | * 40 | * @var array 41 | */ 42 | protected $groupedInjections = [ 43 | self::GROUP_BEFORE => [], 44 | self::GROUP_AFTER => [], 45 | self::GROUP_RESET => [], 46 | ]; 47 | 48 | /** 49 | * @param $group 50 | * @param $injection 51 | * @param bool $sort 52 | */ 53 | protected function addInjection($group, $injection, $sort = true) 54 | { 55 | $this->groupedInjections[$group][] = $injection; 56 | 57 | // If need to sort all injections after inject. 58 | if ($sort) { 59 | $this->sortInjections($group); 60 | } 61 | } 62 | 63 | /** 64 | * @param \Housekeeper\Contracts\Injection\Before $injection 65 | * @param bool $sort 66 | */ 67 | public function addBeforeInjection(BeforeInjectionContract $injection, $sort = true) 68 | { 69 | $this->addInjection(static::GROUP_BEFORE, $injection, $sort); 70 | } 71 | 72 | /** 73 | * @param \Housekeeper\Contracts\Injection\After $injection 74 | * @param bool $sort 75 | */ 76 | public function addAfterInjection(AfterInjectionContract $injection, $sort = true) 77 | { 78 | $this->addInjection(static::GROUP_AFTER, $injection, $sort); 79 | } 80 | 81 | /** 82 | * @param \Housekeeper\Contracts\Injection\Reset $injection 83 | * @param bool $sort 84 | */ 85 | public function addResetInjection(ResetInjectionContract $injection, $sort = true) 86 | { 87 | $this->addInjection(static::GROUP_RESET, $injection, $sort); 88 | } 89 | 90 | /** 91 | * @param $group 92 | * @param $flow 93 | */ 94 | protected function handleFlow($group, $flow) 95 | { 96 | foreach ($this->groupedInjections[$group] as $injection) { 97 | /** 98 | * @var $injection \Housekeeper\Contracts\Injection\Before|\Housekeeper\Contracts\Injection\After|\Housekeeper\Contracts\Injection\Reset 99 | */ 100 | $injection->handle($flow); 101 | } 102 | } 103 | 104 | /** 105 | * @param \Housekeeper\Contracts\Flow\Before $flow 106 | */ 107 | public function handleBeforeFlow(BeforeFlowContract $flow) 108 | { 109 | $this->handleFlow(static::GROUP_BEFORE, $flow); 110 | } 111 | 112 | /** 113 | * @param \Housekeeper\Contracts\Flow\After $flow 114 | */ 115 | public function handleAfterFlow(AfterFlowContract $flow) 116 | { 117 | $this->handleFlow(static::GROUP_AFTER, $flow); 118 | } 119 | 120 | /** 121 | * @param \Housekeeper\Contracts\Flow\Reset $flow 122 | */ 123 | public function handleResetFlow(ResetFlowContract $flow) 124 | { 125 | $this->handleFlow(static::GROUP_RESET, $flow); 126 | } 127 | 128 | /** 129 | * Sort by priority ASC. 130 | * 131 | * @param string|null $group 132 | */ 133 | public function sortInjections($group = null) 134 | { 135 | if ($group) { 136 | usort($this->groupedInjections[$group], [$this, 'sortInjection']); 137 | } else { 138 | foreach ($this->groupedInjections as $injections) { 139 | usort($injections, [$this, 'sortInjection']); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Custom function for "usort" used by "sortAllInjections". 146 | * 147 | * @param \Housekeeper\Contracts\Injection\Basic $a 148 | * @param \Housekeeper\Contracts\Injection\Basic $b 149 | * @return int 150 | */ 151 | static protected function sortInjection(BasicInjectionContract $a, BasicInjectionContract $b) 152 | { 153 | if ($a->priority() == $b->priority()) { 154 | return 0; 155 | } 156 | 157 | return ($a->priority() < $b->priority()) ? - 1 : 1; 158 | } 159 | } -------------------------------------------------------------------------------- /src/Housekeeper/Abilities/Eloquently.php: -------------------------------------------------------------------------------- 1 | 15 | * @package Housekeeper\Traits\Repository 16 | */ 17 | trait Eloquently 18 | { 19 | /** 20 | * @param $column 21 | * @param null $operator 22 | * @param null $value 23 | * @param string $boolean 24 | * @return $this 25 | */ 26 | public function where($column, $operator = null, $value = null, $boolean = 'and') 27 | { 28 | $this->whereAre([ 29 | [$column, $operator, $value, $boolean], 30 | ]); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @param $column 37 | * @param null $operator 38 | * @param null $value 39 | * @return $this 40 | */ 41 | public function orWhere($column, $operator = null, $value = null) 42 | { 43 | $this->whereAre([ 44 | [$column, $operator, $value, 'or'], 45 | ]); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param $relation 52 | * @param string $operator 53 | * @param int $count 54 | * @param string $boolean 55 | * @param \Closure|null $callback 56 | * @return $this 57 | */ 58 | public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null) 59 | { 60 | $this->whereAre([ 61 | function ($query) use (&$relation, &$operator, &$count, &$boolean, &$callback) { 62 | /** 63 | * @var $query \Illuminate\Database\Eloquent\Builder 64 | */ 65 | $query->has($relation, $operator, $count, $boolean, $callback); 66 | }, 67 | ]); 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @param $relation 74 | * @param \Closure $callback 75 | * @param string $operator 76 | * @param int $count 77 | * @return $this 78 | */ 79 | public function whereHas($relation, \Closure $callback, $operator = '>=', $count = 1) 80 | { 81 | return $this->has($relation, $operator, $count, 'and', $callback); 82 | } 83 | 84 | /** 85 | * @param $relation 86 | * @param \Closure|null $callback 87 | * @return \Housekeeper\Abilities\Eloquently 88 | */ 89 | public function whereDoesntHave($relation, \Closure $callback = null) 90 | { 91 | return $this->has($relation, '<', 1, 'and', $callback); 92 | } 93 | 94 | /** 95 | * @param $relation 96 | * @param \Closure $callback 97 | * @param string $operator 98 | * @param int $count 99 | * @return \Housekeeper\Abilities\Eloquently 100 | */ 101 | public function orWhereHas($relation, \Closure $callback, $operator = '>=', $count = 1) 102 | { 103 | return $this->has($relation, $operator, $count, 'or', $callback); 104 | } 105 | 106 | /** 107 | * @param $column 108 | * @param $values 109 | * @param string $boolean 110 | * @param bool $not 111 | * @return \Housekeeper\Abilities\Eloquently 112 | */ 113 | public function whereIn($column, $values, $boolean = 'and', $not = false) 114 | { 115 | return $this->whereAre([ 116 | function ($query) use ($column, $values, $boolean, $not) { 117 | /** 118 | * @var $query Builder 119 | */ 120 | $query->whereIn($column, $values, $boolean, $not); 121 | }, 122 | ]); 123 | } 124 | 125 | /** 126 | * @param $column 127 | * @param $values 128 | * @param string $boolean 129 | * @return \Housekeeper\Abilities\Eloquently 130 | */ 131 | public function whereNotIn($column, $values, $boolean = 'and') 132 | { 133 | return $this->whereIn($column, $values, $boolean, true); 134 | } 135 | 136 | /** 137 | * @param $column 138 | * @param $values 139 | * @return \Housekeeper\Abilities\Eloquently 140 | */ 141 | public function orWhereNotIn($column, $values) 142 | { 143 | return $this->whereNotIn($column, $values, 'or'); 144 | } 145 | 146 | /** 147 | * @param $column 148 | * @param string $boolean 149 | * @param bool $not 150 | * @return \Housekeeper\Abilities\Eloquently 151 | */ 152 | public function whereNull($column, $boolean = 'and', $not = false) 153 | { 154 | $type = $not ? 'NotNull' : 'Null'; 155 | 156 | return $this->whereAre([ 157 | compact('type', 'column', 'boolean'), 158 | ]); 159 | } 160 | 161 | /** 162 | * @param $column 163 | * @return \Housekeeper\Abilities\Eloquently 164 | */ 165 | public function orWhereNull($column) 166 | { 167 | return $this->whereNull($column, 'or'); 168 | } 169 | 170 | /** 171 | * @param $column 172 | * @param string $boolean 173 | * @return \Housekeeper\Abilities\Eloquently 174 | */ 175 | public function whereNotNull($column, $boolean = 'and') 176 | { 177 | return $this->whereNull($column, $boolean, true); 178 | } 179 | 180 | /** 181 | * @param $column 182 | * @return \Housekeeper\Abilities\Eloquently 183 | */ 184 | public function orWhereNotNull($column) 185 | { 186 | return $this->whereNotNull($column, 'or'); 187 | } 188 | } -------------------------------------------------------------------------------- /src/Housekeeper/Console/Generators/MakeRepositoryCommand.php: -------------------------------------------------------------------------------- 1 | 14 | * @package Housekeeper\Console\Generators 15 | */ 16 | class MakeRepositoryCommand extends GeneratorCommand 17 | { 18 | /** 19 | * The console command name. 20 | * 21 | * @var string 22 | */ 23 | protected $signature = 'housekeeper:make' . 24 | ' {name : The name of the repository}' . 25 | ' {--a|adjustable : Allow you to reuse queries}' . 26 | ' {--cache= : Chose from 2 strategies to caching result: statically}' . 27 | ' {--cs : Short for "--cache=statically"}' . 28 | ' {--create= : Create a new model file for the repository.}' . 29 | ' {--e|eloquently : With frequently-used Eloquent-Style query methods}' . 30 | ' {--g|guardable : Enable mass-assignment protection in model}' . 31 | ' {--metadata : Convert all result that implemented `Arrayable` to array automatically}' . 32 | ' {--model= : Specify the model used by the repository (Root Namespace "\App").}' . 33 | ' {--sd : Allow you to interact with the `SoftDeletes` trait of Eloquent.}' . 34 | ' {--vintage : With backward compatible APIs for Housekeeper `0.9.x`}'; 35 | 36 | /** 37 | * The console command description. 38 | * 39 | * @var string 40 | */ 41 | protected $description = 'Make a Housekeeper repository class file.'; 42 | 43 | /** 44 | * @var string 45 | */ 46 | protected $type = 'Repository'; 47 | 48 | /** 49 | * @var array 50 | */ 51 | protected $cacheAbilities = ['statically']; 52 | 53 | 54 | /** 55 | * Execute the console command. 56 | * 57 | * @return void 58 | */ 59 | public function handle() 60 | { 61 | $name = $this->parseName($this->getNameInput()); 62 | 63 | $path = $this->getPath($name); 64 | 65 | if ($this->alreadyExists($this->getNameInput())) { 66 | $this->error($this->type . " \"{$name}\" already exists!"); 67 | 68 | return; 69 | } 70 | 71 | if (! $this->checkOptions()) { 72 | return; 73 | } 74 | 75 | $this->makeDirectory($path); 76 | 77 | $this->files->put($path, $this->buildClass($name)); 78 | 79 | $this->info($this->type . " \"{$name}\" created successfully."); 80 | 81 | // If also needs a new model file 82 | if ($model = $this->option('create')) { 83 | $this->call('make:model', ['name' => $model]); 84 | } 85 | } 86 | 87 | /** 88 | * @return bool 89 | */ 90 | protected function checkOptions() 91 | { 92 | $cache = $this->option('cache'); 93 | 94 | if ($cache && ! in_array($cache, $this->cacheAbilities)) { 95 | $this->error("The \"cache\" option must be one of these: \"" . implode(',', $this->cacheAbilities) . "\"."); 96 | 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | /** 104 | * Build the class with the given name. 105 | * 106 | * @param string $name 107 | * @return string 108 | */ 109 | protected function buildClass($name) 110 | { 111 | $stub = $this->files->get($this->getStub()); 112 | 113 | return $this 114 | ->replaceNamespace($stub, $name) 115 | ->replaceModel($stub) 116 | ->replaceAbility($stub) 117 | ->replaceClass($stub, $name); 118 | } 119 | 120 | /** 121 | * @param $stub 122 | * @return $this 123 | */ 124 | protected function replaceModel(&$stub) 125 | { 126 | $model = ''; 127 | 128 | if ($model = ($this->option('create') ?: $this->option('model'))) { 129 | $rootNamespace = $this->getLaravel()->getNamespace(); 130 | $modelNamespace = (Str::startsWith($model, $rootNamespace) ? '\\' : '\\App\\'); 131 | $model = "return {$modelNamespace}{$model}::class;"; 132 | } else { 133 | $model = '//'; 134 | } 135 | 136 | $stub = str_replace('DummyModel', $model, $stub); 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @param $stub 143 | * @return $this 144 | */ 145 | protected function replaceAbility(&$stub) 146 | { 147 | $use = ''; 148 | $trait = ''; 149 | 150 | $traits = []; 151 | 152 | if ($this->option('adjustable')) { 153 | $use .= "use Housekeeper\\Abilities\\Adjustable;\n"; 154 | $traits[] = "Adjustable"; 155 | } 156 | 157 | if ($cache = $this->option('cache')) { 158 | $cache = ucfirst($cache); 159 | 160 | $use .= "use Housekeeper\\Abilities\\Cache{$cache};\n"; 161 | $traits[] = "Cache$cache"; 162 | } elseif ($this->option('cs')) { 163 | $use .= "use Housekeeper\\Abilities\\CacheStatically;\n"; 164 | $traits[] = 'CacheStatically'; 165 | } 166 | 167 | if ($this->option('guardable')) { 168 | $use .= "use Housekeeper\\Abilities\\Guardable;\n"; 169 | $traits[] = "Guardable"; 170 | } 171 | 172 | if ($this->option('metadata')) { 173 | $use .= "use Housekeeper\\Abilities\\Metadata;\n"; 174 | $traits[] = "Metadata"; 175 | } 176 | 177 | if ($this->option('eloquently')) { 178 | $use .= "use Housekeeper\\Abilities\\Eloquently;\n"; 179 | $traits[] = "Eloquently"; 180 | } 181 | 182 | if ($this->option('vintage')) { 183 | $use .= "use Housekeeper\\Abilities\\Vintage;\n"; 184 | $traits[] = "Vintage"; 185 | } 186 | 187 | if ($this->option('sd')) { 188 | $use .= "use Housekeeper\\Abilities\\SoftDeletes;\n"; 189 | $traits[] = "SoftDeletes"; 190 | } 191 | 192 | if (! empty($traits)) { 193 | $trait = "\n use " . implode(',', $traits) . ";\n"; 194 | } 195 | 196 | $stub = str_replace('DummyUse', $use, $stub); 197 | $stub = str_replace('DummyTrait', $trait, $stub); 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Get the stub file for the generator. 204 | * 205 | * @return string 206 | */ 207 | protected function getStub() 208 | { 209 | return __DIR__ . '/stubs/repository.stub'; 210 | } 211 | 212 | /** 213 | * Get the default namespace for the class. 214 | * 215 | * @param string $rootNamespace 216 | * @return string 217 | */ 218 | protected function getDefaultNamespace($rootNamespace) 219 | { 220 | $config = $this->getLaravel()->make('config'); 221 | 222 | $namespace = '\\' . 223 | trim( 224 | str_replace( 225 | '/', '\\', 226 | $config['housekeeper']['directory'] 227 | ), 228 | '\\' 229 | ); 230 | 231 | return $rootNamespace . $namespace; 232 | } 233 | 234 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/Housekeeper/Repository.php: -------------------------------------------------------------------------------- 1 | 27 | * @package Housekeeper 28 | * @version 2.3.1 29 | */ 30 | abstract class Repository implements RepositoryContract 31 | { 32 | /** 33 | * The name of `Boot Method`. 34 | */ 35 | const BOOT_METHOD = 'boot'; 36 | 37 | /** 38 | * @var \Illuminate\Contracts\Foundation\Application 39 | */ 40 | protected $app; 41 | 42 | /** 43 | * @var string 44 | */ 45 | protected $fullModelClassName; 46 | 47 | /** 48 | * @var int 49 | */ 50 | protected $planStep; 51 | 52 | /** 53 | * @var \Housekeeper\Plan[] 54 | */ 55 | protected $plans; 56 | 57 | /** 58 | * @var \Housekeeper\Plan 59 | */ 60 | protected $defaultPlan; 61 | 62 | /** 63 | * @var InjectionContainer 64 | */ 65 | protected $injectionContainer; 66 | 67 | /** 68 | * Page size for pagination. 69 | * 70 | * @var int 71 | */ 72 | protected $perPage; 73 | 74 | 75 | /** 76 | * Specify the full class name of model for this repository. 77 | * 78 | * @return string 79 | */ 80 | abstract protected function model(); 81 | 82 | /** 83 | * Developer could write a `Boot Method` instead `__construct` in child 84 | * class to make program easier. 85 | * 86 | * @param \Illuminate\Contracts\Foundation\Application $app 87 | */ 88 | public function __construct(Application $app) 89 | { 90 | $this->setApp($app); 91 | 92 | $this->initialize(); 93 | 94 | $this->callBootable(); 95 | 96 | // All official traits for Repository are injecting Injection without 97 | // sorting for better performance, so when injecting finished, then sort 98 | // them at once. 99 | $this->sortAllInjections(); 100 | 101 | // Call the `Boot Method` fo the child with Dependency Injection process 102 | // if that method exists. 103 | // This provide an easy way to add custom logic that will be executed 104 | // when repository been created. 105 | $this->callBoot(); 106 | 107 | // Reset to prepare everything that would be used. 108 | $this->reset(new Action(Action::INTERNAL, [], __METHOD__)); 109 | } 110 | 111 | /** 112 | * 113 | */ 114 | protected function callBoot() 115 | { 116 | if (method_exists($this, static::BOOT_METHOD)) { 117 | $this->getApp()->call([$this, static::BOOT_METHOD]); 118 | } 119 | } 120 | 121 | /** 122 | * @param \Illuminate\Contracts\Foundation\Application $app 123 | */ 124 | protected function setApp(Application $app) 125 | { 126 | $this->app = $app; 127 | } 128 | 129 | /** 130 | * @return \Illuminate\Contracts\Foundation\Application 131 | */ 132 | protected function getApp() 133 | { 134 | return $this->app; 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | protected function getKeyName() 141 | { 142 | return $this->newModelInstance()->getKeyName(); 143 | } 144 | 145 | /** 146 | * Make a new Model instance. 147 | * 148 | * @return Model 149 | */ 150 | protected function newModelInstance() 151 | { 152 | if (is_null($this->fullModelClassName)) { 153 | $this->fullModelClassName = $this->model(); 154 | } 155 | 156 | return new $this->fullModelClassName; 157 | } 158 | 159 | /** 160 | * Read configure from configure file, if it's not exists, "default" will be 161 | * returned. 162 | * 163 | * @param $key 164 | * @param null $default 165 | * @return mixed 166 | */ 167 | protected function getConfig($key, $default = null) 168 | { 169 | $config = $this->getApp()->make('config'); 170 | 171 | return $config->get($key, $default); 172 | } 173 | 174 | /** 175 | * Validate the model that provided by `model` method, and load configures. 176 | */ 177 | protected function initialize() 178 | { 179 | $this->injectionContainer = new InjectionContainer(); 180 | 181 | $model = $this->newModelInstance(); 182 | 183 | // The model instance must be an instance of `Model` class from 184 | // `Laravel`, otherwise just throw an exception. 185 | if (! $model instanceof Model) { 186 | throw new RepositoryException( 187 | "Class \"" . get_class($model) . "\" must be an instance of " . Model::class 188 | ); 189 | } 190 | 191 | // Load configures from `housekeeper.php` or just use default settings. 192 | $this->perPage = $this->getConfig('housekeeper.paginate.per_page', 15); 193 | } 194 | 195 | /** 196 | * @return \Housekeeper\Support\InjectionContainer 197 | */ 198 | protected function getInjectionContainer() 199 | { 200 | return $this->injectionContainer; 201 | } 202 | 203 | /** 204 | * Call all methods that name start with "boot" (must followed by an 205 | * upper-case latter) with DI process. 206 | * This allow us to encapsulate injecting logics in trait. 207 | */ 208 | protected function callBootable() 209 | { 210 | $reflection = new \ReflectionClass($this); 211 | 212 | foreach ($reflection->getMethods() as $method) { 213 | $methodName = $method->getName(); 214 | 215 | // Method name has to start with "boot" and followed by an 216 | // upper-case latter. 217 | if (preg_match('/^boot[A-Z]/', $methodName)) { 218 | $this->getApp()->call([$this, $methodName]); 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Reset the Plan object for the next use. 225 | */ 226 | protected function resetPlan() 227 | { 228 | $this->defaultPlan = new Plan($this->newModelInstance()); 229 | $this->plans = []; 230 | $this->planStep = null; 231 | } 232 | 233 | /** 234 | * @return \Housekeeper\Plan 235 | */ 236 | public function getCurrentPlan() 237 | { 238 | return $this->defaultPlan ?: $this->plans[$this->planStep]; 239 | } 240 | 241 | /** 242 | * @param $offset 243 | */ 244 | protected function dropPlan($offset) 245 | { 246 | unset($this->plans[$offset]); 247 | } 248 | 249 | /** 250 | * @return int 251 | */ 252 | protected function newPlan() 253 | { 254 | if ($this->defaultPlan) { 255 | $offset = $this->planStep = 0; 256 | 257 | $this->plans[$offset] = $this->defaultPlan; 258 | $this->defaultPlan = null; 259 | } else { 260 | $offset = ++ $this->planStep; 261 | $this->plans[$offset] = new Plan($this->newModelInstance()); 262 | } 263 | 264 | return $offset; 265 | } 266 | 267 | /** 268 | * @return int 269 | */ 270 | private function getCurrentWrappedMethodIndex() 271 | { 272 | return count($this->plans); 273 | } 274 | 275 | /** 276 | * Get model instance from Plan. 277 | * 278 | * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder 279 | */ 280 | protected function getModel() 281 | { 282 | return $this->getCurrentPlan()->getModel(); 283 | } 284 | 285 | /** 286 | * @param \Housekeeper\Contracts\Action $action 287 | * @return $this 288 | */ 289 | protected function reset(ActionContract $action) 290 | { 291 | /** 292 | * Conditions are use for identify each method call. 293 | */ 294 | $this->resetPlan(); 295 | 296 | /** 297 | * Make a Reset Flow object. 298 | */ 299 | $flow = new ResetFlow($this, $action, $this->getCurrentWrappedMethodIndex()); 300 | 301 | /** 302 | * Execute all `Reset` Injections. 303 | */ 304 | $this->getInjectionContainer()->handleResetFlow($flow); 305 | 306 | return $this; 307 | } 308 | 309 | /** 310 | * @param \Housekeeper\Action $action 311 | * @return \Housekeeper\Flows\Before 312 | */ 313 | protected function before(Action $action) 314 | { 315 | /** 316 | * Make a Before Flow ojbect. 317 | */ 318 | $flow = new BeforeFlow($this, $action, $this->getCurrentWrappedMethodIndex()); 319 | 320 | /** 321 | * Execute all `Before` Injections. 322 | */ 323 | $this->getInjectionContainer()->handleBeforeFlow($flow); 324 | 325 | return $flow; 326 | } 327 | 328 | /** 329 | * @param \Housekeeper\Action $action 330 | * @param $returnValue 331 | * @return \Housekeeper\Flows\After 332 | */ 333 | protected function after(Action $action, $returnValue) 334 | { 335 | // Make a After Flow ojbect. 336 | $flow = new AfterFlow($this, $action, $this->getCurrentWrappedMethodIndex(), $returnValue); 337 | 338 | // Execute all `After` Injections. 339 | $this->getInjectionContainer()->handleAfterFlow($flow); 340 | 341 | return $flow; 342 | } 343 | 344 | /** 345 | * @param callable $function 346 | * @param array $args 347 | * @param int|ActionContract $actionType 348 | * @return mixed 349 | * @throws \Exception 350 | */ 351 | protected function wrap(callable $function, 352 | array $args, 353 | $actionType = ActionContract::UNKNOW) 354 | { 355 | // Prepare a Plan object for this wrapped function and returns the 356 | // offset. This will allow you to call another wrapped internal function 357 | // that even had queries like "$this->getModel()->where('name', 'kid')" 358 | // without any affection to each other. 359 | $planOffset = $this->newPlan(); 360 | 361 | // Action indecated this method calling. 362 | $action = ($actionType instanceof ActionContract) ? 363 | $actionType : 364 | new Action( 365 | $actionType, 366 | [], 367 | $this->getMethodNameOfCallable($function), 368 | $args 369 | ); 370 | 371 | // First it's the Before Flow, if there has any returned in this Flow, 372 | // then use it as the final returns, jump to the Reset Flow and return 373 | // the result. 374 | $beforeFlow = $this->before($action); 375 | 376 | if ($beforeFlow->hasReturnValue()) { 377 | $this->reset($action); 378 | 379 | return $beforeFlow->getReturnValue(); 380 | } 381 | 382 | // Next, execute the wrapped function and goes to the After Flow. 383 | try { 384 | $result = call_user_func_array($function, $action->getArguments()); 385 | 386 | // After wrapped function executed, it's the After Flow. In this 387 | // Flow, injection may alter returns, thus the final returns are 388 | // come from the After Flow. 389 | $afterFlow = $this->after($action, $result); 390 | 391 | // Release memory of the Plan, since it will not be used anymore. 392 | $this->dropPlan($planOffset); 393 | 394 | return $afterFlow->getReturnValue(); 395 | } catch (\Exception $e) { 396 | // Bubble up the exception. 397 | throw $e; 398 | } finally { 399 | // No matter what happens, must go to the Reset Flow. 400 | $this->reset($action); 401 | } 402 | } 403 | 404 | /** 405 | * @param callable $function 406 | * @return string 407 | */ 408 | private function getMethodNameOfCallable(callable $function) 409 | { 410 | return ($function instanceof \Closure) ? 411 | '\\Closure' : 412 | $function[1]; 413 | } 414 | 415 | /** 416 | * @param int|ActionContract $actionType 417 | * @param callable|null $function 418 | * @return mixed 419 | * @throws \Exception 420 | */ 421 | protected function simpleWrap($actionType, callable $function = null) 422 | { 423 | $caller = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; 424 | 425 | return $this->wrap( 426 | ($function ?: $this->traceToRealMethod($caller['function'])), 427 | $caller['args'], 428 | $actionType 429 | ); 430 | } 431 | 432 | /** 433 | * Get the real method that will be executed. 434 | * 435 | * By convention, the real method should be named start with an underscore 436 | * follow by the cover method's name, for instance: cover method named 437 | * "getUserName", so the real method should be "_getUserName". 438 | * 439 | * @param null|string $coverMethodName 440 | * @return Callable|array 441 | */ 442 | protected function traceToRealMethod($coverMethodName = null) 443 | { 444 | if (! $coverMethodName) { 445 | $coverMethodName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function']; 446 | } 447 | 448 | return [$this, "_{$coverMethodName}"]; 449 | } 450 | 451 | /** 452 | * @param \Housekeeper\Contracts\Injection\Before $injection 453 | * @param bool $sort 454 | */ 455 | protected function injectIntoBefore(BeforeInjectionContract $injection, $sort = true) 456 | { 457 | $this->getInjectionContainer()->addBeforeInjection($injection, $sort); 458 | } 459 | 460 | /** 461 | * @param \Housekeeper\Contracts\Injection\After $injection 462 | * @param bool $sort 463 | */ 464 | protected function injectIntoAfter(AfterInjectionContract $injection, $sort = true) 465 | { 466 | $this->getInjectionContainer()->addAfterInjection($injection, $sort); 467 | } 468 | 469 | /** 470 | * @param \Housekeeper\Contracts\Injection\Reset $injection 471 | * @param bool $sort 472 | */ 473 | protected function injectIntoReset(ResetInjectionContract $injection, $sort = true) 474 | { 475 | $this->getInjectionContainer()->addResetInjection($injection, $sort); 476 | } 477 | 478 | /** 479 | * Sort all event handlers by priority ASC. 480 | */ 481 | protected function sortAllInjections() 482 | { 483 | $this->getInjectionContainer()->sortInjections(); 484 | } 485 | 486 | /** 487 | * This is more semantic than `applyWheres`. 488 | * 489 | * @param array $wheres 490 | * @return $this 491 | */ 492 | public function whereAre(array $wheres) 493 | { 494 | $this->getCurrentPlan()->applyWheres($wheres); 495 | 496 | return $this; 497 | } 498 | 499 | /** 500 | * @param array $wheres 501 | * @return $this 502 | */ 503 | public function applyWheres(array $wheres) 504 | { 505 | return $this->whereAre($wheres); 506 | } 507 | 508 | /** 509 | * @param $column 510 | * @param string $direction 511 | * @return $this 512 | */ 513 | public function orderBy($column, $direction = 'asc') 514 | { 515 | $this->getCurrentPlan()->applyOrderBy($column, $direction); 516 | 517 | return $this; 518 | } 519 | 520 | /** 521 | * Same as the `orderBy`. 522 | * 523 | * @param $column 524 | * @param string $direction 525 | * @return $this 526 | */ 527 | public function applyOrderBy($column, $direction = 'asc') 528 | { 529 | return $this->orderBy($column, $direction); 530 | } 531 | 532 | /** 533 | * Set the relationships that should be eager loaded, just like Eloquent. 534 | * 535 | * @return $this 536 | */ 537 | public function with() 538 | { 539 | call_user_func_array([$this->getCurrentPlan(), 'applyWith'], func_get_args()); 540 | 541 | return $this; 542 | } 543 | 544 | /** 545 | * @param $value 546 | * @return $this 547 | */ 548 | public function offset($value) 549 | { 550 | $this->getCurrentPlan()->applyOffset($value); 551 | 552 | return $this; 553 | } 554 | 555 | /** 556 | * @param $value 557 | * @return $this 558 | */ 559 | public function limit($value) 560 | { 561 | $this->getCurrentPlan()->applyLimit($value); 562 | 563 | return $this; 564 | } 565 | 566 | /** 567 | * @param $id 568 | * @param null $primaryKeyName 569 | * @return mixed 570 | * @throws \Exception 571 | */ 572 | public function exists($id, $primaryKeyName = null) 573 | { 574 | return $this->simpleWrap(Action::READ, [$this, '_exists']); 575 | } 576 | 577 | /** 578 | * @param $id 579 | * @param null $primaryKeyName 580 | * @return bool 581 | */ 582 | protected function _exists($id, $primaryKeyName = null) 583 | { 584 | /** 585 | * Fix auto-completion. 586 | * 587 | * @var \Illuminate\Database\Query\Builder $model 588 | */ 589 | $model = $this->getModel(); 590 | 591 | $primaryKeyName = ($primaryKeyName ?: $model->getKeyName()); 592 | 593 | return $model->where($primaryKeyName, $id)->exists(); 594 | } 595 | 596 | /** 597 | * @param string $columns 598 | * @return int 599 | */ 600 | public function count($columns = '*') 601 | { 602 | return $this->simpleWrap(Action::READ, [$this, '_count']); 603 | } 604 | 605 | /** 606 | * @param string $columns 607 | * @return int 608 | */ 609 | protected function _count($columns = '*') 610 | { 611 | return $this->getModel()->count($columns); 612 | } 613 | 614 | /** 615 | * @param $id 616 | * @param array $columns 617 | * @return Model 618 | * @throws \Exception 619 | */ 620 | public function find($id, $columns = ['*']) 621 | { 622 | return $this->simpleWrap(Action::READ, [$this, '_find']); 623 | } 624 | 625 | /** 626 | * Find data by id 627 | * 628 | * @param $id 629 | * @param array $columns 630 | * @return Model 631 | */ 632 | protected function _find($id, $columns = ['*']) 633 | { 634 | return $this->getModel()->findOrFail($id, $columns); 635 | } 636 | 637 | /** 638 | * Same as the "findMany" method of Eloquent. 639 | * 640 | * @param array $ids 641 | * @param array $columns 642 | * @return EloquentCollection 643 | */ 644 | public function findMany($ids, $columns = ['*']) 645 | { 646 | return $this->simpleWrap(Action::READ, [$this, '_findMany']); 647 | } 648 | 649 | /** 650 | * @param array $ids 651 | * @param array $columns 652 | * @return EloquentCollection 653 | */ 654 | protected function _findMany($ids, $columns = ['*']) 655 | { 656 | return $this->getModel()->findMany($ids, $columns); 657 | } 658 | 659 | /** 660 | * Save a new model and return the instance, like the same method of 661 | * Eloquent. 662 | * 663 | * @param array $attributes 664 | * @return Model 665 | * @throws \Exception 666 | */ 667 | public function create(array $attributes) 668 | { 669 | return $this->simpleWrap(Action::CREATE, [$this, '_create']); 670 | } 671 | 672 | /** 673 | * Save a new entity in repository 674 | * 675 | * @param array $attributes 676 | * @return Model 677 | */ 678 | protected function _create(array $attributes) 679 | { 680 | $model = $this->newModelInstance()->forceFill($attributes); 681 | 682 | $model->save(); 683 | 684 | return $model; 685 | } 686 | 687 | /** 688 | * @param $id 689 | * @return mixed 690 | * @throws ModelNotFoundException 691 | */ 692 | public function delete($id) 693 | { 694 | return $this->simpleWrap(Action::DELETE, [$this, '_delete']); 695 | } 696 | 697 | /** 698 | * @param $id 699 | * @return bool|null 700 | * @throws ModelNotFoundException 701 | */ 702 | protected function _delete($id) 703 | { 704 | $model = $this->getModel()->findOrFail($id); 705 | 706 | $deleted = $model->delete(); 707 | 708 | return $deleted; 709 | } 710 | 711 | /** 712 | * Update a record in the database, could use a model as $id. 713 | * 714 | * @param mixed $id 715 | * @param array $attributes 716 | * @return Model 717 | */ 718 | public function update($id, array $attributes) 719 | { 720 | return $this->simpleWrap(Action::UPDATE, [$this, '_update']); 721 | } 722 | 723 | /** 724 | * @param mixed $id 725 | * @param array $attributes 726 | * @return Model 727 | */ 728 | protected function _update($id, array $attributes) 729 | { 730 | if ($id instanceof Model) { 731 | $id = $id->getKey(); 732 | } 733 | 734 | $model = $this->getModel() 735 | ->findOrFail($id) 736 | ->forceFill($attributes); 737 | 738 | $model->save(); 739 | 740 | return $model; 741 | } 742 | 743 | /** 744 | * Retrieve first data of repository 745 | * 746 | * @param array $columns 747 | * @return Model 748 | */ 749 | public function first($columns = ['*']) 750 | { 751 | return $this->simpleWrap(Action::READ, [$this, '_first']); 752 | } 753 | 754 | /** 755 | * @param array $columns 756 | * @return Model 757 | */ 758 | protected function _first($columns = ['*']) 759 | { 760 | return $this->getModel()->first($columns); 761 | } 762 | 763 | /** 764 | * Retrieve all data of repository 765 | * 766 | * @param array $columns 767 | * @return EloquentCollection 768 | */ 769 | public function all($columns = ['*']) 770 | { 771 | return $this->simpleWrap(Action::READ, [$this, '_all']); 772 | } 773 | 774 | /** 775 | * @param array $columns 776 | * @return EloquentCollection 777 | */ 778 | protected function _all($columns = ['*']) 779 | { 780 | return $this->getModel()->get($columns); 781 | } 782 | 783 | /** 784 | * Same as the "paginate" method of Eloquent. 785 | * 786 | * @param int|null $limit 787 | * @param array $columns 788 | * @return LengthAwarePaginator 789 | */ 790 | public function paginate($limit = null, $columns = ['*'], $pageName = 'page', $page = null) 791 | { 792 | return $this->simpleWrap(Action::READ, [$this, '_paginate']); 793 | } 794 | 795 | /** 796 | * @param int|null $limit 797 | * @param array $columns 798 | * @return LengthAwarePaginator 799 | */ 800 | protected function _paginate($limit = null, $columns = ['*'], $pageName = 'page', $page = null) 801 | { 802 | $limit = $limit ?: $this->perPage; 803 | 804 | return $this->getModel()->paginate($limit, $columns, $pageName, $page); 805 | } 806 | 807 | /** 808 | * Get models by a simple equality query. 809 | * 810 | * @param $field 811 | * @param null $value 812 | * @param array $columns 813 | * @return array|EloquentCollection 814 | */ 815 | public function getByField($field, $value = null, $columns = ['*']) 816 | { 817 | return $this->simpleWrap(Action::READ, [$this, '_getByField']); 818 | } 819 | 820 | /** 821 | * @param $field 822 | * @param null $value 823 | * @param array $columns 824 | * @return array|EloquentCollection 825 | */ 826 | protected function _getByField($field, $value = null, $columns = ['*']) 827 | { 828 | return $this->getModel()->where($field, '=', $value)->get($columns); 829 | } 830 | 831 | /** 832 | * @deprecated This is not a frequently used method. 833 | * 834 | * Get one model by a simple equality query. 835 | * 836 | * @param $field 837 | * @param null $value 838 | * @param array $columns 839 | * @return Model 840 | * @throws \Illuminate\Database\Eloquent\ModelNotFoundException 841 | */ 842 | public function findByField($field, $value = null, $columns = ['*']) 843 | { 844 | return $this->simpleWrap(Action::READ, [$this, '_findByField']); 845 | } 846 | 847 | /** 848 | * @param $field 849 | * @param null $value 850 | * @param array $columns 851 | * @return Model 852 | * @throws \Illuminate\Database\Eloquent\ModelNotFoundException 853 | */ 854 | protected function _findByField($field, $value = null, $columns = ['*']) 855 | { 856 | return $this->getModel()->where($field, '=', $value)->firstOrFail($columns); 857 | } 858 | } 859 | -------------------------------------------------------------------------------- /tests/Housekeeper/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | 22 | * @package Housekeeper\Eloquent 23 | */ 24 | class RepositoryTest extends \PHPUnit_Framework_TestCase 25 | { 26 | /** 27 | * 28 | */ 29 | protected function setUp() 30 | { 31 | 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | public function tearDown() 38 | { 39 | m::close(); 40 | } 41 | 42 | /** 43 | * @runInSeparateProcess 44 | * @covers Housekeeper\Repository::setApp 45 | * @covers Housekeeper\Repository::getApp 46 | */ 47 | public function testSetAppAndGetApp() 48 | { 49 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, true); 50 | 51 | $mockApp = $this->makeMockApplication(); 52 | 53 | $methodSetApp = getUnaccessibleObjectMethod($mockRepository, 'setApp'); 54 | $methodGetApp = getUnaccessibleObjectMethod($mockRepository, 'getApp'); 55 | 56 | $appFromRepository = $methodGetApp->invoke($mockRepository); 57 | $this->assertInstanceOf('Illuminate\Contracts\Foundation\Application', $appFromRepository); 58 | 59 | $methodSetApp->invoke($mockRepository, $mockApp); 60 | $appFromRepositoryLater = $methodGetApp->invoke($mockRepository); 61 | $this->assertEquals($mockApp, $appFromRepositoryLater); 62 | } 63 | 64 | /** 65 | * @runInSeparateProcess 66 | * @covers Housekeeper\Repository::getConfig 67 | */ 68 | public function testGetConfig() 69 | { 70 | $keyShouldBePassed = 'aaron'; 71 | $defaultShouldBePassed = 'jan'; 72 | $keyPassed = false; 73 | $defaultPassed = false; 74 | 75 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 76 | 77 | $mockConfig = m::mock('Illuminate\Config\Repository'); 78 | $mockConfig->shouldReceive('get') 79 | ->andReturnUsing(function ($key, $default) use ($keyShouldBePassed, $defaultShouldBePassed, &$keyPassed, &$defaultPassed) { 80 | if ($key === $keyShouldBePassed) { 81 | $keyPassed = true; 82 | } 83 | 84 | if ($default === $defaultShouldBePassed) { 85 | $defaultPassed = true; 86 | } 87 | }); 88 | 89 | $mockApplication = m::mock('Illuminate\Contracts\Foundation\Application'); 90 | $mockApplication->shouldReceive('make') 91 | ->with('config') 92 | ->andReturn($mockConfig); 93 | 94 | // Set Application instance manually 95 | $methodSetApp = getUnaccessibleObjectMethod($mockRepository, 'setApp'); 96 | $methodSetApp->invoke($mockRepository, $mockApplication); 97 | 98 | $methodGetConfig = getUnaccessibleObjectMethod($mockRepository, 'getConfig'); 99 | $methodGetConfig->invoke($mockRepository, $keyShouldBePassed, $defaultShouldBePassed); 100 | 101 | $this->assertTrue($keyPassed); 102 | $this->assertTrue($defaultPassed); 103 | } 104 | 105 | /** 106 | * @covers Housekeeper\Repository::newModelInstance 107 | */ 108 | public function testNewModelInstance() 109 | { 110 | $methodModelCalled = false; 111 | 112 | $mockRepository = m::mock(MockSetupRepository::class); 113 | $mockRepository->makePartial() 114 | ->shouldAllowMockingProtectedMethods(); 115 | 116 | $mockRepository->shouldReceive('model') 117 | ->once() 118 | ->andReturnUsing(function () use (&$methodModelCalled) { 119 | $methodModelCalled = true; 120 | 121 | return MockModel::class; 122 | }); 123 | 124 | $methodNewModelInstance = getUnaccessibleObjectMethod($mockRepository, 'newModelInstance'); 125 | $model = $methodNewModelInstance->invoke($mockRepository); 126 | 127 | $this->assertInstanceOf(MockModel::class, $model); 128 | $this->assertTrue($methodModelCalled); 129 | 130 | $model = $methodNewModelInstance->invoke($mockRepository); 131 | 132 | $this->assertInstanceOf(MockModel::class, $model); 133 | } 134 | 135 | /** 136 | * @covers Housekeeper\Repository::initialize 137 | */ 138 | public function testInitializeInNormal() 139 | { 140 | $methodNewModelInstanceCalled = false; 141 | 142 | $fakeConfigs = [ 143 | 'housekeeper.paginate.per_page' => 10, 144 | ]; 145 | 146 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 147 | 148 | $mockRepository->shouldReceive('newModelInstance') 149 | ->andReturnUsing(function () use (&$methodNewModelInstanceCalled) { 150 | $methodNewModelInstanceCalled = true; 151 | 152 | return m::mock('Illuminate\Database\Eloquent\Model'); 153 | }); 154 | 155 | $mockRepository->shouldReceive('getConfig') 156 | ->andReturnUsing(function ($key, $default) use ($fakeConfigs) { 157 | return $fakeConfigs[$key]; 158 | }); 159 | 160 | $methodInitialize = getUnaccessibleObjectMethod($mockRepository, 'initialize'); 161 | $methodInitialize->invoke($mockRepository); 162 | 163 | $attributePerPage = getUnaccessibleObjectPropertyValue($mockRepository, 'perPage'); 164 | $this->assertEquals($fakeConfigs['housekeeper.paginate.per_page'], $attributePerPage); 165 | } 166 | 167 | /** 168 | * @covers Housekeeper\Repository::initialize 169 | * @expectedException \Housekeeper\Exceptions\RepositoryException 170 | */ 171 | public function testInitializeExpectException() 172 | { 173 | $methodNewModelInstanceCalled = false; 174 | 175 | $fakeConfigs = [ 176 | 'housekeeper.paginate.per_page' => 10, 177 | ]; 178 | 179 | $mockRepository = m::mock(MockSetupRepository::class); 180 | $mockRepository->makePartial() 181 | ->shouldAllowMockingProtectedMethods(); 182 | 183 | $mockRepository->shouldReceive('newModelInstance') 184 | ->andReturnUsing(function () use (&$methodNewModelInstanceCalled) { 185 | $methodNewModelInstanceCalled = true; 186 | 187 | return new \stdClass(); 188 | }); 189 | 190 | $mockRepository->shouldReceive('getConfig') 191 | ->andReturnUsing(function ($key, $default) use ($fakeConfigs) { 192 | return $fakeConfigs[$key]; 193 | }); 194 | 195 | $methodInitialize = getUnaccessibleObjectMethod($mockRepository, 'initialize'); 196 | $methodInitialize->invoke($mockRepository); 197 | } 198 | 199 | /** 200 | * @runInSeparateProcess 201 | * @covers Housekeeper\Repository::callBootable 202 | */ 203 | public function testCallBootable() 204 | { 205 | $methodBootTestOneCalled = false; 206 | $methodBootTestTwoCalled = false; 207 | 208 | $mockApplication = m::mock('Illuminate\Contracts\Foundation\Application'); 209 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 210 | 211 | $mockApplication->shouldReceive('call') 212 | ->andReturnUsing(function ($callable) use (&$methodBootTestOneCalled, &$methodBootTestTwoCalled) { 213 | list($object, $methodName) = $callable; 214 | 215 | if ($methodName == 'bootTestOne') { 216 | $methodBootTestOneCalled = true; 217 | } elseif ($methodName == 'bootTestTwo') { 218 | $methodBootTestTwoCalled = true; 219 | } 220 | }); 221 | $methodSetApp = getUnaccessibleObjectMethod($mockRepository, 'setApp'); 222 | $methodSetApp->invoke($mockRepository, $mockApplication); 223 | 224 | $methodCallBootable = getUnaccessibleObjectMethod($mockRepository, 'callBootable'); 225 | $methodCallBootable->invoke($mockRepository); 226 | 227 | $this->assertTrue($methodBootTestOneCalled); 228 | $this->assertTrue($methodBootTestTwoCalled); 229 | } 230 | 231 | /** 232 | * @runInSeparateProcess 233 | * @covers Housekeeper\Repository::callBoot 234 | */ 235 | public function testCallBoot() 236 | { 237 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 238 | 239 | $mockApplication = $this->makeMockApplication(); 240 | $mockApplication->shouldReceive('call'); 241 | 242 | $mockRepository->shouldReceive('getApp')->andReturn($mockApplication); 243 | 244 | $methodCallBoot = getUnaccessibleObjectMethod($mockRepository, 'callBoot'); 245 | $methodCallBoot->invoke($mockRepository); 246 | 247 | $mockApplication->shouldHaveReceived('call')->withAnyArgs()->once(); 248 | } 249 | 250 | /** 251 | * @runInSeparateProcess 252 | * @depends testSetAppAndGetApp 253 | * @depends testCallBoot 254 | * @depends testCallBootable 255 | * @depends testInitializeInNormal 256 | * @depends testInitializeExpectException 257 | * @covers Housekeeper\Repository::__construct 258 | */ 259 | public function testConstruct() 260 | { 261 | $methodsWillBeCalledByOrder = [ 262 | 'setApp', 263 | 'initialize', 264 | 'callBootable', 265 | 'sortAllInjections', 266 | 'callBoot', 267 | 'reset', 268 | ]; 269 | $methodsCalledByOrder = []; 270 | 271 | $mockApplication = $this->makeMockApplication(); 272 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 273 | 274 | // `__construct` method will call these methods 275 | $mockRepository->shouldReceive('setApp') 276 | ->andReturnUsing(function ($app) use (&$mockApplication, &$methodsCalledByOrder) { 277 | $method = array_push($methodsCalledByOrder, 'setApp'); 278 | 279 | $this->assertEquals($mockApplication, $app); 280 | }); 281 | $mockRepository->shouldReceive('initialize') 282 | ->andReturnUsing(function () use (&$methodsCalledByOrder) { 283 | array_push($methodsCalledByOrder, 'initialize'); 284 | }); 285 | $mockRepository->shouldReceive('callBootable') 286 | ->andReturnUsing(function () use (&$methodsCalledByOrder) { 287 | array_push($methodsCalledByOrder, 'callBootable'); 288 | }); 289 | $mockRepository->shouldReceive('sortAllInjections') 290 | ->andReturnUsing(function () use (&$methodsCalledByOrder) { 291 | array_push($methodsCalledByOrder, 'sortAllInjections'); 292 | }); 293 | $mockRepository->shouldReceive('callBoot') 294 | ->andReturnUsing(function () use (&$methodsCalledByOrder) { 295 | array_push($methodsCalledByOrder, 'callBoot'); 296 | }); 297 | $mockRepository->shouldReceive('reset') 298 | ->andReturnUsing(function ($action) use (&$methodsCalledByOrder) { 299 | array_push($methodsCalledByOrder, 'reset'); 300 | 301 | $this->assertInstanceOf(Action::class, $action); 302 | 303 | $this->assertEquals(Action::INTERNAL, $action->getType()); 304 | }); 305 | 306 | $mockRepository->__construct($mockApplication); 307 | 308 | $this->assertEquals($methodsWillBeCalledByOrder, $methodsCalledByOrder); 309 | } 310 | 311 | /** 312 | * @covers Housekeeper\Repository::getKeyName 313 | */ 314 | public function testGetKeyName() 315 | { 316 | /** 317 | * @var $mockModel m\MockInterface 318 | */ 319 | $mockRepository = m::mock(MockSetupRepository::class); 320 | $mockRepository->makePartial()->shouldAllowMockingProtectedMethods(); 321 | 322 | $mockModel = $this->makeMockModel(); 323 | $mockModel->shouldReceive('getKeyName')->withNoArgs(); 324 | 325 | $mockRepository->shouldReceive('newModelInstance') 326 | ->andReturnUsing(function () use (&$mockModel) { 327 | return $mockModel; 328 | }); 329 | 330 | $methodGetKeyName = getUnaccessibleObjectMethod($mockRepository, 'getKeyName'); 331 | $methodGetKeyName->invoke($mockRepository); 332 | 333 | $mockModel->shouldHaveReceived('getKeyName', [])->once(); 334 | } 335 | 336 | /** 337 | * @runInSeparateProcess 338 | * @covers Housekeeper\Repository::resetPlan 339 | */ 340 | public function testResetPlan() 341 | { 342 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 343 | 344 | // `resetPlan` method will change these properties, these should be `null` now 345 | $propertyDefaultPlan = getUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan'); 346 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 347 | $propertyPlanStep = getUnaccessibleObjectPropertyValue($mockRepository, 'planStep'); 348 | $this->assertNull($propertyDefaultPlan); 349 | $this->assertNull($propertyPlans); 350 | $this->assertNull($propertyPlanStep); 351 | 352 | $methodResetPlan = getUnaccessibleObjectMethod($mockRepository, 'resetPlan'); 353 | $methodResetPlan->invoke($mockRepository); 354 | 355 | // properties should be changed 356 | $propertyDefaultPlan = getUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan'); 357 | $this->assertInstanceOf(\Housekeeper\Plan::class, $propertyDefaultPlan); 358 | 359 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 360 | $this->assertInternalType('array', $propertyPlans); 361 | $this->assertEmpty($propertyPlans); 362 | 363 | $propertyPlanStep = getUnaccessibleObjectPropertyValue($mockRepository, 'planStep'); 364 | $this->assertNull($propertyPlanStep); 365 | } 366 | 367 | /** 368 | * @covers Housekeeper\Repository::reset 369 | */ 370 | public function testReset() 371 | { 372 | 373 | } 374 | 375 | /** 376 | * @covers Housekeeper\Repository::getCurrentPlan 377 | */ 378 | public function testGetCurrentPlanWhenDefault() 379 | { 380 | $randomNumber = mt_rand(1, 1000); 381 | 382 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 383 | 384 | setUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan', $randomNumber); 385 | 386 | $actualValue = $mockRepository->getCurrentPlan(); 387 | 388 | $this->assertEquals($randomNumber, $actualValue); 389 | } 390 | 391 | /** 392 | * @covers Housekeeper\Repository::getCurrentPlan 393 | */ 394 | public function testGetCurrentPlanWhenNotDefault() 395 | { 396 | $randomNumber = mt_rand(1, 1000); 397 | 398 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 399 | 400 | setUnaccessibleObjectPropertyValue($mockRepository, 'plans', [$randomNumber]); 401 | setUnaccessibleObjectPropertyValue($mockRepository, 'planStep', 0); 402 | 403 | $actualValue = $mockRepository->getCurrentPlan(); 404 | 405 | $this->assertEquals($randomNumber, $actualValue); 406 | } 407 | 408 | /** 409 | * @covers Housekeeper\Repository::dropPlan 410 | */ 411 | public function testDropPlan() 412 | { 413 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 414 | 415 | setUnaccessibleObjectPropertyValue($mockRepository, 'plans', [1]); 416 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 417 | $this->assertEquals([1], $propertyPlans); 418 | 419 | // Execute `dropPlan` to remove the value setted before 420 | $methodDropPlan = getUnaccessibleObjectMethod($mockRepository, 'dropPlan'); 421 | $methodDropPlan->invoke($mockRepository, 0); 422 | 423 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 424 | $this->assertEquals([], $propertyPlans); 425 | } 426 | 427 | /** 428 | * @covers Housekeeper\Repository::newPlan 429 | */ 430 | public function testNewPlanWhenHaveDefault() 431 | { 432 | $expectDefaultPlan = mt_rand(1, 1000); 433 | 434 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 435 | 436 | setUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan', $expectDefaultPlan); 437 | setUnaccessibleObjectPropertyValue($mockRepository, 'plans', []); 438 | 439 | $methodNewPlan = getUnaccessibleObjectMethod($mockRepository, 'newPlan'); 440 | $offset = $methodNewPlan->invoke($mockRepository); 441 | 442 | $propertyPlanStep = getUnaccessibleObjectPropertyValue($mockRepository, 'planStep'); 443 | $propertyDefaultPlan = getUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan'); 444 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 445 | 446 | $this->assertEquals(0, $offset); 447 | $this->assertEquals(0, $propertyPlanStep); 448 | $this->assertNull($propertyDefaultPlan); 449 | $this->assertEquals([$expectDefaultPlan], $propertyPlans); 450 | } 451 | 452 | /** 453 | * @runInSeparateProcess 454 | * @covers Housekeeper\Repository::newPlan 455 | */ 456 | public function testNewPlanWhenDoNotHaveDefault() 457 | { 458 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 459 | 460 | setUnaccessibleObjectPropertyValue($mockRepository, 'defaultPlan', null); 461 | setUnaccessibleObjectPropertyValue($mockRepository, 'plans', [1]); 462 | setUnaccessibleObjectPropertyValue($mockRepository, 'planStep', 0); 463 | 464 | $methodNewPlan = getUnaccessibleObjectMethod($mockRepository, 'newPlan'); 465 | $offset = $methodNewPlan->invoke($mockRepository); 466 | 467 | $propertyPlanStep = getUnaccessibleObjectPropertyValue($mockRepository, 'planStep'); 468 | $propertyPlans = getUnaccessibleObjectPropertyValue($mockRepository, 'plans'); 469 | 470 | $this->assertEquals(1, $offset); 471 | $this->assertEquals(1, $propertyPlanStep); 472 | $this->assertCount(2, $propertyPlans); 473 | 474 | $plan = $propertyPlans[1]; 475 | $this->assertInstanceOf(\Housekeeper\Plan::class, $plan); 476 | } 477 | 478 | /** 479 | * @covers Housekeeper\Repository::getModel 480 | */ 481 | public function testGetModel() 482 | { 483 | $mockPlan = m::mock(); 484 | $mockPlan->shouldReceive('getModel') 485 | ->andReturn(''); 486 | 487 | $mockRepository = $this->makeMockRepository(MockSetupRepository::class, false); 488 | 489 | $mockRepository->shouldReceive('getCurrentPlan') 490 | ->andReturn($mockPlan); 491 | 492 | $methodGetModel = getUnaccessibleObjectMethod($mockRepository, 'getModel'); 493 | $methodGetModel->invoke($mockRepository); 494 | 495 | $mockPlan->shouldHaveReceived('getModel')->once(); 496 | } 497 | 498 | /** 499 | * @covers Housekeeper\Repository::before 500 | */ 501 | public function testBefore() 502 | { 503 | 504 | } 505 | 506 | /** 507 | * @covers Housekeeper\Repository::after 508 | */ 509 | public function testAfter() 510 | { 511 | 512 | } 513 | 514 | /** 515 | * @covers Housekeeper\Repository::wrap 516 | */ 517 | public function testWrap() 518 | { 519 | 520 | } 521 | 522 | /** 523 | * @covers Housekeeper\Repository::getMethodNameofCallable 524 | */ 525 | public function testGetMethodNameOfCallable() 526 | { 527 | 528 | } 529 | 530 | /** 531 | * @covers Housekeeper\Repository::simpleWrap 532 | */ 533 | public function testSimpleWrap() 534 | { 535 | 536 | } 537 | 538 | /** 539 | * @covers Housekeeper\Repository::traceToRealMethod 540 | */ 541 | public function testTraceToRealMethod() 542 | { 543 | 544 | } 545 | 546 | /** 547 | * @covers Housekeeper\Repository::whereAre 548 | * @covers Housekeeper\Repository::applyWheres 549 | */ 550 | public function testWhereAre() 551 | { 552 | 553 | } 554 | 555 | /** 556 | * @covers Housekeeper\Repository::orderBy 557 | * @covers Housekeeper\Repository::applyOrderBy 558 | */ 559 | public function testOrderBy() 560 | { 561 | 562 | } 563 | 564 | /** 565 | * @covers Housekeeper\Repository::with 566 | */ 567 | public function testWith() 568 | { 569 | 570 | } 571 | 572 | /** 573 | * @covers Housekeeper\Repository::offset 574 | */ 575 | public function testOffset() 576 | { 577 | 578 | } 579 | 580 | /** 581 | * @covers Housekeeper\Repository::limit 582 | */ 583 | public function testLimit() 584 | { 585 | 586 | } 587 | 588 | /** 589 | * @covers Housekeeper\Repository::exists 590 | */ 591 | public function testExists() 592 | { 593 | 594 | } 595 | 596 | /** 597 | * @covers Housekeeper\Repository::find 598 | * @covers Housekeeper\Repository::_find 599 | */ 600 | public function testFind() 601 | { 602 | 603 | } 604 | 605 | /** 606 | * @covers Housekeeper\Repository::findMany 607 | * @covers Housekeeper\Repository::_findMany 608 | */ 609 | public function testFindMany() 610 | { 611 | 612 | } 613 | 614 | /** 615 | * @covers Housekeeper\Repository::create 616 | * @covers Housekeeper\Repository::_create 617 | */ 618 | public function testCreate() 619 | { 620 | 621 | } 622 | 623 | /** 624 | * @covers Housekeeper\Repository::delete 625 | * @covers Housekeeper\Repository::_delete 626 | */ 627 | public function testDelete() 628 | { 629 | 630 | } 631 | 632 | /** 633 | * @covers Housekeeper\Repository::update 634 | * @covers Housekeeper\Repository::_update 635 | */ 636 | public function testUpdate() 637 | { 638 | 639 | } 640 | 641 | /** 642 | * @covers Housekeeper\Repository::all 643 | * @covers Housekeeper\Repository::_all 644 | */ 645 | public function testAll() 646 | { 647 | 648 | } 649 | 650 | /** 651 | * @covers Housekeeper\Repository::paginate 652 | * @covers Housekeeper\Repository::_paginate 653 | */ 654 | public function testPaginate() 655 | { 656 | 657 | } 658 | 659 | /** 660 | * @covers Housekeeper\Repository::getByField 661 | * @covers Housekeeper\Repository::_getByField 662 | */ 663 | public function testGetByField() 664 | { 665 | 666 | } 667 | 668 | /** 669 | * @covers Housekeeper\Repository::findByField 670 | * @covers Housekeeper\_findByField::_firstOrFailByField 671 | */ 672 | public function testFindByField() 673 | { 674 | 675 | } 676 | 677 | /** 678 | * Internal method calling should creating different layer to isolate 679 | * outcome from each other. 680 | */ 681 | public function testIsolationWhenCallingInternalMethod() 682 | { 683 | 684 | } 685 | 686 | 687 | // ======================================================================== 688 | 689 | /** 690 | * @param string $class 691 | * @param bool $concrete 692 | * @return Repository|m\MockInterface 693 | */ 694 | protected function makeMockRepository($class = 'Housekeeper\Repository', 695 | $concrete = true) 696 | { 697 | /** 698 | * Setup some hints for variables. 699 | * 700 | * @var \Housekeeper\Repository|\Mockery\MockInterface $mockRepository 701 | */ 702 | 703 | /** 704 | * Mock Repository 705 | */ 706 | $mockRepository = m::mock($class); 707 | $mockRepository->makePartial() 708 | ->shouldAllowMockingProtectedMethods(); 709 | 710 | /** 711 | * Override "newModelInstance" method, returns a mock model. 712 | */ 713 | $mockRepository->shouldReceive('newModelInstance') 714 | ->andReturnUsing(function () { 715 | return $this->makeMockModel(); 716 | }); 717 | 718 | /** 719 | * Once we mocked "makeModel" method, we can safely Re-concreting 720 | * Repository object. 721 | */ 722 | if ($concrete) { 723 | $mockRepository->__construct($this->makeMockApplication()); 724 | } 725 | 726 | return $mockRepository; 727 | } 728 | 729 | /** 730 | * @return m\MockInterface|\Illuminate\Contracts\Foundation\Application 731 | */ 732 | protected function makeMockApplication() 733 | { 734 | /** 735 | * Mock "Config" instance. 736 | */ 737 | $mockConfig = m::mock('Illuminate\Config\Repository'); 738 | 739 | $mockConfig->shouldReceive('get') 740 | ->andReturnNull(); 741 | 742 | /** 743 | * Mock "Application". 744 | */ 745 | $mockApplication = m::mock('Illuminate\Contracts\Foundation\Application'); 746 | 747 | $mockApplication->shouldReceive('config') 748 | ->andReturn([]); 749 | 750 | $mockApplication->shouldReceive('make') 751 | ->with('config') 752 | ->andReturn($mockConfig); 753 | 754 | $mockApplication->shouldReceive('make') 755 | ->with() 756 | ->andReturn([]); 757 | 758 | $mockApplication->shouldReceive('call') 759 | ->andReturn([]); 760 | 761 | return $mockApplication; 762 | } 763 | 764 | /** 765 | * @param string $class 766 | * @return m\MockInterface 767 | */ 768 | protected function makeMockModel($class = 'Illuminate\Database\Eloquent\Model') 769 | { 770 | $mock = m::mock($class); 771 | 772 | $mock->shouldReceive('get') 773 | ->andReturn(m::mock('Illuminate\Database\Eloquent\Collection')); 774 | 775 | return $mock; 776 | } 777 | 778 | /** 779 | * @return m\MockInterface 780 | */ 781 | protected function makeMockAction() 782 | { 783 | $mock = m::mock('Housekeeper\Action'); 784 | 785 | $mock->shouldReceive('getArguments') 786 | ->andReturn([]); 787 | 788 | $mock->shouldReceive('getMethodName') 789 | ->andReturn('fake'); 790 | 791 | return $mock; 792 | } 793 | 794 | } 795 | 796 | // ============================================================================ 797 | 798 | /** 799 | * Class MockBasicInjection 800 | * 801 | * @package Housekeeper 802 | */ 803 | class MockBasicInjection implements BasicInjectionContract 804 | 805 | { 806 | /** 807 | * @return int 808 | */ 809 | public function priority() 810 | { 811 | return 1; 812 | } 813 | } 814 | 815 | /** 816 | * Class MockSetupRepository 817 | * 818 | * @package Housekeeper 819 | */ 820 | class MockSetupRepository extends Repository 821 | { 822 | /** 823 | * 824 | */ 825 | protected function model() 826 | { 827 | 828 | } 829 | 830 | public function boot() 831 | { 832 | 833 | } 834 | 835 | /** 836 | * 837 | */ 838 | public function bootTestOne() 839 | { 840 | $mockInjection = new MockBasicInjection(); 841 | 842 | $this->inject($mockInjection); 843 | } 844 | 845 | /** 846 | * 847 | */ 848 | public function bootTestTwo() 849 | { 850 | 851 | } 852 | 853 | /** 854 | * @param \Housekeeper\Contracts\Injection\Basic $a 855 | * @param \Housekeeper\Contracts\Injection\Basic $b 856 | * @return int 857 | */ 858 | protected static function sortInjection(\Housekeeper\Contracts\Injection\Basic $a, \Housekeeper\Contracts\Injection\Basic $b) 859 | { 860 | return 0; 861 | } 862 | } 863 | 864 | /** 865 | * Class MockModel 866 | * 867 | * @package Housekeeper 868 | */ 869 | class MockModel 870 | { 871 | 872 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Housekeeper 3 | 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/aaronjan/housekeeper/v/stable)](https://packagist.org/packages/aaronjan/housekeeper) 6 | [![License](https://poser.pugx.org/aaronjan/housekeeper/license)](https://packagist.org/packages/aaronjan/housekeeper) 7 | 8 | 9 | # Housekeeper - Laravel 10 | 11 | After nearly six months developing, testing and polishing, the first stable version of `Housekeeper 2` is finally released! 12 | 13 | `Housekeeper` aims to be the coolest, handiest `Repository Pattern` implementation, any useful suggestion and PR are welcomed. 14 | 15 | Increasing unit test code coverage is a work in progress (lots of works), but there is a set of integration tests running locally that covered most code. 16 | 17 | 18 | ## Introduction 19 | 20 | `Housekeeper` is a flexable and powerful `Repository Pattern` implemention for `Laravel`. In addition to the basic `Repository Pattern` and elegant syntax, `Housekeeper` has features like `Injection system`, Auto-Booting Method that will let you creating endless possibilities. The goal of `Housekeeper` is free you from the redundant and boring [`DAL`](https://en.wikipedia.org/wiki/Data_access_layer) stuff, coding more intuitively. 21 | 22 | 23 | ## Sections 24 | 25 | - [Repository Pattern and Housekeeper](#repository-pattern-and-housekeeper) 26 | - [Installation](#installation) 27 | - [TL;DR (Quick start)](#tldr) 28 | - [Features](#features) 29 | - [API](#api) 30 | - [Abilities](#abilities) 31 | - [Adjustable](#adjustable) 32 | - [Eloquently](#eloquently) 33 | - [CacheStatically](#cachestatically) 34 | - [Guardable](#guardable) 35 | - [SoftDeletes](#softdeletes) 36 | - [Issue](#issue) 37 | - [Lisence](#lisence) 38 | - [Credits](#credits) 39 | 40 | ## Repository Pattern and Housekeeper 41 | 42 | The `Repository Pattern` is a software design pattern. In a nutshell, it means to encapsulate your data interaction code as methods that belong to different classes (Base on data domain), we call this type of class as `Repository`. When your business logic layer needs to accessing data such as an article entry in the database, it should ask to the `Article Repository` instead of writing inline query that deal with database directly. 43 | 44 | OK, but ... I already got Eloquent, why not just using that? 45 | 46 | Of course you can! But there're people who's not a fan of the Active Record, it just doesn't feel right for them, for these people, the Repository Pattern makes more sense. Besides, you can write method that is more expressive on your repository class, like **getActivatedUsers()**, and you can write tests for them very easily. 47 | 48 | More importantly, `Housekeeper` is a better version of `Repository Pattern` (In some ways), you could read more about it below. 49 | 50 | Housekeeper loves Eloquent. Most query APIs are the same as the Eloquent's, so you can use them without the needing to learn anything, and the returns are like Eloquent's too. 51 | 52 | 53 | ## Installation 54 | 55 | ### Requirement 56 | 57 | `PHP` `>= 5.5` and `Laravel` `>= 5.1` 58 | 59 | ### Install Via Composer 60 | 61 | ``` 62 | $ composer require aaronjan/housekeeper ~2.3 63 | ``` 64 | 65 | or add these to your `composer.json` file: 66 | 67 | ``` 68 | "require": { 69 | "aaronjan/housekeeper": "~2.3" 70 | } 71 | ``` 72 | 73 | then execute console command: 74 | 75 | ``` 76 | $ composer install 77 | ``` 78 | 79 | After `Composer` finish running, add the `HousekeeperServiceProvider` to the providers in `config/app.php`: 80 | 81 | ```php 82 | [ 87 | // ... 88 | 89 | // Add this: 90 | \Housekeeper\Providers\HousekeeperServiceProvider::class, 91 | 92 | // ... 93 | ], 94 | 95 | // ... 96 | 97 | ``` 98 | 99 | Make a configuration file for `Housekeeper` could allow you to tweak things: 100 | 101 | ``` 102 | $ artisan vendor:publish --provider=Housekeeper\\Providers\\HousekeeperServiceProvider --tag=config 103 | ``` 104 | 105 | It's done! Now you can make a repository: 106 | 107 | ``` 108 | $ artisan housekeeper:make UserRepository 109 | ``` 110 | 111 | 112 | ## TL;DR 113 | 114 | If you have outstanding insight, this section will tell you how to use `Housekeeper` in the simplest words. 115 | 116 | * Do not write `class constructor`, use `boot` method instead, supports `Type-Hinting`. 117 | 118 | * Any public method that name starts with **boot** and followed by an upper-case letter (for instance, **bootForInject**), then this method will be called during class initializing, also support `Type-Hinting`. 119 | 120 | * If you want to do something before/after some methods belong across multiple repository, encapsulate these logics as `Injections` then inject them into your repositorys. 121 | 122 | * By using the two features above, you can write `Trait` to inject `Injection`, and **use** it in your repository. 123 | 124 | Take a example: 125 | 126 | ```php 127 | injectIntoBefore(new LogTimeBefore()); 157 | } 158 | } 159 | 160 | ``` 161 | 162 | ```php 163 | simpleWrap(Action::READ, [$this, '_getByName']); 207 | } 208 | 209 | protected function _getByName($name) 210 | { 211 | return $this->getModel() // this function give you an Eloquent / Builder instance 212 | ->where('name', '=', $name) 213 | ->get(); 214 | } 215 | } 216 | 217 | ``` 218 | 219 | That's it, take a look at the `Abilities`' code for more usages, Have fun! 220 | 221 | 222 | ## Features 223 | 224 | ### Extend-less & Flows 225 | 226 | The `Repository Pattern` is usually used with the `Decorator Pattern`. For instance, you have a repository class to interacting with data source directly, later you decide to add cache logic on top of that, so instead of changing the repository class its self, you could create a new class that extending it, and the whole thing may looks like this: 227 | 228 | ```php 229 | simpleWrap(Action::READ, [$this, '_getByName']); 275 | } 276 | 277 | protected function _getByName($name) 278 | { 279 | return $this->getModel() // this function give you an Eloquent / Builder instance 280 | ->where('name', '=', $name) 281 | ->get(); 282 | } 283 | } 284 | ``` 285 | 286 | Why there are two methods that had similar names? Well, the `getByName` method is basically a configuration and an API hint for the core method `_getByName`, it wrapped the core method by calling the `simpleWrap` with an `Callable` which is `[$this, '_getByName']`, It says what this method does is `reading` data (`Action::READ`), the whole reading logic is in the `_getByName` method. 287 | 288 | You don't have to worry about method arguments, `Housekeeper` will takes care of that. In fact, you don't even need to write `[$this, '_getByName']`, since it's a convention in `Housekeeper` (An underscore before your method name): 289 | 290 | ```php 291 | simpleWrap(Action::READ); 296 | } 297 | 298 | ``` 299 | 300 | Let's back to the `cache logic` topic. In `Housekeeper`, if you wrapped your method like above, than to adding cache process, all you need to do is writing a single line of code like this: 301 | 302 | ```php 303 | getAction(); 341 | 342 | // Or get the `Repository` 343 | $repository = $beforeFlow->getRepository(); 344 | 345 | // And you can set the returns (Only in `Before Flow`) 346 | $beforeFlow->setReturnValue(1); 347 | } 348 | } 349 | ``` 350 | 351 | The `handle` method in `Injection` takes a `Flow` object, depends on what `Flow` you injected into, the methods of the `Flow` object could be different, for instance, `Before Flow` provides `setReturnValue` method, you could call it by pass a value to it, then `Housekeeper` will use this value as the return and skip the actual method. 352 | 353 | You can inject `Injection` by using the these methods: `injectIntoBefore`, `injectIntoAfter` and `injectIntoReset`. 354 | 355 | ```php 356 | injectIntoBefore(new MyBeforeInjection()); 364 | } 365 | 366 | // ... 367 | } 368 | ``` 369 | 370 | 371 | Here is flowchart of the `Before Flow` execution: 372 | 373 | ![method execution in Housekeeper](https://aaronjan.github.io/Housekeeper/2.x.x/images/flow.png) 374 | 375 | `Housekeeper` also will calling every method in the `Repository` class that name start with `boot` (before calling the `boot` method) when `Repository` instance been creating, some of the out-of-the-box `Abilities` in `Housekeeper` are took advantage of this, like in `Adjustable` trait: 376 | 377 | ```php 378 | injectIntoBefore(new ApplyCriteriasBefore()); 387 | } 388 | 389 | // ... 390 | } 391 | ``` 392 | 393 | 394 | ### Wrapping layer 395 | 396 | Let's assume someone wrote code like these: 397 | 398 | ```php 399 | simpleWrap(Action::READ); 408 | } 409 | 410 | protected function _getArticlesByAuthorId($authorId) 411 | { 412 | return $this 413 | ->applyWheres([ 414 | ['author_id', '=', $authorId], 415 | ]) 416 | ->get(); 417 | } 418 | 419 | public function getArticlesBySameAuthor($articleId) 420 | { 421 | return $this->simpleWrap(Action::READ); 422 | } 423 | 424 | protected function _getArticlesBySameAuthor($articleId) 425 | { 426 | $article = $this->getModel()->find($articleId, ['id', 'author_id']); 427 | 428 | return $this->getArticlesByAuthorId($article->author_id); 429 | } 430 | 431 | 432 | // ... 433 | } 434 | ``` 435 | 436 | ```php 437 | applyWheres([ 445 | ['language', '=', 'chinese'], 446 | ]) 447 | ->getArticlesBySameAuthor($articleId); 448 | 449 | return view('article.recommend-for-article', compact('articles')); 450 | } 451 | 452 | // ... 453 | } 454 | ``` 455 | 456 | In this example, the `applyWheres` method has been used twice, one is in the `Controller`, the other is in the `Repository`, could the first one affecting the `_getArticlesByAuthorId` method? No. It will only affecting the `_getArticlesBySameAuthor` method, and be more precisely, it's affecting this line: 457 | 458 | ```php 459 | $article = $this->getModel()->find($articleId, ['id', 'author_id']); 460 | ``` 461 | 462 | Every wrapped method in `Housekeeper` has their own `Scope`, means they have their own `Eloquent Model` (Or `Builder`), thus they will not taking any affect to each other. If you calling `applyWheres` or `ApplyOrderBy` outside the repository, they would only affecting the first wrapped method you called. 463 | 464 | 465 | ### Another Choice For Wrapping 466 | 467 | Having two methods could be annoying, you can write an `Anonymous Function`, before the `simpleWrap` takes a `Callable`: 468 | 469 | ```php 470 | simpleWrap(Action::READ, function (name) { 475 | return $this->getModel() 476 | ->where('name', '=', $name) 477 | ->get(); 478 | }); 479 | } 480 | ``` 481 | 482 | 483 | ## API 484 | 485 | ----------------------------- 486 | 487 | #### whereAre(array $wheres) 488 | 489 | Add an array of where clauses to the query. 490 | 491 | __Arguments__ 492 | 493 | * `$wheres` - An array of `where` conditions. 494 | 495 | __Example__ 496 | 497 | ```php 498 | whereAre([ 502 | ['age', '>', 40], 503 | ['area', 'west'] 504 | ]) 505 | ->all(); 506 | ``` 507 | 508 | ```php 509 | whereAre([ 513 | ['area', 'east'], 514 | function ($query) { 515 | $query->whereHas('posts', function ($hasQuery) { 516 | $hasQuery->where('type', 1); 517 | }); 518 | 519 | $query->whereNull('has_membership'); 520 | }, 521 | ]) 522 | ->paginate(12); 523 | 524 | ``` 525 | 526 | 527 | ----------------------------- 528 | 529 | #### applyWheres(array $wheres) 530 | 531 | Alias for the `whereAre` method. 532 | 533 | __Arguments__ 534 | 535 | * `$wheres` - An array of `where` conditions. 536 | 537 | 538 | ----------------------------- 539 | 540 | #### orderBy($column, $direction = 'asc') 541 | 542 | Add an "order by" clause to the query. 543 | 544 | __Arguments__ 545 | 546 | * `$column` 547 | * `$direction` 548 | 549 | __Example__ 550 | 551 | ```php 552 | orderBy('age', 'desc') 556 | ->all(); 557 | ``` 558 | 559 | 560 | ----------------------------- 561 | 562 | #### applyOrderBy($column, $direction = 'asc') 563 | 564 | Alias for the `orderBy` method. 565 | 566 | __Arguments__ 567 | 568 | * `$column` 569 | * `$direction` 570 | 571 | 572 | ----------------------------- 573 | 574 | #### offset($value) 575 | 576 | Set the "offset" value of the query. 577 | 578 | __Arguments__ 579 | 580 | * `$value` - The specified offset of the first row to return. 581 | 582 | __Example__ 583 | 584 | ```php 585 | limit(10) 589 | ->all(); 590 | ``` 591 | 592 | 593 | ----------------------------- 594 | 595 | #### limit($value) 596 | 597 | Set the "limit" value of the query. 598 | 599 | __Arguments__ 600 | 601 | * `$value` - The maximum number of rows to return. 602 | 603 | __Example__ 604 | 605 | ```php 606 | limit(10) 610 | ->all(); 611 | ``` 612 | 613 | 614 | ----------------------------- 615 | 616 | #### exists($id, $column = null) 617 | 618 | Determine if the record exists using its primary key. 619 | 620 | __Arguments__ 621 | 622 | * `$id` - The primary key of the record. 623 | * `$column` - You could also specify a column other than primary key, and change the value of `$id` correspondingly. 624 | 625 | __Examples__ 626 | 627 | ```php 628 | exists(3); 631 | 632 | ``` 633 | 634 | ```php 635 | exists('name', 'John'); 638 | 639 | ``` 640 | 641 | You could use this method with custom query conditions too: 642 | 643 | ```php 644 | whereAre(['gender' => 'female'])->exists(1); 647 | 648 | ``` 649 | 650 | 651 | ----------------------------- 652 | 653 | #### count($columns = '*') 654 | 655 | Retrieve the "count" result of the query. 656 | 657 | __Arguments__ 658 | 659 | * `$columns` 660 | 661 | 662 | ----------------------------- 663 | 664 | #### find($id, $columns = array('*')) 665 | 666 | Find a model by its primary key. 667 | 668 | __Arguments__ 669 | 670 | * `$id` 671 | * `$columns` - Specify columns that you want to retrieve. 672 | 673 | __Examples__ 674 | 675 | ```php 676 | find(1, ['id', 'name', 'gender', 'age']); 679 | 680 | ``` 681 | 682 | 683 | ----------------------------- 684 | 685 | #### findMany($ids, $columns = array('*')) 686 | 687 | Find a collection of models by their primary key. 688 | 689 | __Arguments__ 690 | 691 | * `$ids` 692 | * `$columns` - Specify columns that you want to retrieve. 693 | 694 | 695 | ----------------------------- 696 | 697 | #### update($id, array $attributes) 698 | 699 | Update a record in the database. 700 | 701 | __Arguments__ 702 | 703 | * `$id` 704 | * `$attributes` 705 | 706 | __Examples__ 707 | 708 | ```php 709 | update(24, [ 712 | 'name' => 'Kobe Bryant' 713 | ]); 714 | 715 | ``` 716 | 717 | 718 | ----------------------------- 719 | 720 | #### create(array $attributes) 721 | 722 | Create a model with `$attributes`. 723 | 724 | __Arguments__ 725 | 726 | * `$attributes` 727 | 728 | 729 | ----------------------------- 730 | 731 | #### delete($id) 732 | 733 | Delete a record from the database by its primary key. 734 | 735 | __Arguments__ 736 | 737 | * `$id` 738 | 739 | 740 | ----------------------------- 741 | 742 | #### first($columns = ['*']) 743 | 744 | Execute the query and retrieve the first result. 745 | 746 | __Arguments__ 747 | 748 | * `$columns` 749 | 750 | 751 | ----------------------------- 752 | 753 | #### all($columns = ['*']) 754 | 755 | Execute the query as a "select" statement. 756 | 757 | __Arguments__ 758 | 759 | * `$columns` 760 | 761 | 762 | ----------------------------- 763 | 764 | #### paginate($limit = null, $columns = ['*'], $pageName = 'page', $page = null) 765 | 766 | Paginate the given query. 767 | 768 | __Arguments__ 769 | 770 | * `$limit` 771 | * `$columns` 772 | * `$pageName` 773 | * `$page` 774 | 775 | 776 | ----------------------------- 777 | 778 | #### getByField($field, $value = null, $columns = ['*']) 779 | 780 | Retrieve models by a simple equality query. 781 | 782 | __Arguments__ 783 | 784 | * `$field` 785 | * `$value` 786 | * `$columns` 787 | 788 | 789 | ----------------------------- 790 | 791 | #### with($relations) 792 | 793 | Set the relationships that should be eager loaded, like `Eloquent`. 794 | 795 | __Examples__ 796 | 797 | ```php 798 | with('posts')->paginate(10); 801 | 802 | ``` 803 | 804 | 805 | ----------------------------- 806 | 807 | ## Adjustable 808 | 809 | For more complex queries, you could put them in a `Criteria` class that is more semantic and reuse them anywhere you want, for that, using the `Adjustable` ability. 810 | 811 | ### Examples 812 | 813 | ```php 814 | whereAre([ 823 | ['paid', '=', 1], 824 | ['logged_recently', '=', 1], 825 | ]); 826 | } 827 | } 828 | 829 | ``` 830 | 831 | Then in your **controller**: 832 | 833 | ```php 834 | applyCriteria($activeUserCriteria)->all(); 840 | 841 | // Or you can remember this Criteria: 842 | $userRepository->rememberCriteria($activeUserCriteria); 843 | 844 | $activeUsers = $userRepository->all(); 845 | 846 | $femaleActiveUsers = $userRepository->where('gender', '=', 'female')->all(); 847 | 848 | ``` 849 | 850 | 851 | ### API 852 | 853 | ----------------------------- 854 | 855 | #### applyCriteria(\Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria) 856 | 857 | Apply this `Criteria` only once. 858 | 859 | __Arguments__ 860 | 861 | * `$criteria` - Criteria object. 862 | 863 | 864 | ----------------------------- 865 | 866 | #### rememberCriteria(\Housekeeper\Abilities\Adjustable\Contracts\Criteria $criteria) 867 | 868 | Remember this `Criteria`, it will be applied when every wrapped method been called (Only the first one, iternal method calling will be ignored). 869 | 870 | __Arguments__ 871 | 872 | * `$criteria` - Criteria object. 873 | 874 | 875 | ----------------------------- 876 | 877 | #### forgetCriterias() 878 | 879 | Remove all remembered `Criterias` (Not applied). 880 | 881 | 882 | ----------------------------- 883 | 884 | #### getCriterias() 885 | 886 | Get all remembered `Criterias`. 887 | 888 | 889 | ----------------------------- 890 | 891 | ## Eloquently 892 | 893 | This `Abilitiy` provides lots of `Eloquent` style query APIs that you are very familiar with. 894 | 895 | 896 | ### API 897 | 898 | ----------------------------- 899 | 900 | #### where($column, $operator = null, $value = null, $boolean = 'and') 901 | 902 | 903 | ----------------------------- 904 | 905 | #### orWhere($column, $operator = null, $value = null) 906 | 907 | 908 | ----------------------------- 909 | 910 | #### has($relation, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null) 911 | 912 | 913 | ----------------------------- 914 | 915 | #### whereHas($relation, Closure $callback, $operator = '>=', $count = 1) 916 | 917 | 918 | ----------------------------- 919 | 920 | #### whereDoesntHave($relation, Closure $callback = null) 921 | 922 | 923 | ----------------------------- 924 | 925 | #### orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1) 926 | 927 | 928 | ----------------------------- 929 | 930 | #### whereIn($column, $values, $boolean = 'and', $not = false) 931 | 932 | 933 | ----------------------------- 934 | 935 | #### whereNotIn($column, $values, $boolean = 'and') 936 | 937 | 938 | ----------------------------- 939 | 940 | #### orWhereNotIn($column, $values) 941 | 942 | 943 | ----------------------------- 944 | 945 | #### whereNull($column, $boolean = 'and', $not = false) 946 | 947 | 948 | ----------------------------- 949 | 950 | #### orWhereNull($column) 951 | 952 | 953 | ----------------------------- 954 | 955 | #### whereNotNull($column, $boolean = 'and') 956 | 957 | 958 | ----------------------------- 959 | 960 | #### orWhereNotNull($column) 961 | 962 | 963 | ----------------------------- 964 | 965 | ## CacheStatically 966 | 967 | This `Ability` implemented a very simple cache system: Caching all method returns, and delete them all when creating/updating/deleting, you can clear cache manually too. 968 | 969 | Once you use this `Ability`, everything is automatic. `all()`, `find()`, `paginate()` and others will go through the cache logic, if any cached return be found, then no database query will be executed. Different method has different cache key, even applying query will change the cache key. 970 | 971 | This `Ability` may not be much practical in large project, but it shows the flexibility of `Housekeeper`, and other cache system is in the roadmap. 972 | 973 | 974 | ### Examples 975 | 976 | ```php 977 | enableCache()->all(); 981 | 982 | // This also will be cached! 983 | $userRepository->where('age', '<', '30')->orderBy('age', 'desc')->all(); 984 | 985 | ``` 986 | 987 | Wrapped methods has their own cache: 988 | 989 | ```php 990 | simpleWrap(Housekeeper\Action::READ); 999 | } 1000 | 1001 | protected function _getOnlyActive() 1002 | { 1003 | // Every wrapped method has it's own scope, they don't interfere with each other 1004 | return $this->whereAre([ 1005 | ['paid', '=', 1], 1006 | ['logged_recently', '=', 1], 1007 | ]) 1008 | ->all(); // Cached too 1009 | } 1010 | } 1011 | 1012 | ``` 1013 | 1014 | ### API 1015 | 1016 | ----------------------------- 1017 | 1018 | #### enableCache() 1019 | 1020 | Enable cache system. 1021 | 1022 | 1023 | ----------------------------- 1024 | 1025 | #### disableCache() 1026 | 1027 | Disable cache system. 1028 | 1029 | 1030 | ----------------------------- 1031 | 1032 | #### isCacheEnabled() 1033 | 1034 | Indicate whether cache system is enabled or not. 1035 | 1036 | 1037 | ----------------------------- 1038 | 1039 | #### clearCache() 1040 | 1041 | Delete all caches of this repository. 1042 | 1043 | 1044 | ----------------------------- 1045 | 1046 | ## Guardable 1047 | 1048 | `Housekeeper` ignored `Mass Assignment Protection` by default, use this `Ability` if you need it. 1049 | 1050 | `Guardable` disabled `Mass Assignment Protection` by default too, you have to turn it on manually. 1051 | 1052 | 1053 | ### Examples 1054 | 1055 | ```php 1056 | guardUp()->create($request->all()); 1060 | 1061 | // But we can trust our internal process 1062 | $userRepository->guardDown()->create($attributes); 1063 | 1064 | ``` 1065 | 1066 | 1067 | ### API 1068 | 1069 | ----------------------------- 1070 | 1071 | #### guardUp() 1072 | 1073 | Enable `Mass Assignment Protection`。 1074 | 1075 | 1076 | ----------------------------- 1077 | 1078 | #### guardDown() 1079 | 1080 | Disable `Mass Assignment Protection`。 1081 | 1082 | 1083 | ----------------------------- 1084 | 1085 | #### isGuarded() 1086 | 1087 | Whether or not the `Mass Assignment Protection` is enabled. 1088 | 1089 | 1090 | ----------------------------- 1091 | 1092 | ## SoftDeletes 1093 | 1094 | To utilize the `SoftDeletes` trait of the `Eloquent`, you should use this `Ability` in your repository. 1095 | 1096 | 1097 | ### API 1098 | 1099 | ----------------------------- 1100 | 1101 | #### startWithTrashed() 1102 | 1103 | Include soft deletes. 1104 | 1105 | 1106 | ----------------------------- 1107 | 1108 | #### startWithTrashedOnly() 1109 | 1110 | Include soft deletes only. 1111 | 1112 | 1113 | ----------------------------- 1114 | 1115 | #### forceDelete($id) 1116 | 1117 | Hard delete a record by primary key. 1118 | 1119 | __Arguments__ 1120 | 1121 | * `$id` 1122 | 1123 | 1124 | ----------------------------- 1125 | 1126 | #### restore($id) 1127 | 1128 | Restore a soft-deleted record by primary key. 1129 | 1130 | __Arguments__ 1131 | 1132 | * `$id` 1133 | 1134 | 1135 | ----------------------------- 1136 | 1137 | 1138 | ## Console Commands 1139 | 1140 | Create a new repository: 1141 | 1142 | ``` 1143 | php artisan housekeeper:make MyRepository 1144 | ``` 1145 | 1146 | Create a new repsitory and a new model: 1147 | 1148 | ``` 1149 | php artisan housekeeper:make MyRepository --create=Models\\Student 1150 | ``` 1151 | 1152 | Create a new repository with some `Abilities`: 1153 | 1154 | ``` 1155 | php artisan housekeeper:make MyRepository --cache=statically --eloquently --adjustable --sd 1156 | ``` 1157 | 1158 | 1159 | ## Issue 1160 | 1161 | If you have any question about `Housekeeper`, feel free to create an issue, I'll reply you ASAP. 1162 | 1163 | Any useful pull request are welcomed too. 1164 | 1165 | 1166 | ## Lisence 1167 | 1168 | Licensed under the [APACHE LISENCE 2.0](http://www.apache.org/licenses/LICENSE-2.0) 1169 | 1170 | 1171 | ## Credits 1172 | 1173 | Thanks to [prettus/l5-repository](https://github.com/prettus/l5-repository) for inspiring. 1174 | 1175 | Thanks to [sunkey](https://github.com/sunkeyfong) for the awesome LOGOs! 1176 | 1177 | Thanks to [@DarKDinDoN](https://github.com/DarKDinDoN), [@Bruce Peng](https://github.com/ipengxh), [@FelipeUmpierre](https://github.com/FelipeUmpierre), [@rsdev000](https://github.com/rsdev000) for your contributions! 1178 | 1179 | Thanks to [Laravel](https://github.com/laravel/framework) for making our life easier! 1180 | --------------------------------------------------------------------------------