├── src ├── CodeGenerator │ ├── Formatters │ │ ├── Contracts │ │ │ └── FormatterInterface.php │ │ ├── TabFormatter.php │ │ └── BreakLineFormatter.php │ ├── Models │ │ ├── Contracts │ │ │ └── ModelInterface.php │ │ ├── PhpTagModel.php │ │ ├── CloseBracketModel.php │ │ ├── OpenBracketModel.php │ │ ├── UseModel.php │ │ ├── NamespaceModel.php │ │ ├── MethodBodyModel.php │ │ ├── DocBlockModel.php │ │ ├── MethodArgumentModel.php │ │ ├── ConstantModel.php │ │ ├── PropertyModel.php │ │ ├── EntityNameModel.php │ │ └── MethodModel.php │ ├── Exceptions │ │ ├── TemplateException.php │ │ └── CodeGeneratorException.php │ ├── Traits │ │ ├── CodeGeneratorTrait.php │ │ └── ValueTrait.php │ ├── Services │ │ ├── Contracts │ │ │ ├── CodeGeneratorServiceInterface.php │ │ │ └── DbManagerServiceInterface.php │ │ ├── CodeGeneratorService.php │ │ └── DbManagerService.php │ └── Templates │ │ ├── Contracts │ │ └── TemplateInterface.php │ │ ├── Repository │ │ ├── RepositoryInterfaceTemplate.php │ │ └── RepositoryTemplate.php │ │ ├── Service │ │ ├── ServiceInterfaceTemplate.php │ │ └── ServiceTemplate.php │ │ ├── Request │ │ └── RequestTemplate.php │ │ ├── Resource │ │ └── ResourceTemplate.php │ │ ├── ApiResourceController │ │ └── ApiResourceControllerTemplate.php │ │ └── Model │ │ └── ModelTemplate.php ├── Models │ └── BaseModel.php ├── Repositories │ ├── Contracts │ │ ├── BaseCachableRepositoryInterface.php │ │ └── BaseRepositoryInterface.php │ ├── BaseRepository.php │ └── BaseCacheableRepository.php ├── Exceptions │ ├── Service │ │ └── ServiceException.php │ └── Repository │ │ ├── RepositoryException.php │ │ └── InvalidModelClassException.php ├── CacheDrivers │ ├── TaggableCacheDriver.php │ └── BaseCacheDriver.php ├── config │ └── repository-service-pattern.php ├── Console │ └── Commands │ │ ├── ResourceGeneratorCommand.php │ │ ├── ModelGeneratorCommand.php │ │ ├── RequestGeneratorCommand.php │ │ ├── ResourceControllerGeneratorCommand.php │ │ ├── RepositoryAndServiceCodeGeneratorCommand.php │ │ └── CrudGeneratorCommand.php ├── LaravelRepositoryServicePatternServiceProvider.php ├── Traits │ ├── Crudable.php │ └── Queryable.php └── Services │ ├── Contracts │ └── BaseCrudServiceInterface.php │ └── BaseCrudService.php ├── phpunit.xml ├── composer.json └── tests ├── Unit ├── BaseCrudServiceWithCompositeKeysTest.php ├── RepositoryFiltersTest.php ├── BaseCrudServiceTest.php └── BaseCacheableRepositoryTest.php └── TestCase.php /src/CodeGenerator/Formatters/Contracts/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | getTable(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Repositories/Contracts/BaseCachableRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | use;"; 24 | } 25 | 26 | /** 27 | * Set use 28 | * 29 | * @param string $use 30 | * @return UseModel 31 | */ 32 | public function setUse(string $use): UseModel 33 | { 34 | $this->use = $use; 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getUse(): string 43 | { 44 | return $this->use; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CodeGenerator/Services/Contracts/DbManagerServiceInterface.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * @param array $params 32 | * @return string 33 | */ 34 | public function render(array $params = []): string 35 | { 36 | return sprintf('namespace %s;', $this->namespace); 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getNamespace(): string 43 | { 44 | return $this->namespace; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/config/repository-service-pattern.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'is_create_entity_folder' => true, 9 | 'namespace' => 'App\\Services', 10 | ], 11 | 'repository' => [ 12 | 'is_create_entity_folder' => true, 13 | 'namespace' => 'App\\Repositories', 14 | ], 15 | 'request' => [ 16 | 'namespace' => 'App\\Http\\Requests', 17 | ], 18 | 'resource' => [ 19 | 'is_create_entity_folder' => true, 20 | 'namespace' => 'App\\Http\\Resources', 21 | ], 22 | 'controller' => [ 23 | 'is_create_entity_folder' => true, 24 | 'namespace' => 'App\\Http\\Controllers', 25 | 'store_request_name' => 'StoreRequest', 26 | 'update_request_name' => 'UpdateRequest', 27 | ], 28 | 'model' => [ 29 | 'is_create_entity_folder' => true, 30 | 'namespace' => 'App\\Models' 31 | ] 32 | ]; 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./app 6 | 7 | 8 | 9 | 10 | ./tests/Unit 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adobrovolsky97/laravel-repository-service-pattern", 3 | "description": "Laravel 5|6|7|8|9|10 - Repository - Service Pattern", 4 | "keywords": [ 5 | "laravel repository", 6 | "laravel service", 7 | "base service", 8 | "service", 9 | "repository", 10 | "base repository", 11 | "laravel service and repository", 12 | "service and repository", 13 | "laravel", 14 | "repository", 15 | "eloquent", 16 | "model", 17 | "cache", 18 | "code generator", 19 | "laravel skeleton", 20 | "laravel boilerplate" 21 | ], 22 | "license": "MIT", 23 | "authors": [ 24 | { 25 | "name": "Andrew Dobrovolsky", 26 | "email": "adobrovolsky97@gmail.com" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=7.2", 31 | "laravel/framework": ">=8.37 <13.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Adobrovolsky97\\LaravelRepositoryServicePattern\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Adobrovolsky97\\LaravelRepositoryServicePattern\\Tests\\": "tests/" 41 | } 42 | }, 43 | "require-dev": { 44 | "phpunit/phpunit": "^9.5", 45 | "orchestra/testbench": "^7.20" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/MethodBodyModel.php: -------------------------------------------------------------------------------- 1 | lines) . PHP_EOL; 25 | } 26 | 27 | /** 28 | * Add code line 29 | * 30 | * @param string $codeLine 31 | * @return $this 32 | */ 33 | public function addLine(string $codeLine): self 34 | { 35 | $format = resolve(TabFormatter::class)->getFormat(); 36 | $this->lines[] = $format . $format . $codeLine; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set code lines array 43 | * 44 | * @param array $codeLines 45 | * @return $this 46 | */ 47 | public function setLines(array $codeLines): self 48 | { 49 | foreach ($codeLines as $codeLine) { 50 | $this->addLine($codeLine); 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Get code lines 58 | * 59 | * @return array 60 | */ 61 | public function getLines(): array 62 | { 63 | return $this->lines; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Console/Commands/ResourceGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 47 | } 48 | 49 | /** 50 | * Execute the console command. 51 | * 52 | * @return void 53 | * @throws Exception 54 | */ 55 | public function handle(): void 56 | { 57 | $this->codeGeneratorService->generate( 58 | new ResourceTemplate($this->getEntityNameFromTableName($this->argument('table'))) 59 | ); 60 | 61 | $this->info('Resource generated'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Console/Commands/ModelGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 46 | } 47 | 48 | /** 49 | * Execute the console command. 50 | * 51 | * @return void 52 | * @throws Exception 53 | */ 54 | public function handle(): void 55 | { 56 | $this->codeGeneratorService->generate( 57 | new ModelTemplate( 58 | $this->argument('table'), 59 | $this->getEntityNameFromTableName($this->argument('table')) 60 | ) 61 | ); 62 | 63 | $this->info('Model generated'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/DocBlockModel.php: -------------------------------------------------------------------------------- 1 | formatter) ? $this->formatter->getFormat() : ''; 29 | 30 | $lines = []; 31 | $lines[] = "$format/**"; 32 | if ($this->content) { 33 | foreach ($this->content as $item) { 34 | $lines[] = sprintf("$format * %s", $item); 35 | } 36 | } else { 37 | $lines[] = "$format *"; 38 | } 39 | $lines[] = "$format */"; 40 | 41 | return implode(PHP_EOL, $lines); 42 | } 43 | 44 | /** 45 | * @param array $content 46 | * @return DocBlockModel 47 | */ 48 | public function setContent(array $content): self 49 | { 50 | $this->content = $content; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * @param FormatterInterface|null $formatter 57 | * @return DocBlockModel 58 | */ 59 | public function setFormatter(FormatterInterface $formatter): self 60 | { 61 | $this->formatter = $formatter; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function getContent(): array 70 | { 71 | return $this->content; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Console/Commands/RequestGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 43 | } 44 | 45 | /** 46 | * Execute the console command. 47 | * 48 | * @return void 49 | * @throws Exception 50 | */ 51 | public function handle(): void 52 | { 53 | $exploded = explode('\\', $this->argument('namespace')); 54 | $className = last($exploded); 55 | 56 | unset($exploded[count($exploded) - 1]); 57 | 58 | $namespace = implode('\\', $exploded); 59 | 60 | $this->codeGeneratorService->generate( 61 | new RequestTemplate($className, $namespace, $this->argument('modelNamespace')) 62 | ); 63 | 64 | $this->info('Request generated'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/CodeGenerator/Traits/ValueTrait.php: -------------------------------------------------------------------------------- 1 | renderTyped($item); 40 | } 41 | 42 | $value = count($parts) > 5 43 | ? $this->printBigArray($parts) 44 | : '[' . implode(', ', $parts) . ']'; 45 | break; 46 | default: 47 | $value = null; 48 | } 49 | 50 | return $value; 51 | } 52 | 53 | /** 54 | * Print big array 55 | * 56 | * @param array $values 57 | * @return string 58 | */ 59 | private function printBigArray(array $values): string 60 | { 61 | $lines = ['[']; 62 | 63 | $count = count($values); 64 | 65 | foreach ($values as $id => $value) { 66 | $lines[] = "\t\t" . $value . ($id === $count - 1 ? '' : ','); 67 | } 68 | 69 | $lines[] = "\t]"; 70 | 71 | return implode(PHP_EOL, $lines); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/CodeGenerator/Services/CodeGeneratorService.php: -------------------------------------------------------------------------------- 1 | writeContentAndGetPath($template, $template->render()); 26 | 27 | // Including file 28 | include_once $fullPath; 29 | } 30 | 31 | /** 32 | * Write content to a project directory 33 | * 34 | * @param TemplateInterface $template 35 | * @param string $content 36 | * @return void 37 | * @throws Exception 38 | */ 39 | protected function writeContentAndGetPath(TemplateInterface $template, string $content): string 40 | { 41 | $path = app_path(str_replace(['\\', 'App'], ['/', ''], $template->getNamespace())); 42 | $filesystems = new Filesystem(); 43 | 44 | if (!$filesystems->isDirectory($path)) { 45 | if (!$filesystems->makeDirectory($path, 0777, true)) { 46 | throw new Exception(sprintf('Could not create directory %s', $path)); 47 | } 48 | } 49 | 50 | if (!$filesystems->isWritable($path)) { 51 | throw new Exception(sprintf('%s is not writeable', $path)); 52 | } 53 | 54 | $fullPath = "$path/{$template->getName()}.php"; 55 | 56 | $filesystems->put($fullPath, $content); 57 | 58 | return $fullPath; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Repository/RepositoryInterfaceTemplate.php: -------------------------------------------------------------------------------- 1 | setNamespace($entityName)->setName($entityName); 24 | } 25 | 26 | /** 27 | * @param array $params 28 | * @return string 29 | * @throws Exception 30 | */ 31 | public function render(array $params = []): string 32 | { 33 | $this->setType(EntityNameModel::TYPE_INTERFACE)->setExtends(BaseRepositoryInterface::class); 34 | 35 | return parent::render($params); 36 | } 37 | 38 | /** 39 | * @param string $name 40 | * @return ClassTemplate 41 | */ 42 | public function setName(string $name): ClassTemplate 43 | { 44 | return parent::setName("{$name}RepositoryInterface"); 45 | } 46 | 47 | /** 48 | * @param string $namespace 49 | * @return ClassTemplate 50 | */ 51 | public function setNamespace(string $namespace): ClassTemplate 52 | { 53 | if (config('repository-service-pattern.repository.is_create_entity_folder')) { 54 | return parent::setNamespace( 55 | config('repository-service-pattern.repository.namespace') . "\\$namespace\\Contracts" 56 | ); 57 | } 58 | 59 | return parent::setNamespace( 60 | config('repository-service-pattern.repository.namespace') . "\\Contracts" 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Service/ServiceInterfaceTemplate.php: -------------------------------------------------------------------------------- 1 | entityName = $entityName; 29 | 30 | $this->setNamespace($entityName)->setName($entityName); 31 | } 32 | 33 | /** 34 | * @param array $params 35 | * @return string 36 | * @throws Exception 37 | */ 38 | public function render(array $params = []): string 39 | { 40 | $this->setType(EntityNameModel::TYPE_INTERFACE)->setExtends(BaseCrudServiceInterface::class); 41 | 42 | return parent::render($params); 43 | } 44 | 45 | /** 46 | * @param string|null $name 47 | * @return void 48 | */ 49 | public function setName(string $name): ClassTemplate 50 | { 51 | return parent::setName("{$name}ServiceInterface"); 52 | } 53 | 54 | /** 55 | * @param string|null $namespace 56 | * @return void 57 | */ 58 | public function setNamespace(string $namespace): ClassTemplate 59 | { 60 | if (config('repository-service-pattern.service.is_create_entity_folder')) { 61 | return parent::setNamespace( 62 | config('repository-service-pattern.service.namespace') . "\\$namespace\\Contracts" 63 | ); 64 | } 65 | 66 | return parent::setNamespace( 67 | config('repository-service-pattern.service.namespace') . "\\Contracts" 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Repositories/BaseRepository.php: -------------------------------------------------------------------------------- 1 | getModelClass(); 31 | if (!$keyOrModel instanceof $modelClass) { 32 | throw new RepositoryException("Model is not an entity of repository model class"); 33 | } 34 | return $keyOrModel; 35 | } 36 | 37 | return $this->withTrashed()->findOrFail($keyOrModel); 38 | } 39 | 40 | /** 41 | * Check if model is instance of SoftDeletes trait 42 | * 43 | * @param Model|null $model 44 | * @return bool 45 | */ 46 | protected function isInstanceOfSoftDeletes(Model $model = null): bool 47 | { 48 | $model = $model ?? app($this->getModelClass()); 49 | 50 | return in_array(SoftDeletes::class, class_uses_recursive($model)); 51 | } 52 | 53 | /** 54 | * Check if model has soft delete column 55 | * 56 | * @param Model|null $model 57 | * @return bool 58 | */ 59 | protected function isModelHasSoftDeleteColumn(Model $model = null): bool 60 | { 61 | $model = $model ?? app($this->getModelClass()); 62 | 63 | return Schema::hasColumn($model->getTable(), $this->deletedAtColumnName); 64 | } 65 | 66 | /** 67 | * Get model class 68 | * 69 | * @return string 70 | */ 71 | abstract protected function getModelClass(): string; 72 | } 73 | -------------------------------------------------------------------------------- /src/Console/Commands/ResourceControllerGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 47 | } 48 | 49 | /** 50 | * Execute the console command. 51 | * 52 | * @return void 53 | * @throws Exception 54 | */ 55 | public function handle(): void 56 | { 57 | $entityName = $this->getEntityNameFromTableName($this->argument('table')); 58 | 59 | $serviceInterfaceNamespace = config('repository-service-pattern.service.is_create_entity_folder') 60 | ? config('repository-service-pattern.service.namespace') . "\\$entityName\\Contracts\\{$entityName}ServiceInterface" 61 | : config('repository-service-pattern.service.namespace') . "\\Contracts\\{$entityName}ServiceInterface"; 62 | 63 | $template = new ApiResourceControllerTemplate($entityName, $serviceInterfaceNamespace); 64 | 65 | $this->codeGeneratorService->generate($template); 66 | 67 | $this->info('Api Resource Controller generated'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/MethodArgumentModel.php: -------------------------------------------------------------------------------- 1 | type ? $this->type . ' ' : '', 35 | $this->name, 36 | isset($this->defaultValue) ? ' = ' . $this->defaultValue : '' 37 | ]; 38 | 39 | return implode('', $lines); 40 | } 41 | 42 | /** 43 | * Set method argument type 44 | * 45 | * @param string|null $type 46 | * @return MethodArgumentModel 47 | */ 48 | public function setType(string $type = null): MethodArgumentModel 49 | { 50 | $this->type = $type; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Set method argument name 57 | * 58 | * @param string $name 59 | * @return MethodArgumentModel 60 | */ 61 | public function setName(string $name): MethodArgumentModel 62 | { 63 | $this->name = '$'.$name; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Set method argument default value 70 | * 71 | * @param mixed $defaultValue 72 | * @return MethodArgumentModel 73 | */ 74 | public function setDefaultValue($defaultValue): self 75 | { 76 | $this->defaultValue = $defaultValue; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getType(): string 85 | { 86 | return $this->type; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getName(): string 93 | { 94 | return $this->name; 95 | } 96 | 97 | /** 98 | * @return mixed 99 | */ 100 | public function getDefaultValue() 101 | { 102 | return $this->defaultValue; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Console/Commands/RepositoryAndServiceCodeGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 48 | } 49 | 50 | /** 51 | * Execute the console command. 52 | * 53 | * @return void 54 | */ 55 | public function handle(): void 56 | { 57 | $entityName = $this->getEntityNameFromTableName($this->argument('table')); 58 | $templates = [ 59 | new RepositoryInterfaceTemplate($entityName), 60 | new RepositoryTemplate($entityName), 61 | new ServiceInterfaceTemplate($entityName), 62 | new ServiceTemplate($entityName) 63 | ]; 64 | 65 | foreach ($templates as $template) { 66 | $this->codeGeneratorService->generate($template); 67 | } 68 | 69 | $this->info('Repositories and Services were generated'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/LaravelRepositoryServicePatternServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 36 | __DIR__.'/config/repository-service-pattern.php' => config_path('repository-service-pattern.php'), 37 | ]); 38 | 39 | $this->mergeConfigFrom( 40 | __DIR__.'/config/repository-service-pattern.php', 41 | 'repository-service-pattern' 42 | ); 43 | 44 | if ($this->app->runningInConsole()) { 45 | $this->commands([ 46 | RepositoryAndServiceCodeGeneratorCommand::class, 47 | ResourceControllerGeneratorCommand::class, 48 | RequestGeneratorCommand::class, 49 | ModelGeneratorCommand::class, 50 | ResourceGeneratorCommand::class, 51 | CrudGeneratorCommand::class 52 | ]); 53 | } 54 | 55 | $this->app->singleton(CodeGeneratorServiceInterface::class, CodeGeneratorService::class); 56 | $this->app->singleton(DbManagerServiceInterface::class, DbManagerService::class); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/BaseCrudServiceWithCompositeKeysTest.php: -------------------------------------------------------------------------------- 1 | id(); 23 | $table->integer('property'); 24 | $table->timestamps(); 25 | }); 26 | Schema::create('tests', function (Blueprint $table) { 27 | $table->string('prop_one'); 28 | $table->integer('prop_two'); 29 | $table->foreignId('related_id')->nullable()->constrained('relations'); 30 | $table->timestamps(); 31 | $table->softDeletes(); 32 | $table->primary(['prop_one', 'prop_two']); 33 | }); 34 | } 35 | 36 | /** 37 | * @return void 38 | */ 39 | protected function initializeModel(): void 40 | { 41 | $this->model = new class extends Model { 42 | use SoftDeletes; 43 | 44 | public $incrementing = false; 45 | protected $primaryKey = ['prop_one', 'prop_two']; 46 | protected $keyType = 'array'; 47 | protected $table = 'tests'; 48 | protected $fillable = ['prop_one', 'prop_two', 'related_id', 'deleted_at']; 49 | 50 | protected $related; 51 | 52 | public function __construct(array $attributes = []) 53 | { 54 | parent::__construct($attributes); 55 | 56 | $this->related = new class extends Model { 57 | protected $table = 'relations'; 58 | protected $fillable = ['property']; 59 | }; 60 | } 61 | 62 | public function related(): BelongsTo 63 | { 64 | return $this->belongsTo(get_class($this->related), 'related_id'); 65 | } 66 | 67 | protected function setKeysForSaveQuery($query) 68 | { 69 | return $query->where('prop_one', $this->getAttribute('prop_one')) 70 | ->where('prop_two', $this->getAttribute('prop_two')); 71 | } 72 | public function getKey() 73 | { 74 | /** @var array $keys */ 75 | if (!is_array($keys = $this->getKeyName())) { 76 | return $this->getAttribute($this->primaryKey); 77 | } 78 | 79 | $attributes = []; 80 | 81 | foreach ($keys as $key) { 82 | $attributes[$key] = $this->getAttribute($key); 83 | } 84 | 85 | return $attributes; 86 | } 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/CacheDrivers/BaseCacheDriver.php: -------------------------------------------------------------------------------- 1 | validateKeyData($keyData); 23 | 24 | // Basic lock to prevent double-query under high concurrency 25 | $lockName = 'lock:' . $keyData['keyWithTag']; 26 | 27 | return Cache::lock($lockName, 5)->block(3, function () use ($keyData, $ttl, $callback) { 28 | return Cache::tags($keyData['tags'])->remember($keyData['paramsKey'], $ttl, function () use ($callback) { 29 | $value = $callback(); 30 | 31 | // Skip caching unserializable objects 32 | if ($this->isUnserializable($value)) { 33 | return $value; 34 | } 35 | 36 | // Store sentinel for nulls 37 | return $value ?? self::NULL_SENTINEL; 38 | }); 39 | }); 40 | } 41 | 42 | /** 43 | * Put data with safety checks 44 | */ 45 | public function put(array $keyData, $data, int $ttl): void 46 | { 47 | if ($this->isUnserializable($data)) { 48 | return; 49 | } 50 | 51 | $value = $data ?? self::NULL_SENTINEL; 52 | 53 | Cache::tags($keyData['tags'])->put($keyData['paramsKey'], $value, $ttl); 54 | } 55 | 56 | /** 57 | * Forget cache data (flush tag group) 58 | */ 59 | public function forget(array $keyData): void 60 | { 61 | if (!empty($keyData['tags'])) { 62 | Cache::tags($keyData['tags'])->flush(); 63 | } 64 | } 65 | 66 | /** 67 | * Detect unserializable / volatile objects 68 | */ 69 | protected function isUnserializable($value): bool 70 | { 71 | return $value instanceof Closure 72 | || $value instanceof EloquentBuilder 73 | || $value instanceof QueryBuilder 74 | || $value instanceof LazyCollection 75 | || is_resource($value); 76 | } 77 | 78 | /** 79 | * Validate cache key structure 80 | * @throws RepositoryException 81 | */ 82 | protected function validateKeyData(array $keyData): void 83 | { 84 | if (!isset($keyData['tags'], $keyData['keyWithTag'], $keyData['paramsKey'])) { 85 | throw new RepositoryException('Cache key data is invalid'); 86 | } 87 | } 88 | 89 | /** 90 | * Retrieve and normalize sentinel values 91 | */ 92 | public function get(array $keyData) 93 | { 94 | $value = Cache::tags($keyData['tags'])->get($keyData['paramsKey']); 95 | 96 | return $value === self::NULL_SENTINEL ? null : $value; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Service/ServiceTemplate.php: -------------------------------------------------------------------------------- 1 | modelName = $modelName; 30 | $this->setNamespace($modelName)->setName($modelName); 31 | } 32 | 33 | /** 34 | * @param array $params 35 | * @return string 36 | * @throws Exception 37 | */ 38 | public function render(array $params = []): string 39 | { 40 | $repositoryInterfaceNamespace = config('repository-service-pattern.repository.is_create_entity_folder') 41 | ? config('repository-service-pattern.repository.namespace') . "\\$this->modelName\\Contracts\\{$this->modelName}RepositoryInterface" 42 | : config('repository-service-pattern.repository.namespace') . "\\Contracts\\{$this->modelName}RepositoryInterface"; 43 | 44 | $serviceInterfaceNamespace = config('repository-service-pattern.service.is_create_entity_folder') 45 | ? config('repository-service-pattern.service.namespace') . "\\$this->modelName\\Contracts\\{$this->modelName}ServiceInterface" 46 | : config('repository-service-pattern.service.namespace') . "\\Contracts\\{$this->modelName}ServiceInterface"; 47 | 48 | $this 49 | ->addUse($repositoryInterfaceNamespace) 50 | ->setType(EntityNameModel::TYPE_CLASS) 51 | ->setExtends(BaseCrudService::class) 52 | ->setImplements($serviceInterfaceNamespace) 53 | ->addMethod( 54 | 'getRepositoryClass', 55 | [], 56 | PropertyModel::ACCESS_PROTECTED, 57 | null, 58 | 'string', 59 | ['return '.$this->modelName . 'RepositoryInterface::class;'] 60 | ); 61 | 62 | return parent::render($params); 63 | } 64 | 65 | /** 66 | * @param string $name 67 | * @return ClassTemplate 68 | */ 69 | public function setName(string $name): ClassTemplate 70 | { 71 | return parent::setName("{$name}Service"); 72 | } 73 | 74 | /** 75 | * @param string $namespace 76 | * @return ClassTemplate 77 | */ 78 | public function setNamespace(string $namespace): ClassTemplate 79 | { 80 | if (config('repository-service-pattern.service.is_create_entity_folder')) { 81 | return parent::setNamespace( 82 | config('repository-service-pattern.service.namespace') . "\\$namespace" 83 | ); 84 | } 85 | 86 | return parent::setNamespace( 87 | config('repository-service-pattern.service.namespace') . "" 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Repository/RepositoryTemplate.php: -------------------------------------------------------------------------------- 1 | modelName = $modelName; 30 | 31 | $this->setNamespace($modelName)->setName($modelName); 32 | } 33 | 34 | /** 35 | * @param array $params 36 | * @return string 37 | * @throws Exception 38 | */ 39 | public function render(array $params = []): string 40 | { 41 | $repositoryInterfaceNamespace = config('repository-service-pattern.repository.is_create_entity_folder') 42 | ? config('repository-service-pattern.repository.namespace') . "\\$this->modelName\\Contracts\\{$this->modelName}RepositoryInterface" 43 | : config('repository-service-pattern.repository.namespace') . "\\Contracts\\{$this->modelName}RepositoryInterface"; 44 | 45 | $this 46 | ->setType(EntityNameModel::TYPE_CLASS) 47 | ->addUse($this->getModelNameWithNamespace()) 48 | ->setExtends(BaseRepository::class) 49 | ->setImplements($repositoryInterfaceNamespace) 50 | ->addMethod( 51 | 'getModelClass', 52 | [], 53 | PropertyModel::ACCESS_PROTECTED, 54 | null, 55 | 'string', 56 | ['return '. $this->modelName . '::class;'] 57 | ); 58 | 59 | return parent::render($params); 60 | } 61 | 62 | /** 63 | * @param string $name 64 | * @return ClassTemplate 65 | */ 66 | public function setName(string $name): ClassTemplate 67 | { 68 | return parent::setName("{$name}Repository"); 69 | } 70 | 71 | /** 72 | * @param string $namespace 73 | * @return ClassTemplate 74 | */ 75 | public function setNamespace(string $namespace): ClassTemplate 76 | { 77 | if (config('repository-service-pattern.repository.is_create_entity_folder')) { 78 | return parent::setNamespace( 79 | config('repository-service-pattern.repository.namespace') . "\\$namespace" 80 | ); 81 | } 82 | 83 | return parent::setNamespace( 84 | config('repository-service-pattern.repository.namespace') 85 | ); 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getModelNameWithNamespace(): string 92 | { 93 | if (config('repository-service-pattern.model.is_create_entity_folder')) { 94 | return config('repository-service-pattern.model.namespace') . "\\$this->modelName\\$this->modelName"; 95 | } 96 | 97 | return config('repository-service-pattern.model.namespace') . "\\$this->modelName"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/ConstantModel.php: -------------------------------------------------------------------------------- 1 | docBlock) ? $this->docBlock->render() . PHP_EOL : null, 52 | $this->access ?? null, 53 | resolve(TabFormatter::class)->getFormat(), 54 | 'const', 55 | ' ', 56 | $this->name, 57 | ' ', 58 | '=', 59 | ' ', 60 | $this->renderTyped($this->value), 61 | ';' 62 | ]; 63 | 64 | return implode('', $lines); 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getAccess(): string 71 | { 72 | return $this->access; 73 | } 74 | 75 | /** 76 | * Set constant access 77 | * 78 | * @param string|null $access 79 | * @return ConstantModel 80 | * @throws CodeGeneratorException 81 | */ 82 | public function setAccess(string $access = null): self 83 | { 84 | $access = $access ?? self::ACCESS_PUBLIC; 85 | 86 | if (!in_array($access, [self::ACCESS_PUBLIC, self::ACCESS_PROTECTED, self::ACCESS_PRIVATE])) { 87 | throw new CodeGeneratorException('Invalid access'); 88 | } 89 | 90 | $this->access = $access === self::ACCESS_PUBLIC ? null : $access; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getName(): string 99 | { 100 | return $this->name; 101 | } 102 | 103 | /** 104 | * @param string $name 105 | * @return ConstantModel 106 | */ 107 | public function setName(string $name): self 108 | { 109 | $this->name = $name; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * @return mixed 116 | */ 117 | public function getValue() 118 | { 119 | return $this->value; 120 | } 121 | 122 | /** 123 | * @param mixed $value 124 | */ 125 | public function setValue($value): self 126 | { 127 | $this->value = $value; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * @param DocBlockModel|null $docBlock 134 | * @return ConstantModel 135 | */ 136 | public function setDocBlock(DocBlockModel $docBlock): self 137 | { 138 | $this->docBlock = $docBlock; 139 | 140 | return $this; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | runMigrations(); 37 | $this->initializeModel(); 38 | $this->initializeRepository(); 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | protected function runMigrations(): void 45 | { 46 | Schema::create('relations', function (Blueprint $table) { 47 | $table->id(); 48 | $table->integer('property'); 49 | $table->timestamps(); 50 | }); 51 | Schema::create('tests', function (Blueprint $table) { 52 | $table->id(); 53 | $table->string('prop_one'); 54 | $table->integer('prop_two'); 55 | $table->foreignId('related_id')->nullable()->constrained('relations'); 56 | $table->timestamps(); 57 | $table->softDeletes(); 58 | }); 59 | } 60 | 61 | /** 62 | * @return void 63 | */ 64 | protected function initializeModel(): void 65 | { 66 | $this->model = new class extends Model { 67 | use SoftDeletes; 68 | 69 | protected $table = 'tests'; 70 | 71 | protected $fillable = ['prop_one', 'prop_two', 'related_id', 'deleted_at']; 72 | 73 | protected $related; 74 | 75 | public function __construct(array $attributes = []) 76 | { 77 | parent::__construct($attributes); 78 | 79 | $this->related = new class extends Model { 80 | protected $table = 'relations'; 81 | protected $fillable = ['property']; 82 | }; 83 | } 84 | 85 | public function related(): BelongsTo 86 | { 87 | return $this->belongsTo(get_class($this->related), 'related_id'); 88 | } 89 | }; 90 | } 91 | 92 | /** 93 | * @return void 94 | */ 95 | protected function initializeRepository(): void 96 | { 97 | $model = $this->model; 98 | 99 | $this->repository = new class($model) extends BaseRepository { 100 | protected $modelClass; 101 | public function __construct(Model $model) 102 | { 103 | $this->modelClass = get_class($model); 104 | } 105 | 106 | protected function getTableColumns(string $tableName): array 107 | { 108 | return Schema::getColumnListing($tableName); 109 | } 110 | 111 | protected function getModelClass(): string 112 | { 113 | return $this->modelClass; 114 | } 115 | }; 116 | } 117 | 118 | /** 119 | * @param array $data 120 | * @return Model 121 | */ 122 | protected function createModel(array $data): Model 123 | { 124 | return $this->model->query()->create($data); 125 | } 126 | 127 | /** 128 | * @param Application $app 129 | * @return array 130 | */ 131 | protected function getPackageProviders($app): array 132 | { 133 | return ['Adobrovolsky97\LaravelRepositoryServicePattern\LaravelRepositoryServicePatternServiceProvider']; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Traits/Crudable.php: -------------------------------------------------------------------------------- 1 | getModelClass()); 26 | 27 | if (!$model->fill($data)->save()) { 28 | return null; 29 | } 30 | 31 | if (!is_array($model->getKey())) { 32 | return $model->refresh(); 33 | } 34 | 35 | return $model; 36 | } 37 | 38 | /** 39 | * Insert records 40 | * 41 | * @param array $data 42 | * @return bool 43 | */ 44 | public function insert(array $data): bool 45 | { 46 | return $this->getQuery()->insert($data); 47 | } 48 | 49 | /** 50 | * Update model 51 | * 52 | * @param Model|mixed $keyOrModel 53 | * @param array $data 54 | * @return Model|null 55 | * @throws RepositoryException 56 | */ 57 | public function update($keyOrModel, array $data): ?Model 58 | { 59 | $model = $this->resolveModel($keyOrModel); 60 | 61 | if (!$model->update($data)) { 62 | return null; 63 | } 64 | 65 | if (!is_array($model->getKey())) { 66 | return $model->refresh(); 67 | } 68 | 69 | return $model; 70 | } 71 | 72 | /** 73 | * Update or create model 74 | * 75 | * @param array $attributes 76 | * @param array $data 77 | * @return Model|null 78 | */ 79 | public function updateOrCreate(array $attributes, array $data): ?Model 80 | { 81 | return $this->getQuery()->updateOrCreate($attributes, $data); 82 | } 83 | 84 | /** 85 | * Delete model 86 | * 87 | * @param Model|mixed $keyOrModel 88 | * @return bool 89 | * @throws Exception 90 | */ 91 | public function delete($keyOrModel): bool 92 | { 93 | $model = $this->resolveModel($keyOrModel); 94 | 95 | if ($this->isInstanceOfSoftDeletes($model)) { 96 | return !is_null($model->forceDelete()); 97 | } 98 | 99 | return !is_null($model->delete()); 100 | } 101 | 102 | /** 103 | * Perform model soft delete 104 | * 105 | * @param $keyOrModel 106 | * @return void 107 | * @throws RepositoryException 108 | * @throws Exception 109 | */ 110 | public function softDelete($keyOrModel): void 111 | { 112 | $model = $this->resolveModel($keyOrModel); 113 | 114 | if ($this->isInstanceOfSoftDeletes($model)) { 115 | $model->delete(); 116 | return; 117 | } 118 | 119 | if ($this->isModelHasSoftDeleteColumn($model)) { 120 | $this->update($model, [$this->deletedAtColumnName => now()]); 121 | return; 122 | } 123 | 124 | throw new RepositoryException('Model does not support soft deletes.'); 125 | } 126 | 127 | /** 128 | * Restore soft deleted model 129 | * 130 | * @param $keyOrModel 131 | * @return void 132 | * @throws RepositoryException 133 | */ 134 | public function restore($keyOrModel): void 135 | { 136 | /** @var Model|SoftDeletes $model */ 137 | $model = $this->resolveModel($keyOrModel); 138 | 139 | if ($model->{$this->deletedAtColumnName} === null) { 140 | throw new RepositoryException('Model is not deleted so could not be restored'); 141 | } 142 | 143 | if ($this->isInstanceOfSoftDeletes($model)) { 144 | $model->restore(); 145 | return; 146 | } 147 | 148 | if ($this->isModelHasSoftDeleteColumn($model)) { 149 | $this->update($model, [$this->deletedAtColumnName => null]); 150 | return; 151 | } 152 | 153 | throw new RepositoryException('Model does not support soft deletes.'); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/PropertyModel.php: -------------------------------------------------------------------------------- 1 | docBlockModel)) { 62 | $result = $this->docBlockModel->render() . PHP_EOL; 63 | } 64 | 65 | $parts = [ 66 | !is_null($this->formatter) ? $this->formatter->getFormat() : '', 67 | $this->access, 68 | ' ', 69 | '$', 70 | $this->name, 71 | ]; 72 | 73 | if ($this->value !== self::VALUE_NON_INITIALIZED) { 74 | $parts = array_merge($parts, [ 75 | ' ', 76 | '=', 77 | ' ', 78 | $this->renderTyped($this->value), 79 | ]); 80 | } 81 | 82 | $parts[] = ';'; 83 | 84 | return $result . implode('', $parts); 85 | } 86 | 87 | /** 88 | * @param string $name 89 | * @return PropertyModel 90 | */ 91 | public function setName(string $name): PropertyModel 92 | { 93 | $this->name = $name; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * @param string $access 100 | * @return PropertyModel 101 | * @throws CodeGeneratorException 102 | */ 103 | public function setAccess(string $access): PropertyModel 104 | { 105 | $this->access = $access; 106 | 107 | if (!in_array($access, [self::ACCESS_PUBLIC, self::ACCESS_PRIVATE, self::ACCESS_PROTECTED])) { 108 | throw new CodeGeneratorException('Invalid access given'); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * @param mixed|null $value 116 | * @return PropertyModel 117 | */ 118 | public function setValue($value = null): self 119 | { 120 | $this->value = $value; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * @param DocBlockModel|null $docBlockModel 127 | * @return PropertyModel 128 | */ 129 | public function setDocBlockModel(DocBlockModel $docBlockModel): PropertyModel 130 | { 131 | $this->docBlockModel = $docBlockModel; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * @param FormatterInterface|null $formatter 138 | * @return PropertyModel 139 | */ 140 | public function setFormatter(FormatterInterface $formatter): PropertyModel 141 | { 142 | $this->formatter = $formatter; 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * @return string 149 | */ 150 | public function getName(): string 151 | { 152 | return $this->name; 153 | } 154 | 155 | /** 156 | * @return string 157 | */ 158 | public function getAccess(): string 159 | { 160 | return $this->access; 161 | } 162 | 163 | /** 164 | * @return DocBlockModel|null 165 | */ 166 | public function getDocBlockModel(): ?DocBlockModel 167 | { 168 | return $this->docBlockModel; 169 | } 170 | 171 | /** 172 | * @return mixed|null 173 | */ 174 | public function getValue() 175 | { 176 | return $this->value; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Repositories/Contracts/BaseRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 'array', 32 | 'simple_array' => 'array', 33 | 'json_array' => 'string', 34 | 'bigint' => 'integer', 35 | 'boolean' => 'boolean', 36 | 'datetime' => 'date_format:Y-m-d H:i', 37 | 'datetimetz' => 'date_format:Y-m-d H:i', 38 | 'date' => 'date_format:Y-m-d', 39 | 'time' => 'date_format:H:i', 40 | 'decimal' => 'numeric', 41 | 'integer' => 'integer', 42 | 'smallint' => 'integer', 43 | 'string' => 'string', 44 | 'text' => 'string', 45 | 'binary' => 'string', 46 | 'blob' => 'string', 47 | 'float' => 'numeric', 48 | 'guid' => 'string', 49 | ]; 50 | 51 | /** 52 | * @param string $className 53 | * @param string $namespace 54 | * @param string|null $modelNamespace 55 | * @throws TemplateException 56 | */ 57 | public function __construct(string $className, string $namespace, string $modelNamespace = null) 58 | { 59 | parent::__construct(); 60 | 61 | if (!is_null($modelNamespace) && !class_exists($modelNamespace)) { 62 | throw new TemplateException("Model '$modelNamespace' not found."); 63 | } 64 | 65 | $this->modelNamespace = $modelNamespace; 66 | 67 | $this 68 | ->setName($className) 69 | ->setNamespace($namespace); 70 | } 71 | 72 | /** 73 | * @param array $params 74 | * @return string 75 | * @throws Exception 76 | */ 77 | public function render(array $params = []): string 78 | { 79 | $this 80 | ->setType(EntityNameModel::TYPE_CLASS) 81 | ->setExtends(Request::class) 82 | ->addMethod( 83 | 'rules', 84 | [], 85 | MethodModel::ACCESS_PUBLIC, 86 | null, 87 | 'array', 88 | $this->getRulesBody(), 89 | ['Get validation rules', '', '@return array'] 90 | ) 91 | ->addMethod( 92 | 'messages', 93 | [], 94 | MethodModel::ACCESS_PUBLIC, 95 | null, 96 | 'array', 97 | ['return [', '', '];'], 98 | ['Get validation messages', '', '@return array'] 99 | ); 100 | 101 | return parent::render($params); 102 | } 103 | 104 | /** 105 | * @param string $namespace 106 | * @return ClassTemplate 107 | */ 108 | public function setNamespace(string $namespace): ClassTemplate 109 | { 110 | return parent::setNamespace( 111 | config('repository-service-pattern.request.namespace') . ($namespace ? "\\$namespace" : '') 112 | ); 113 | } 114 | 115 | /** 116 | * @return string[] 117 | * @throws TemplateException 118 | */ 119 | protected function getRulesBody(): array 120 | { 121 | if (is_null($this->modelNamespace)) { 122 | return ['return [', '', '];']; 123 | } 124 | 125 | /** @var Model $model */ 126 | $model = resolve($this->modelNamespace); 127 | 128 | if (!$model instanceof Model) { 129 | throw new TemplateException('Model must be an instance of ' . Model::class); 130 | } 131 | 132 | /** @var DbManagerServiceInterface $dbManager */ 133 | $dbManager = resolve(DbManagerServiceInterface::class); 134 | $formatter = resolve(TabFormatter::class); 135 | 136 | $body = ['return [']; 137 | 138 | $fillable = $model->getFillable(); 139 | 140 | /** @var Column $tableColumn */ 141 | foreach ($dbManager->getTableColumns($model->getTable()) as $tableColumn) { 142 | 143 | if (!in_array($tableColumn->getName(), $fillable)) { 144 | continue; 145 | } 146 | 147 | $type = $this->columnTypeToValidationRuleMapping[$tableColumn->getType()->getName()] ?? 'string'; 148 | $body[] = $formatter->getFormat() . "'{$tableColumn->getName()}' => 'required|$type',"; 149 | } 150 | 151 | $body[] = '];'; 152 | 153 | return $body; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Resource/ResourceTemplate.php: -------------------------------------------------------------------------------- 1 | modelName = $modelName; 43 | $this->modelWithNamespace = config('repository-service-pattern.model.is_create_entity_folder') 44 | ? config('repository-service-pattern.model.namespace') . "\\$modelName\\$modelName" 45 | : config('repository-service-pattern.model.namespace')."\\$modelName"; 46 | 47 | if (!class_exists($this->modelWithNamespace)) { 48 | throw new TemplateException("Model '$this->modelWithNamespace' not found."); 49 | } 50 | 51 | $this->setName($modelName)->setNamespace($modelName); 52 | } 53 | 54 | /** 55 | * @param array $params 56 | * @return string 57 | * @throws Exception 58 | */ 59 | public function render(array $params = []): string 60 | { 61 | $this 62 | ->addUse(JsonResponse::class) 63 | ->addUse(Response::class) 64 | ->setType(EntityNameModel::TYPE_CLASS) 65 | ->setExtends(JsonResource::class) 66 | ->addProperty( 67 | 'statusCode', 68 | 'Response::HTTP_OK', 69 | PropertyModel::ACCESS_PROTECTED, 70 | ['@var integer'] 71 | ) 72 | ->addMethod( 73 | '__construct', 74 | [ 75 | ['name' => 'resource'], 76 | ['type' => 'int', 'name' => 'statusCode', 'defaultValue' => 'Response::HTTP_OK'] 77 | ], 78 | MethodModel::ACCESS_PUBLIC, 79 | null, 80 | null, 81 | ['$this->statusCode = $statusCode;', '', 'parent::__construct($resource);'] 82 | ) 83 | ->addMethod( 84 | 'toArray', 85 | [ 86 | ['name' => 'request'] 87 | ], 88 | MethodModel::ACCESS_PUBLIC, 89 | null, 90 | 'array', 91 | $this->getResourceBody() 92 | ) 93 | ->addMethod( 94 | 'toResponse', 95 | [ 96 | ['name' => 'request'] 97 | ], 98 | MethodModel::ACCESS_PUBLIC, 99 | null, 100 | 'JsonResponse', 101 | ['return parent::toResponse($request)->setStatusCode($this->statusCode);'] 102 | ); 103 | 104 | return parent::render($params); 105 | } 106 | 107 | /** 108 | * @param string $name 109 | * @return ClassTemplate 110 | */ 111 | public function setName(string $name): ClassTemplate 112 | { 113 | return parent::setName($name.'Resource'); 114 | } 115 | 116 | /** 117 | * @param string $namespace 118 | * @return ClassTemplate 119 | */ 120 | public function setNamespace(string $namespace): ClassTemplate 121 | { 122 | if (config('repository-service-pattern.resource.is_create_entity_folder')) { 123 | return parent::setNamespace( 124 | config('repository-service-pattern.resource.namespace') . "\\$namespace" 125 | ); 126 | } 127 | 128 | return parent::setNamespace( 129 | config('repository-service-pattern.resource.namespace') 130 | ); 131 | } 132 | 133 | /** 134 | * Get resource body 135 | * 136 | * @return array 137 | * @throws TemplateException 138 | */ 139 | protected function getResourceBody(): array 140 | { 141 | if (is_null($this->modelWithNamespace)) { 142 | return ['return [', '', '];']; 143 | } 144 | 145 | /** @var Model $model */ 146 | $model = resolve($this->modelWithNamespace); 147 | 148 | if (!$model instanceof Model) { 149 | throw new TemplateException('Model must be an instance of ' . Model::class); 150 | } 151 | 152 | $this->addUse($this->modelWithNamespace); 153 | 154 | $this->setDocBlockContent(['Class ' . $this->getName(), '', '@mixin ' . $this->modelName]); 155 | 156 | $body = ['return [']; 157 | 158 | $tabFormatter = resolve(TabFormatter::class); 159 | 160 | foreach (array_unique(array_merge(Arr::wrap($model->getKeyName()), $model->getFillable())) as $fillable) { 161 | $body[] = $tabFormatter->getFormat() . "'$fillable'" . ' => $this->' . $fillable . ','; 162 | } 163 | 164 | $body[] = '];'; 165 | 166 | return $body; 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /tests/Unit/RepositoryFiltersTest.php: -------------------------------------------------------------------------------- 1 | andReturn(array_keys($testTableData)); 40 | 41 | $this->createModel([]); 42 | $relatedId = DB::table('relations')->insertGetId(['property' => $propertyVal]); 43 | $model = $this->createModel(array_merge($testTableData, ['related_id' => $relatedId])); 44 | 45 | $collection = $this->repository->findMany($search); 46 | $this->assertCount(1, $collection); 47 | $this->assertEquals($model->getKey(), optional($collection->first())->getKey()); 48 | } 49 | 50 | public function dataForTestProvider(): array 51 | { 52 | return [ 53 | // tests table data, relations table data, filters 54 | [['string' => 'stringVal'], 'val', ['string' => 'stringVal']], 55 | [['string' => 'stringVal'], 'val', ['string', 'like', '%stringVal%']], 56 | [['string' => 'stringVal'], 'val', [['string' => 'stringVal']]], 57 | [['string' => 'stringVal'], 'val', [['string', 'stringVal']]], 58 | [['string' => 'stringVal'], 'val', ['string', 'stringVal']], 59 | [['integer' => 5], 'val', ['integer', 5]], 60 | [['integer' => 5], 'val', ['integer', '>', 3]], 61 | [['integer' => 5], 'val', ['integer', '<=', 5]], 62 | [['integer' => 5], 'val', ['integer', '=', 5]], 63 | [['integer' => 5], 'val', ['integer', '>=', 5]], 64 | [['integer' => 5], 'val', ['integer', 'in', [1, 3, 5]]], 65 | [['integer' => 5], 'val', ['integer', 'NOT_IN', [1, 3, 2]]], 66 | [['integer' => 5], 'val', ['integer', 'not_null']], 67 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'date', '2022-01-01']], 68 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'date <', '2022-01-02']], 69 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'date <=', '2022-01-01']], 70 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'date =', '2022-01-01']], 71 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'date >=', '2022-01-01']], 72 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'month', '01']], 73 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'month >=', '01']], 74 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'month <=', '01']], 75 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'year', '2022']], 76 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'year >=', '2022']], 77 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'year <=', '2022']], 78 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'day <=', '01']], 79 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'day', '01']], 80 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'day =', '01']], 81 | [['datetime' => '2022-01-01 10:00:00'], 'val', ['datetime', 'day >= ', '01']], 82 | [ 83 | [], 84 | 'val', 85 | [ 86 | 'related', 87 | 'has', 88 | function ($query) { 89 | $query->where('property', 'val'); 90 | } 91 | ] 92 | ], 93 | [['integer' => 5], 'val', ['integer', 'between', [1, 6]]], 94 | [['integer' => 5], 'val', ['integer', 'not_between', [6, 10]]], 95 | [['integer' => 5], 'val', ['integer', 'not_between', [6, 10]]], 96 | ]; 97 | } 98 | 99 | /** 100 | * @return void 101 | */ 102 | protected function runMigrations(): void 103 | { 104 | Schema::create('relations', function (Blueprint $table) { 105 | $table->id(); 106 | $table->integer('property'); 107 | $table->timestamps(); 108 | }); 109 | Schema::create('tests', function (Blueprint $table) { 110 | $table->id(); 111 | $table->string('string')->nullable(); 112 | $table->integer('integer')->nullable(); 113 | $table->dateTime('datetime')->nullable(); 114 | $table->foreignId('related_id')->nullable()->constrained('relations'); 115 | $table->timestamps(); 116 | $table->softDeletes(); 117 | }); 118 | } 119 | 120 | /** 121 | * @return void 122 | */ 123 | protected function initializeModel(): void 124 | { 125 | $this->model = new class extends Model { 126 | use SoftDeletes; 127 | 128 | protected $table = 'tests'; 129 | 130 | protected $fillable = ['string', 'integer', 'datetime', 'related_id', 'deleted_at']; 131 | 132 | protected $related; 133 | 134 | public function __construct(array $attributes = []) 135 | { 136 | parent::__construct($attributes); 137 | 138 | $this->related = new class extends Model { 139 | protected $table = 'relations'; 140 | protected $fillable = ['property']; 141 | }; 142 | } 143 | 144 | public function related(): BelongsTo 145 | { 146 | return $this->belongsTo(get_class($this->related), 'related_id'); 147 | } 148 | }; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/EntityNameModel.php: -------------------------------------------------------------------------------- 1 | type, [self::TYPE_INTERFACE, self::TYPE_TRAIT])) { 101 | throw new CodeGeneratorException("$this->type could not be final or abstract"); 102 | } 103 | 104 | if (!in_array($this->type, self::TYPES)) { 105 | throw new CodeGeneratorException('Invalid type given'); 106 | } 107 | 108 | $this->type = $type; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Set class type (final, abstract) 115 | * 116 | * @param string $classType 117 | * @return EntityNameModel 118 | * @throws CodeGeneratorException 119 | */ 120 | public function setClassType(string $classType): self 121 | { 122 | $this->classType = $classType; 123 | 124 | if (!in_array($this->classType, self::CLASS_TYPES)) { 125 | throw new CodeGeneratorException('Invalid class type given'); 126 | } 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Set class/interface name 133 | * 134 | * @param string $name 135 | * @return $this 136 | */ 137 | public function setName(string $name): self 138 | { 139 | $this->name = $name; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Set extends of interface/class 146 | * 147 | * @param string|array $extends 148 | * @return EntityNameModel 149 | * @throws CodeGeneratorException 150 | */ 151 | public function setExtends($extends): self 152 | { 153 | if ($this->type === self::TYPE_CLASS && count($extends) > 1) { 154 | throw new CodeGeneratorException('Class can not extend more that 1 class'); 155 | } 156 | 157 | if ($this->type === self::TYPE_TRAIT) { 158 | throw new CodeGeneratorException('Trait could not extend any classes'); 159 | } 160 | 161 | $this->extends = Arr::wrap($extends); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * @param array $implements 168 | * @return EntityNameModel 169 | * @throws CodeGeneratorException 170 | */ 171 | public function setImplements(array $implements): self 172 | { 173 | if ($this->type === self::TYPE_TRAIT) { 174 | throw new CodeGeneratorException('Trait could not implement any interfaces'); 175 | } 176 | 177 | $this->implements = $implements; 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * @param DocBlockModel|null $docBlockModel 184 | * @return EntityNameModel 185 | */ 186 | public function setDocBlockModel(DocBlockModel $docBlockModel): self 187 | { 188 | $this->docBlockModel = $docBlockModel; 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * @param array $params 195 | * @return string 196 | */ 197 | public function render(array $params = []): string 198 | { 199 | $result = !is_null($this->docBlockModel) ? $this->docBlockModel->render() . PHP_EOL : ''; 200 | 201 | if (!is_null($this->classType)) { 202 | $result .= "$this->classType "; 203 | } 204 | 205 | $result .= "$this->type $this->name"; 206 | 207 | if (!empty($this->extends)) { 208 | $result .= " extends " . implode(', ', $this->extends); 209 | } 210 | 211 | if (!empty($this->implements)) { 212 | $result .= " implements " . implode(', ', $this->implements); 213 | } 214 | 215 | return $result; 216 | } 217 | 218 | /** 219 | * @return string 220 | */ 221 | public function getName(): string 222 | { 223 | return $this->name; 224 | } 225 | 226 | /** 227 | * @return array|string 228 | */ 229 | public function getExtends() 230 | { 231 | return $this->extends; 232 | } 233 | 234 | /** 235 | * @return array 236 | */ 237 | public function getImplements(): array 238 | { 239 | return $this->implements; 240 | } 241 | 242 | /** 243 | * @return string 244 | */ 245 | public function getClassType(): ?string 246 | { 247 | return $this->classType; 248 | } 249 | 250 | /** 251 | * @return string|null 252 | */ 253 | public function getType(): ?string 254 | { 255 | return $this->type; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Console/Commands/CrudGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | codeGeneratorService = $codeGeneratorService; 54 | } 55 | 56 | /** 57 | * Execute the console command. 58 | * 59 | * @return void 60 | * @throws Exception 61 | */ 62 | public function handle(): void 63 | { 64 | $modelName = $this->getEntityNameFromTableName($this->argument('table')); 65 | 66 | // Model generation 67 | $modelWithNamespace = config('repository-service-pattern.model.is_create_entity_folder') 68 | ? config('repository-service-pattern.model.namespace') . "\\$modelName\\$modelName" 69 | : config('repository-service-pattern.model.namespace') . "\\$modelName"; 70 | 71 | if (!class_exists($modelWithNamespace)) { 72 | $modelTemplate = new ModelTemplate($this->argument('table'), $modelName); 73 | 74 | $this->codeGeneratorService->generate($modelTemplate); 75 | $this->info("Generated Model '$modelName'"); 76 | } 77 | 78 | // Repository and RepositoryInterface generation 79 | $repositoryInterfaceTemplate = new RepositoryInterfaceTemplate($modelName); 80 | 81 | if (!interface_exists("{$repositoryInterfaceTemplate->getNamespace()}\\{$repositoryInterfaceTemplate->getName()}")) { 82 | $this->codeGeneratorService->generate($repositoryInterfaceTemplate); 83 | $this->info("Generated Repository Interface '{$repositoryInterfaceTemplate->getName()}'"); 84 | } 85 | 86 | $repositoryTemplate = new RepositoryTemplate($modelName); 87 | 88 | if (!class_exists("{$repositoryTemplate->getNamespace()}\\{$repositoryTemplate->getName()}")) { 89 | $this->codeGeneratorService->generate($repositoryTemplate); 90 | $this->info("Generated Repository '{$repositoryTemplate->getName()}'"); 91 | } 92 | 93 | // Service and ServiceInterface generation 94 | $serviceInterfaceTemplate = new ServiceInterfaceTemplate($modelName); 95 | 96 | if (!interface_exists("{$serviceInterfaceTemplate->getNamespace()}\\{$serviceInterfaceTemplate->getName()}")) { 97 | $this->codeGeneratorService->generate($serviceInterfaceTemplate); 98 | $this->info("Generated Service Interface '{$serviceInterfaceTemplate->getName()}'"); 99 | } 100 | 101 | $serviceTemplate = new ServiceTemplate($modelName); 102 | 103 | if (!class_exists("{$serviceTemplate->getNamespace()}\\{$serviceTemplate->getName()}")) { 104 | $this->codeGeneratorService->generate($serviceTemplate); 105 | $this->info("Generated Service '{$serviceTemplate->getName()}'"); 106 | } 107 | 108 | // Resource generation 109 | $resourceTemplate = new ResourceTemplate($modelName); 110 | 111 | if (!class_exists("{$resourceTemplate->getNamespace()}\\{$resourceTemplate->getName()}")) { 112 | $this->codeGeneratorService->generate($resourceTemplate); 113 | $this->info("Generated Resource '{$resourceTemplate->getName()}'"); 114 | } 115 | 116 | // Requests generation 117 | $storeRequestTemplate = new RequestTemplate( 118 | config('repository-service-pattern.controller.store_request_name'), 119 | $modelName, 120 | $modelWithNamespace 121 | ); 122 | 123 | if (!class_exists("{$storeRequestTemplate->getNamespace()}\\{$storeRequestTemplate->getName()}")) { 124 | $this->codeGeneratorService->generate($storeRequestTemplate); 125 | $this->info("Generated Store Request '{$storeRequestTemplate->getName()}'"); 126 | } 127 | 128 | $updateRequestTemplate = new RequestTemplate( 129 | config('repository-service-pattern.controller.update_request_name'), 130 | $modelName, 131 | $modelWithNamespace 132 | ); 133 | 134 | if (!class_exists("{$updateRequestTemplate->getNamespace()}\\{$updateRequestTemplate->getName()}")) { 135 | $this->codeGeneratorService->generate($updateRequestTemplate); 136 | $this->info("Generated Update Request '{$updateRequestTemplate->getName()}'"); 137 | } 138 | 139 | // Controller generation 140 | $controllerTemplate = new ApiResourceControllerTemplate( 141 | $modelName, 142 | "{$serviceInterfaceTemplate->getNamespace()}\\{$serviceInterfaceTemplate->getName()}" 143 | ); 144 | 145 | if (!class_exists("{$controllerTemplate->getNamespace()}\\{$controllerTemplate->getName()}")) { 146 | $this->codeGeneratorService->generate($controllerTemplate); 147 | $this->info("Generated Controller '{$controllerTemplate->getName()}'"); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/CodeGenerator/Models/MethodModel.php: -------------------------------------------------------------------------------- 1 | formatter) ? $this->formatter->getFormat() : ''); 75 | $lines = [ 76 | !is_null($this->docBlock) ? $this->docBlock->render() . PHP_EOL : null, 77 | $format . $this->access, 78 | ' ', 79 | $this->methodType ? $this->methodType . ' ' : '', 80 | 'function', 81 | ' ' 82 | ]; 83 | 84 | $methodSignature = []; 85 | 86 | foreach ($this->arguments as $argument) { 87 | 88 | if (!$argument instanceof MethodArgumentModel) { 89 | throw new CodeGeneratorException('Method argument is invalid'); 90 | } 91 | 92 | $methodSignature[] = $argument->render(); 93 | } 94 | 95 | $lines[] = !empty($methodSignature) 96 | ? $this->name . '(' . implode(', ', $methodSignature) . ')' 97 | : $this->name . '()'; 98 | 99 | if ($this->returnType) { 100 | $lines[] = ': ' . $this->returnType; 101 | } 102 | $lines[] = PHP_EOL . $format . '{' . PHP_EOL; 103 | 104 | if (!is_null($this->body)) { 105 | $lines[] = $this->body->render(); 106 | } 107 | 108 | $lines[] = $format . '}'; 109 | 110 | return implode('', array_filter($lines)); 111 | } 112 | 113 | /** 114 | * @param string $access 115 | * @return MethodModel 116 | * @throws CodeGeneratorException 117 | */ 118 | public function setAccess(string $access): MethodModel 119 | { 120 | if (!in_array($access, [self::ACCESS_PUBLIC, self::ACCESS_PROTECTED, self::ACCESS_PRIVATE])) { 121 | throw new CodeGeneratorException('Invalid access for method provided'); 122 | } 123 | 124 | $this->access = $access; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * @param mixed $name 131 | * @return MethodModel 132 | */ 133 | public function setName(string $name): self 134 | { 135 | $this->name = $name; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @param array|MethodArgumentModel[] $arguments 142 | * @return MethodModel 143 | */ 144 | public function setArguments(array $arguments): MethodModel 145 | { 146 | $this->arguments = $arguments; 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * @param string $returnType 153 | * @return MethodModel 154 | */ 155 | public function setReturnType(string $returnType): MethodModel 156 | { 157 | $this->returnType = $returnType; 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * @param DocBlockModel $docBlock 164 | * @return MethodModel 165 | */ 166 | public function setDocBlock(DocBlockModel $docBlock): MethodModel 167 | { 168 | $this->docBlock = $docBlock; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * @param string $methodType 175 | * @return MethodModel 176 | * @throws CodeGeneratorException 177 | */ 178 | public function setMethodType(string $methodType): MethodModel 179 | { 180 | if (!in_array($methodType, [self::TYPE_FINAL, self::TYPE_ABSTRACT])) { 181 | throw new CodeGeneratorException('Invalid method type given'); 182 | } 183 | 184 | $this->methodType = $methodType; 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * @param FormatterInterface|null $formatter 191 | * @return MethodModel 192 | */ 193 | public function setFormatter(FormatterInterface $formatter): MethodModel 194 | { 195 | $this->formatter = $formatter; 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * @param MethodBodyModel|null $body 202 | * @return MethodModel 203 | */ 204 | public function setBody(MethodBodyModel $body): MethodModel 205 | { 206 | $this->body = $body; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * @return array|MethodArgumentModel[] 213 | */ 214 | public function getArguments(): array 215 | { 216 | return $this->arguments; 217 | } 218 | 219 | /** 220 | * @return string 221 | */ 222 | public function getName(): string 223 | { 224 | return $this->name; 225 | } 226 | 227 | /** 228 | * @return string|null 229 | */ 230 | public function getMethodType(): ?string 231 | { 232 | return $this->methodType; 233 | } 234 | 235 | /** 236 | * @return string 237 | */ 238 | public function getReturnType(): string 239 | { 240 | return $this->returnType; 241 | } 242 | 243 | /** 244 | * @return DocBlockModel|null 245 | */ 246 | public function getDocBlock(): ?DocBlockModel 247 | { 248 | return $this->docBlock; 249 | } 250 | 251 | /** 252 | * @return MethodBodyModel|null 253 | */ 254 | public function getBody(): ?MethodBodyModel 255 | { 256 | return $this->body; 257 | } 258 | 259 | /** 260 | * @return string 261 | */ 262 | public function getAccess(): string 263 | { 264 | return $this->access; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/CodeGenerator/Services/DbManagerService.php: -------------------------------------------------------------------------------- 1 | 'array', 47 | 'simple_array' => 'array', 48 | 'json_array' => 'string', 49 | 'bigint' => 'integer', 50 | 'boolean' => 'boolean', 51 | 'datetime' => 'Carbon', 52 | 'datetimetz' => 'string', 53 | 'date' => 'Carbon', 54 | 'time' => 'string', 55 | 'decimal' => 'float', 56 | 'integer' => 'integer', 57 | 'object' => 'object', 58 | 'smallint' => 'integer', 59 | 'string' => 'string', 60 | 'text' => 'string', 61 | 'binary' => 'string', 62 | 'blob' => 'string', 63 | 'float' => 'float', 64 | 'guid' => 'string', 65 | ]; 66 | 67 | /** 68 | * @param DatabaseManager $dbManager 69 | */ 70 | public function __construct(DatabaseManager $dbManager) 71 | { 72 | $this->dbManager = $dbManager; 73 | $this->schemaManager = $this->dbManager->connection(config('database.default'))->getDoctrineSchemaManager(); 74 | $this->tablePrefix = $this->dbManager->connection(config('database.default'))->getTablePrefix(); 75 | } 76 | 77 | /** 78 | * Get table PK 79 | * 80 | * @param string $tableName 81 | * @return mixed 82 | */ 83 | public function getPrimaryKey(string $tableName) 84 | { 85 | $tableDetails = $this->schemaManager->listTableDetails($this->tablePrefix . $tableName); 86 | 87 | $primaryKey = $tableDetails->getPrimaryKey() ? $tableDetails->getPrimaryKey()->getColumns() : []; 88 | 89 | if (count($primaryKey) === 1) { 90 | $primaryKey = $primaryKey[0]; 91 | } 92 | 93 | return $primaryKey; 94 | } 95 | 96 | /** 97 | * @param string $tableName 98 | * @return array 99 | */ 100 | public function getTableProperties(string $tableName): array 101 | { 102 | $properties = []; 103 | $tableDetails = $this->schemaManager->listTableDetails($this->tablePrefix . $tableName); 104 | 105 | foreach ($tableDetails->getColumns() as $column) { 106 | 107 | $type = $this->resolveType($column->getType()->getName()); 108 | $properties[$column->getName()] = $type; 109 | } 110 | 111 | return $properties; 112 | } 113 | 114 | /** 115 | * Get table columns 116 | * 117 | * @param string $tableName 118 | * @return array 119 | */ 120 | public function getTableColumns(string $tableName): array 121 | { 122 | return $this->schemaManager 123 | ->listTableDetails($this->tablePrefix . $tableName) 124 | ->getColumns(); 125 | } 126 | 127 | /** 128 | * Get table relations and relation to the table 129 | * 130 | * @param string $tableName 131 | * @return array 132 | */ 133 | public function getRelations(string $tableName): array 134 | { 135 | $relations = []; 136 | 137 | foreach ($this->schemaManager->listTableForeignKeys($this->tablePrefix . $tableName) as $tableForeignKey) { 138 | $tableForeignColumns = $tableForeignKey->getForeignColumns(); 139 | if (count($tableForeignColumns) !== 1) { 140 | continue; 141 | } 142 | 143 | $relations[] = [ 144 | 'type' => self::RELATION_BELONGS_TO, 145 | 'relatedTable' => $tableForeignKey->getForeignTableName(), 146 | 'foreignKey' => $tableForeignKey->getLocalColumns()[0], 147 | 'localKey' => $tableForeignColumns[0] 148 | ]; 149 | } 150 | 151 | foreach ($this->schemaManager->listTables() as $table) { 152 | if ($table->getName() === $this->tablePrefix . $tableName) { 153 | continue; 154 | } 155 | 156 | $foreignKeys = $table->getForeignKeys(); 157 | 158 | foreach ($foreignKeys as $name => $foreignKey) { 159 | 160 | if ($foreignKey->getForeignTableName() !== $this->tablePrefix . $tableName) { 161 | continue; 162 | } 163 | 164 | $localColumns = $foreignKey->getLocalColumns(); 165 | 166 | if (count($localColumns) !== 1) { 167 | continue; 168 | } 169 | 170 | if (count($foreignKeys) === 2 && count($table->getColumns()) === 2) { 171 | 172 | $keys = array_keys($foreignKeys); 173 | $key = array_search($name, $keys) === 0 ? 1 : 0; 174 | $secondForeignKey = $foreignKeys[$keys[$key]]; 175 | $secondForeignTable = $this->removePrefix($secondForeignKey->getForeignTableName()); 176 | 177 | $relations[] = [ 178 | 'type' => self::RELATION_BELONGS_TO_MANY, 179 | 'relatedTable' => $secondForeignTable, 180 | 'joinTable' => $this->removePrefix($table->getName()), 181 | 'foreignKey' => $localColumns[0], 182 | 'localKey' => $secondForeignKey->getLocalColumns()[0] 183 | ]; 184 | 185 | break; 186 | } 187 | 188 | $relations[] = [ 189 | 'type' => $this->isColumnUnique($table, $localColumns[0]) 190 | ? self::RELATION_HAS_ONE 191 | : self::RELATION_HAS_MANY, 192 | 'relatedTable' => $this->removePrefix($foreignKey->getLocalTableName()), 193 | 'foreignKey' => $localColumns[0], 194 | 'localKey' => $foreignKey->getForeignColumns()[0] 195 | ]; 196 | } 197 | } 198 | 199 | return $relations; 200 | } 201 | 202 | /** 203 | * @param string $type 204 | * 205 | * @return string 206 | */ 207 | protected function resolveType($type): string 208 | { 209 | return $this->types[$type] ?? 'mixed'; 210 | } 211 | 212 | /** 213 | * @param Table $table 214 | * @param string $column 215 | * @return bool 216 | */ 217 | protected function isColumnUnique(Table $table, string $column): bool 218 | { 219 | foreach ($table->getIndexes() as $index) { 220 | $indexColumns = $index->getColumns(); 221 | if (count($indexColumns) !== 1) { 222 | continue; 223 | } 224 | $indexColumn = $indexColumns[0]; 225 | if ($indexColumn === $column && $index->isUnique()) { 226 | return true; 227 | } 228 | } 229 | 230 | return false; 231 | } 232 | 233 | /** 234 | * Remove prefix from table name 235 | * 236 | * @param string $tableName 237 | * @return string 238 | */ 239 | protected function removePrefix(string $tableName): string 240 | { 241 | $prefix = preg_quote($this->tablePrefix, '/'); 242 | 243 | return preg_replace("/^$prefix/", '', $tableName); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Services/BaseCrudService.php: -------------------------------------------------------------------------------- 1 | repository = app($this->getRepositoryClass()); 35 | } 36 | 37 | /** 38 | * Set with for repository querying 39 | * 40 | * @param array $with 41 | * @return BaseCrudServiceInterface 42 | */ 43 | public function with(array $with): BaseCrudServiceInterface 44 | { 45 | $this->repository->with($with); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Set withCount for repository querying 52 | * 53 | * @param array $withCount 54 | * @return BaseCrudServiceInterface 55 | */ 56 | public function withCount(array $withCount): BaseCrudServiceInterface 57 | { 58 | $this->repository->withCount($withCount); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Include soft deleted records to a query 65 | * 66 | * @return BaseCrudServiceInterface 67 | */ 68 | public function withTrashed(): BaseCrudServiceInterface 69 | { 70 | $this->repository->withTrashed(); 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Show only soft deleted records in a query 77 | * 78 | * @return BaseCrudServiceInterface 79 | */ 80 | public function onlyTrashed(): BaseCrudServiceInterface 81 | { 82 | $this->repository->onlyTrashed(); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Exclude soft deleted records from a query 89 | * 90 | * @return BaseCrudServiceInterface 91 | */ 92 | public function withoutTrashed(): BaseCrudServiceInterface 93 | { 94 | $this->repository->withoutTrashed(); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Get filtered results 101 | * 102 | * @param array $search 103 | * @param int $pageSize 104 | * @return LengthAwarePaginator 105 | * @throws ContainerExceptionInterface 106 | * @throws NotFoundExceptionInterface 107 | */ 108 | public function getAllPaginated(array $search = [], int $pageSize = 15): LengthAwarePaginator 109 | { 110 | return $this->repository->getAllPaginated($search, request()->get('page_size', $pageSize)); 111 | } 112 | 113 | /** 114 | * Get all records as collection 115 | * 116 | * @param array $search 117 | * @return EloquentCollection 118 | */ 119 | public function getAll(array $search = []): EloquentCollection 120 | { 121 | return $this->repository->getAll($search); 122 | } 123 | 124 | /** 125 | * Get all records as lazy collection (cursor) 126 | * 127 | * @param array $search 128 | * @return LazyCollection 129 | */ 130 | public function getAllAsCursor(array $search = []): LazyCollection 131 | { 132 | return $this->repository->getAllCursor($search); 133 | } 134 | 135 | /** 136 | * Get results count 137 | * 138 | * @throws RepositoryException 139 | */ 140 | public function count(array $search = []): int 141 | { 142 | return $this->repository->count($search); 143 | } 144 | 145 | /** 146 | * Find or fail the model 147 | * 148 | * @param $key 149 | * @param string|null $column 150 | * @return Model 151 | */ 152 | public function findOrFail($key, string $column = null): Model 153 | { 154 | return $this->repository->findOrFail($key, $column); 155 | } 156 | 157 | /** 158 | * Find models by attributes 159 | * 160 | * @param array $attributes 161 | * @return Collection 162 | */ 163 | public function find(array $attributes): Collection 164 | { 165 | return $this->repository->findMany($attributes); 166 | } 167 | 168 | /** 169 | * Create model 170 | * 171 | * @param array $data 172 | * @return Model|null 173 | * @throws ServiceException 174 | */ 175 | public function create(array $data): ?Model 176 | { 177 | if (is_null($model = $this->repository->create($data))) { 178 | throw new ServiceException('Error while creating model'); 179 | } 180 | 181 | return $model; 182 | } 183 | 184 | /** 185 | * Insert data into db 186 | * 187 | * @param array $data 188 | * @return bool 189 | */ 190 | public function insert(array $data): bool 191 | { 192 | return $this->repository->insert($data); 193 | } 194 | 195 | /** 196 | * Create many models 197 | * 198 | * @param array $attributes 199 | * @return Collection 200 | * @throws ServiceException 201 | */ 202 | public function createMany(array $attributes): Collection 203 | { 204 | if (empty($attributes)) { 205 | throw new ServiceException('Data is empty'); 206 | } 207 | 208 | return DB::transaction(function () use ($attributes) { 209 | $models = collect(); 210 | 211 | foreach ($attributes as $data) { 212 | $models->push($this->create($data)); 213 | } 214 | 215 | return $models; 216 | }); 217 | } 218 | 219 | /** 220 | * Update or create model 221 | * 222 | * @param array $attributes 223 | * @param array $data 224 | * @return Model|null 225 | * @throws ServiceException 226 | */ 227 | public function updateOrCreate(array $attributes, array $data): ?Model 228 | { 229 | if (is_null($model = $this->repository->updateOrCreate($attributes, $data))) { 230 | throw new ServiceException('Error while creating or updating the model'); 231 | } 232 | 233 | return $model; 234 | } 235 | 236 | /** 237 | * Update model 238 | * 239 | * @param $keyOrModel 240 | * @param array $data 241 | * @return Model|null 242 | */ 243 | public function update($keyOrModel, array $data): ?Model 244 | { 245 | return $this->repository->update($keyOrModel, $data); 246 | } 247 | 248 | /** 249 | * Delete model 250 | * 251 | * @param $keyOrModel 252 | * @return bool 253 | * @throws Exception 254 | */ 255 | public function delete($keyOrModel): bool 256 | { 257 | if (!$this->repository->delete($keyOrModel)) { 258 | throw new ServiceException('Error while deleting model'); 259 | } 260 | 261 | return true; 262 | } 263 | 264 | /** 265 | * Delete many records 266 | * 267 | * @param array $keysOrModels 268 | * @return void 269 | */ 270 | public function deleteMany(array $keysOrModels): void 271 | { 272 | DB::transaction(function () use ($keysOrModels) { 273 | foreach ($keysOrModels as $keyOrModel) { 274 | $this->delete($keyOrModel); 275 | } 276 | }); 277 | } 278 | 279 | /** 280 | * Perform soft delete on model 281 | * 282 | * @param $keyOrModel 283 | * @return void 284 | */ 285 | public function softDelete($keyOrModel): void 286 | { 287 | $this->repository->softDelete($keyOrModel); 288 | } 289 | 290 | /** 291 | * Restore model 292 | * 293 | * @param $keyOrModel 294 | * @return void 295 | * @throws RepositoryException 296 | */ 297 | public function restore($keyOrModel): void 298 | { 299 | $this->repository->restore($keyOrModel); 300 | } 301 | 302 | /** 303 | * Should return RepositoryInterface::class 304 | * 305 | * @return string 306 | */ 307 | abstract protected function getRepositoryClass(): string; 308 | } 309 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/ApiResourceController/ApiResourceControllerTemplate.php: -------------------------------------------------------------------------------- 1 | setName($entityName)->setNamespace($entityName); 63 | 64 | $this->serviceInterfaceNamespace = $serviceInterfaceNamespace; 65 | $this->serviceName = $this->getServiceNameFromNamespace(); 66 | $this->modelName = $entityName; 67 | $this->resourceName = "{$entityName}Resource"; 68 | $this->storeRequestName = config('repository-service-pattern.controller.store_request_name'); 69 | $this->updateRequestName = config('repository-service-pattern.controller.update_request_name'); 70 | } 71 | 72 | /** 73 | * @param string $name 74 | * @return ClassTemplate 75 | */ 76 | public function setName(string $name): ClassTemplate 77 | { 78 | return parent::setName($name . 'Controller'); 79 | } 80 | 81 | /** 82 | * @param array $params 83 | * @return string 84 | * @throws TemplateException 85 | * @throws CodeGeneratorException 86 | */ 87 | public function render(array $params = []): string 88 | { 89 | $this 90 | ->addUse($this->serviceInterfaceNamespace) 91 | ->addUse(AnonymousResourceCollection::class) 92 | ->addUse(JsonResponse::class) 93 | ->addUse(Exception::class) 94 | ->addUse(Response::class) 95 | ->addUse(ResponseStatus::class . ' as ResponseStatus') 96 | ->addUse(config('repository-service-pattern.request.namespace') . "\\$this->modelName\\$this->storeRequestName") 97 | ->addUse(config('repository-service-pattern.request.namespace') . "\\$this->modelName\\$this->updateRequestName") 98 | ->addUse(config('repository-service-pattern.resource.namespace') . "\\$this->modelName\\$this->resourceName") 99 | ->addUse(config('repository-service-pattern.model.namespace') . "\\$this->modelName\\$this->modelName") 100 | ->setExtends(Controller::class) 101 | ->addProperty( 102 | 'service', 103 | PropertyModel::VALUE_NON_INITIALIZED, 104 | PropertyModel::ACCESS_PROTECTED, 105 | ['@var ' . $this->serviceName] 106 | ) 107 | ->setConstruct() 108 | ->setIndexAction() 109 | ->setShowAction() 110 | ->setStoreAction() 111 | ->setUpdateAction() 112 | ->setDestroyAction(); 113 | 114 | return parent::render($params); 115 | } 116 | 117 | /** 118 | * @param string|null $namespace 119 | * @return void 120 | */ 121 | public function setNamespace(string $namespace): ClassTemplate 122 | { 123 | if (config('repository-service-pattern.controller.is_create_entity_folder')) { 124 | return parent::setNamespace( 125 | config('repository-service-pattern.controller.namespace') . "\\$namespace" 126 | ); 127 | } 128 | 129 | return parent::setNamespace(config('repository-service-pattern.controller.namespace')); 130 | } 131 | 132 | /** 133 | * @return string 134 | */ 135 | protected function getServiceNameFromNamespace(): string 136 | { 137 | return last(explode('\\', $this->serviceInterfaceNamespace)); 138 | } 139 | 140 | /** 141 | * Set up construct method 142 | * 143 | * @return $this 144 | * @throws CodeGeneratorException 145 | * @throws TemplateException 146 | */ 147 | protected function setConstruct(): self 148 | { 149 | return $this->addMethod( 150 | '__construct', 151 | [['type' => $this->serviceName, 'name' => 'service']], 152 | MethodModel::ACCESS_PUBLIC, 153 | null, 154 | null, 155 | ['$this->service = $service;'] 156 | ); 157 | } 158 | 159 | /** 160 | * Set index controller action 161 | * 162 | * @return $this 163 | * @throws CodeGeneratorException 164 | * @throws TemplateException 165 | */ 166 | protected function setIndexAction(): self 167 | { 168 | return $this 169 | ->addMethod( 170 | 'index', 171 | [], 172 | MethodModel::ACCESS_PUBLIC, 173 | null, 174 | 'AnonymousResourceCollection', 175 | ["return {$this->modelName}Resource::collection(" . '$this->service->getAllPaginated());'] 176 | ); 177 | } 178 | 179 | /** 180 | * Set show action 181 | * 182 | * @return $this 183 | * @throws CodeGeneratorException 184 | * @throws TemplateException 185 | */ 186 | protected function setShowAction(): self 187 | { 188 | $varName = Str::camel($this->modelName); 189 | return $this 190 | ->addMethod( 191 | 'show', 192 | [['type' => $this->modelName, 'name' => $varName]], 193 | MethodModel::ACCESS_PUBLIC, 194 | null, 195 | $this->resourceName, 196 | ["return $this->resourceName::make(" . '$' . $varName . ');'] 197 | ); 198 | } 199 | 200 | /** 201 | * Set store action 202 | * 203 | * @return $this 204 | * @throws CodeGeneratorException 205 | * @throws TemplateException 206 | */ 207 | protected function setStoreAction(): self 208 | { 209 | return $this->addMethod( 210 | 'store', 211 | [['type' => $this->storeRequestName, 'name' => 'request']], 212 | MethodModel::ACCESS_PUBLIC, 213 | null, 214 | $this->resourceName, 215 | ['return ' . $this->resourceName . '::make($this->service->create($request->validated()), ResponseStatus::HTTP_CREATED);'] 216 | ); 217 | } 218 | 219 | /** 220 | * Set update action 221 | * 222 | * @return $this 223 | * @throws CodeGeneratorException 224 | * @throws TemplateException 225 | */ 226 | protected function setUpdateAction(): self 227 | { 228 | $varName = Str::camel($this->modelName); 229 | return $this->addMethod( 230 | 'update', 231 | [['type' => $this->modelName, 'name' => $varName], ['type' => $this->updateRequestName, 'name' => 'request']], 232 | MethodModel::ACCESS_PUBLIC, 233 | null, 234 | $this->resourceName, 235 | ['return ' . $this->resourceName . '::make($this->service->update($' . $varName . ', $request->validated()));'] 236 | ); 237 | } 238 | 239 | /** 240 | * Set destroy action 241 | * 242 | * @return $this 243 | * @throws CodeGeneratorException 244 | * @throws TemplateException 245 | */ 246 | protected function setDestroyAction(): self 247 | { 248 | $varName = Str::camel($this->modelName); 249 | return $this 250 | ->addMethod( 251 | 'destroy', 252 | [['type' => $this->modelName, 'name' => $varName]], 253 | MethodModel::ACCESS_PUBLIC, 254 | null, 255 | 'JsonResponse', 256 | [ 257 | '$this->service->delete($' . $varName . ');', 258 | '', 259 | "return Response::json(null, ResponseStatus::HTTP_NO_CONTENT);" 260 | ], 261 | [ 262 | '@param ' . $this->modelName . ' $' . $varName, 263 | '', 264 | '@return JsonResponse', 265 | '@throws Exception' 266 | ] 267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Repositories/BaseCacheableRepository.php: -------------------------------------------------------------------------------- 1 | cacheDriver = app($this->cacheDriver); 53 | } 54 | 55 | /** 56 | * Get models collection 57 | * 58 | * @param array $search 59 | * @return Collection 60 | * @throws RepositoryException 61 | */ 62 | public function getAll(array $search = []): Collection 63 | { 64 | return $this->cacheDriver->remember( 65 | $this->generateCacheKey( 66 | self::KEY_ALL, 67 | array_merge( 68 | $search, 69 | [ 70 | 'with' => $this->with, 71 | 'withCount' => $this->withCount, 72 | 'softDeleteQueryMode' => $this->softDeleteQueryMode 73 | ] 74 | ) 75 | ), 76 | $this->getTtl(), 77 | function () use ($search) { 78 | return parent::getAll($search); 79 | } 80 | ); 81 | } 82 | 83 | /** 84 | * Get paginated data 85 | * 86 | * @param array $search 87 | * @param int $pageSize 88 | * @return LengthAwarePaginator 89 | * @throws RepositoryException 90 | */ 91 | public function getAllPaginated(array $search = [], int $pageSize = 15): LengthAwarePaginator 92 | { 93 | $page = $search['page'] ?? (request()?->input('page', 1) ?? 1); 94 | 95 | return $this->cacheDriver->remember( 96 | $this->generateCacheKey( 97 | self::KEY_PAGINATED, 98 | array_merge( 99 | $search, 100 | [ 101 | 'with' => $this->with, 102 | 'withCount' => $this->withCount, 103 | 'softDeleteQueryMode' => $this->softDeleteQueryMode, 104 | 'pageSize' => $pageSize, 105 | 'page' => (int) $page, 106 | ] 107 | ) 108 | ), 109 | $this->getTtl(), 110 | function () use ($search, $pageSize, $page) { 111 | $search['page'] = $page; 112 | return parent::getAllPaginated($search, $pageSize); 113 | } 114 | ); 115 | } 116 | 117 | /** 118 | * Get all as cursor 119 | * 120 | * @param array $search 121 | * @return LazyCollection 122 | * @throws RepositoryException 123 | */ 124 | public function getAllCursor(array $search = []): LazyCollection 125 | { 126 | return parent::getAllCursor($search); 127 | } 128 | 129 | /** 130 | * Find first model 131 | * 132 | * @param array $attributes 133 | * @return Model|null 134 | * @throws RepositoryException 135 | */ 136 | public function findFirst(array $attributes): ?Model 137 | { 138 | $model = parent::findFirst($attributes); 139 | 140 | $this->cacheModel($model); 141 | 142 | return $model; 143 | } 144 | 145 | /** 146 | * Find model by PK 147 | * 148 | * @param $key 149 | * @return Model|null 150 | * @throws RepositoryException 151 | */ 152 | public function find($key): ?Model 153 | { 154 | return $this->cacheDriver->remember( 155 | $this->generateCacheKeyForModelInstance($key), 156 | $this->getTtl(), 157 | function () use ($key) { 158 | return parent::find($key); 159 | } 160 | ); 161 | } 162 | 163 | /** 164 | * Find or fail by primary key or custom column 165 | * 166 | * @param $value 167 | * @param string|null $column 168 | * @return Model 169 | * @throws RepositoryException 170 | */ 171 | public function findOrFail($value, ?string $column = null): Model 172 | { 173 | $keyData = is_null($column) 174 | ? $this->generateCacheKeyForModelInstance($value) 175 | : $this->generateCacheKeyForModelInstance("{$column}.{$value}"); 176 | 177 | return $this->cacheDriver->remember( 178 | $keyData, 179 | $this->getTtl(), 180 | fn() => parent::findOrFail($value, $column) 181 | ); 182 | } 183 | 184 | /** 185 | * Insert data into DB 186 | * 187 | * @param array $data 188 | * @return bool 189 | */ 190 | public function insert(array $data): bool 191 | { 192 | $this->flushListsCaches(); 193 | 194 | return parent::insert($data); 195 | } 196 | 197 | /** 198 | * Create model with data 199 | * 200 | * @param array $data 201 | * @return Model|null 202 | * @throws RepositoryException 203 | */ 204 | public function create(array $data): ?Model 205 | { 206 | $model = parent::create($data); 207 | 208 | $this->cacheModel($model); 209 | $this->flushListsCaches(); 210 | 211 | return $model; 212 | } 213 | 214 | /** 215 | * Update model 216 | * 217 | * @param Model|mixed $keyOrModel 218 | * @param array $data 219 | * @return Model|null 220 | * @throws RepositoryException 221 | */ 222 | public function update($keyOrModel, array $data): ?Model 223 | { 224 | $model = parent::update($keyOrModel, $data); 225 | 226 | $this->cacheModel($model); 227 | $this->flushListsCaches(); 228 | 229 | return $model; 230 | } 231 | 232 | /** 233 | * Update or create model 234 | * 235 | * @param array $attributes 236 | * @param array $data 237 | * @return Model|null 238 | * @throws RepositoryException 239 | */ 240 | public function updateOrCreate(array $attributes, array $data): ?Model 241 | { 242 | $model = parent::updateOrCreate($attributes, $data); 243 | 244 | $this->cacheModel($model); 245 | $this->flushListsCaches(); 246 | 247 | return $model; 248 | } 249 | 250 | /** 251 | * Delete model 252 | * 253 | * @param Model|mixed $keyOrModel 254 | * @return bool 255 | * @throws Exception 256 | */ 257 | public function delete($keyOrModel): bool 258 | { 259 | $model = $this->resolveModel($keyOrModel); 260 | 261 | $this->cacheDriver->forget($this->generateCacheKeyForModelInstance($model->getKey())); 262 | $this->flushListsCaches(); 263 | 264 | return parent::delete($model); 265 | } 266 | 267 | /** 268 | * Soft delete model 269 | * 270 | * @param $keyOrModel 271 | * @return void 272 | * @throws RepositoryException 273 | */ 274 | public function softDelete($keyOrModel): void 275 | { 276 | $model = $this->resolveModel($keyOrModel); 277 | parent::softDelete($model); 278 | 279 | $this->flushListsCaches(); 280 | $this->cacheDriver->forget($this->generateCacheKeyForModelInstance($model->getKey())); 281 | } 282 | 283 | /** 284 | * Soft delete model 285 | * 286 | * @param $keyOrModel 287 | * @return void 288 | * @throws RepositoryException 289 | */ 290 | public function restore($keyOrModel): void 291 | { 292 | parent::restore($keyOrModel); 293 | 294 | $this->cacheModel($keyOrModel); 295 | $this->flushListsCaches(); 296 | } 297 | 298 | /** 299 | * Clear get all & paginated cache keys 300 | * 301 | * @return void 302 | */ 303 | public function flushListsCaches(): void 304 | { 305 | $this->cacheDriver->forget($this->generateCacheKey(self::KEY_ALL)); 306 | $this->cacheDriver->forget($this->generateCacheKey(self::KEY_PAGINATED)); 307 | } 308 | 309 | /** 310 | * Get cache key from query params 311 | * 312 | * @param string $keyName 313 | * @param array $params 314 | * @return array 315 | */ 316 | protected function generateCacheKey(string $keyName, array $params = []): array 317 | { 318 | ksort($params); 319 | $alias = $this->cacheAlias ?? Str::camel(class_basename($this->getModelClass())); 320 | $hash = !empty($params) ? md5(serialize($params)) : ''; 321 | 322 | return [ 323 | 'tags' => ["$alias.$keyName"], 324 | 'keyWithTag' => "$alias.$keyName" . ($hash ? ".$hash" : ''), 325 | 'paramsKey' => $hash, 326 | ]; 327 | } 328 | 329 | /** 330 | * Generate cache key for single model 331 | * 332 | * @param $primaryKey 333 | * @return array 334 | */ 335 | protected function generateCacheKeyForModelInstance($primaryKey): array 336 | { 337 | if (is_array($primaryKey)) { 338 | ksort($primaryKey); 339 | $primaryKey = implode('_', array_map(fn($k, $v) => "$k-$v", array_keys($primaryKey), $primaryKey)); 340 | } else { 341 | $primaryKey = (string) $primaryKey; 342 | } 343 | 344 | $alias = $this->cacheAlias ?? Str::camel(class_basename($this->getModelClass())); 345 | 346 | return [ 347 | 'tags' => ["$alias.model"], 348 | 'keyWithTag' => "$alias.$primaryKey", 349 | 'paramsKey' => "$alias.$primaryKey", 350 | ]; 351 | } 352 | 353 | /** 354 | * Cache model 355 | * 356 | * @param null|string|integer|Model $keyOrModel 357 | * @return void 358 | * @throws RepositoryException 359 | */ 360 | protected function cacheModel($keyOrModel = null): void 361 | { 362 | if (is_null($keyOrModel)) { 363 | return; 364 | } 365 | 366 | $model = $this->resolveModel($keyOrModel); 367 | 368 | $this->cacheDriver->put($this->generateCacheKeyForModelInstance($model->getKey()), $model, $this->getTtl()); 369 | } 370 | 371 | /** 372 | * Get cache ttl in seconds 373 | * 374 | * @return int 375 | */ 376 | protected function getTtl(): int 377 | { 378 | return $this->cacheTtl * 60; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/CodeGenerator/Templates/Model/ModelTemplate.php: -------------------------------------------------------------------------------- 1 | dbManager = resolve(DbManagerServiceInterface::class); 93 | 94 | $this->tableName = $tableName; 95 | $this->isCustomModelName = strtolower(Str::snake(Str::plural($modelName))) !== $this->tableName; 96 | 97 | $this->setName($modelName)->setNamespace($modelName); 98 | $this->parseModelProperties(); 99 | $this->parsePrimaryKey(); 100 | $this->collectDocBlockContent(); 101 | $this->parseRelations(); 102 | } 103 | 104 | /** 105 | * @param string $namespace 106 | * @return ClassTemplate 107 | */ 108 | public function setNamespace(string $namespace): ClassTemplate 109 | { 110 | if (config('repository-service-pattern.model.is_create_entity_folder')) { 111 | return parent::setNamespace( 112 | config('repository-service-pattern.model.namespace') . "\\$namespace" 113 | ); 114 | } 115 | 116 | return parent::setNamespace( 117 | config('repository-service-pattern.model.namespace') 118 | ); 119 | } 120 | 121 | /** 122 | * @param array $params 123 | * @return string 124 | * @throws Exception 125 | */ 126 | public function render(array $params = []): string 127 | { 128 | $this 129 | ->setExtends(BaseModel::class) 130 | ->setDocBlockContent($this->docBlockContent); 131 | 132 | if ($this->isUseCarbon || $this->isTimestamps) { 133 | $this->addUse(Carbon::class); 134 | } 135 | 136 | if (!$this->isTimestamps) { 137 | $this->addProperty('timestamps', 'false', PropertyModel::ACCESS_PUBLIC); 138 | } 139 | 140 | if ($this->isCustomModelName) { 141 | $this->addProperty('table', $this->tableName, PropertyModel::ACCESS_PROTECTED); 142 | } 143 | 144 | if ($this->primaryKey !== self::DEFAULT_PRIMARY_KEY) { 145 | $this 146 | ->addProperty('primaryKey', $this->primaryKey, PropertyModel::ACCESS_PROTECTED) 147 | ->addProperty('keyType', $this->keyType, PropertyModel::ACCESS_PROTECTED) 148 | ->addProperty('incrementing', false, PropertyModel::ACCESS_PUBLIC); 149 | } 150 | 151 | $this->addProperty('fillable', 152 | collect($this->modelProperties) 153 | ->keys() 154 | ->filter(function ($propertyName) { 155 | if ($this->primaryKey === self::DEFAULT_PRIMARY_KEY) { 156 | return !in_array($propertyName, Arr::wrap($this->primaryKey)); 157 | } 158 | 159 | return true; 160 | }) 161 | ->toArray(), 162 | PropertyModel::ACCESS_PROTECTED 163 | ); 164 | 165 | return parent::render($params); 166 | } 167 | 168 | /** 169 | * Parse primaryKey 170 | * 171 | * @return void 172 | */ 173 | protected function parsePrimaryKey(): void 174 | { 175 | $this->primaryKey = $this->dbManager->getPrimaryKey($this->tableName); 176 | $this->keyType = is_array($this->primaryKey) ? 'array' : $this->modelProperties[$this->primaryKey]; 177 | } 178 | 179 | /** 180 | * Collect doc block 181 | * 182 | * @return void 183 | */ 184 | protected function collectDocBlockContent(): void 185 | { 186 | $this->addDocBlockContent("Class " . $this->getName())->addDocBlockContent(''); 187 | 188 | foreach ($this->modelProperties as $name => $type) { 189 | $this->addDocBlockContent("@property $type $" . $name); 190 | } 191 | } 192 | 193 | /** 194 | * Parse table relations 195 | * 196 | * @return void 197 | * @throws Exception 198 | */ 199 | protected function parseRelations(): void 200 | { 201 | foreach ($this->dbManager->getRelations($this->tableName) as $relation) { 202 | $this->addRelation( 203 | $relation['type'], 204 | $relation['relatedTable'], 205 | $relation['foreignKey'], 206 | $relation['localKey'], 207 | $relation['joinTable'] ?? null 208 | ); 209 | } 210 | } 211 | 212 | /** 213 | * Parse table properties 214 | * 215 | * @return void 216 | */ 217 | protected function parseModelProperties(): void 218 | { 219 | $this->modelProperties = $this->dbManager->getTableProperties($this->tableName); 220 | $this->isTimestamps = !empty(array_intersect(array_keys($this->modelProperties), ['created_at', 'updated_at'])); 221 | $this->isUseCarbon = $this->isTimestamps || in_array('Carbon', $this->modelProperties); 222 | } 223 | 224 | /** 225 | * Add relation to a model methods 226 | * 227 | * @param string $relationType 228 | * @param string $tableName 229 | * @param string $foreignKeyName 230 | * @param string $localKeyName 231 | * @param string|null $joinTableName 232 | * 233 | * @return void 234 | * @throws Exception 235 | */ 236 | protected function addRelation( 237 | string $relationType, 238 | string $tableName, 239 | string $foreignKeyName, 240 | string $localKeyName, 241 | string $joinTableName = null 242 | ): void 243 | { 244 | $tableName = ucfirst(Str::singular($tableName)); 245 | $className = Str::ucfirst(Str::camel($tableName)); 246 | 247 | $this->addUse(config('repository-service-pattern.model.namespace') . "\\$className\\$className"); 248 | 249 | switch ($relationType) { 250 | case DbManagerService::RELATION_BELONGS_TO: 251 | $this 252 | ->addUse(BelongsTo::class) 253 | ->addDocBlockContent("@property $className $" . Str::camel($tableName)) 254 | ->addMethod( 255 | Str::camel($tableName), 256 | [], 257 | MethodModel::ACCESS_PUBLIC, 258 | null, 259 | 'BelongsTo', 260 | ["return " . '$' . "this->belongsTo($className::class, '$foreignKeyName', '$localKeyName');"] 261 | ); 262 | break; 263 | case DbManagerService::RELATION_BELONGS_TO_MANY: 264 | $this 265 | ->addUse(BelongsToMany::class) 266 | ->addUse(Collection::class) 267 | ->addDocBlockContent("@property " . $className . "[]|Collection $" . Str::camel(Str::plural($tableName))) 268 | ->addMethod( 269 | Str::camel(Str::plural($tableName)), 270 | [], 271 | MethodModel::ACCESS_PUBLIC, 272 | null, 273 | 'BelongsToMany', 274 | ["return " . '$' . "this->belongsToMany($className::class, '$joinTableName', '$foreignKeyName', '$localKeyName');"] 275 | ); 276 | break; 277 | case DbManagerService::RELATION_HAS_MANY: 278 | $this 279 | ->addUse(HasMany::class) 280 | ->addUse(Collection::class) 281 | ->addDocBlockContent("@property " . $className . "[]|Collection $" . Str::camel(Str::plural($tableName))) 282 | ->addMethod( 283 | Str::camel(Str::plural($tableName)), 284 | [], 285 | MethodModel::ACCESS_PUBLIC, 286 | null, 287 | 'HasMany', 288 | ["return " . '$' . "this->hasMany($className::class, '$foreignKeyName', '$localKeyName');"] 289 | ); 290 | break; 291 | case DbManagerService::RELATION_HAS_ONE: 292 | $this 293 | ->addUse(HasOne::class) 294 | ->addDocBlockContent("@property $className $" . Str::camel($tableName)) 295 | ->addMethod( 296 | Str::camel($tableName), 297 | [], 298 | MethodModel::ACCESS_PUBLIC, 299 | null, 300 | 'HasOne', 301 | ["return " . '$' . "this->hasOne($className::class, '$foreignKeyName', '$localKeyName');"] 302 | ); 303 | break; 304 | default: 305 | throw new Exception('Invalid relation type'); 306 | } 307 | } 308 | 309 | /** 310 | * Add docBlock content 311 | * 312 | * @param string $content 313 | * 314 | * @return self 315 | */ 316 | protected function addDocBlockContent(string $content): self 317 | { 318 | $this->docBlockContent[] = $content; 319 | 320 | return $this; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /tests/Unit/BaseCrudServiceTest.php: -------------------------------------------------------------------------------- 1 | initializeService(); 37 | } 38 | 39 | /** 40 | * Test getAllPaginated() 41 | * 42 | * @return void 43 | * @throws ContainerExceptionInterface 44 | * @throws NotFoundExceptionInterface 45 | */ 46 | public function testGetAllPaginated(): void 47 | { 48 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 49 | 50 | $data = $this->service->getAllPaginated(); 51 | 52 | $this->assertNotEmpty($data->items()); 53 | $this->assertEquals(1, $data->total()); 54 | $this->assertInstanceOf(LengthAwarePaginator::class, $data); 55 | } 56 | 57 | /** 58 | * Test getAll() 59 | * 60 | * @return void 61 | */ 62 | public function testGetAll(): void 63 | { 64 | $this->createModel([ 65 | 'prop_one' => 'one', 66 | 'prop_two' => 2, 67 | 'related_id' => DB::table('relations')->insertGetId(['property' => 'test']) 68 | ]); 69 | 70 | $data = $this->service 71 | ->with(['related']) 72 | ->withCount(['related']) 73 | ->getAll(); 74 | 75 | $this->assertNotEmpty($data); 76 | $this->assertCount(1, $data); 77 | $this->assertInstanceOf(Collection::class, $data); 78 | $this->assertTrue($data->first()->relationLoaded('related')); 79 | $this->assertEquals(1, $data->first()->getAttribute('related_count')); 80 | } 81 | 82 | /** 83 | * Test getAll() with softDeletes 84 | * 85 | * @return void 86 | */ 87 | public function testGetAllWithSoftDeletes(): void 88 | { 89 | $trashed = $this->createModel([ 90 | 'prop_one' => 'one', 91 | 'prop_two' => 2, 92 | 'deleted_at' => now() 93 | ]); 94 | 95 | $notTrashed = $this->createModel([ 96 | 'prop_one' => 'two', 97 | 'prop_two' => 2, 98 | 'deleted_at' => null 99 | ]); 100 | 101 | // By default, soft deleted records are excluded from query 102 | $data = $this->service->getAll(); 103 | $this->assertCount(1, $data); 104 | $this->assertNotNull( 105 | $data 106 | ->where('prop_one', $notTrashed->getAttribute('prop_one')) 107 | ->where('prop_two', $notTrashed->getAttribute('prop_two')) 108 | ->first() 109 | ); 110 | 111 | $data = $this->service->withoutTrashed()->getAll(); 112 | $this->assertCount(1, $data); 113 | $this->assertNotNull( 114 | $data 115 | ->where('prop_one', $notTrashed->getAttribute('prop_one')) 116 | ->where('prop_two', $notTrashed->getAttribute('prop_two')) 117 | ->first() 118 | ); 119 | 120 | // Included soft deleted 121 | $this->assertCount(2, $this->service->withTrashed()->getAll()); 122 | 123 | $data = $this->service->onlyTrashed()->getAll(); 124 | $this->assertCount(1, $data); 125 | $this->assertNotNull( 126 | $data 127 | ->where('prop_one', $trashed->getAttribute('prop_one')) 128 | ->where('prop_two', $trashed->getAttribute('prop_two')) 129 | ->first() 130 | ); 131 | } 132 | 133 | /** 134 | * Test getAllAsCursor() 135 | * 136 | * @return void 137 | */ 138 | public function testGetAllAsCursor(): void 139 | { 140 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 141 | 142 | $data = $this->service->getAllAsCursor(); 143 | 144 | $this->assertNotEmpty($data); 145 | $this->assertCount(1, $data); 146 | $this->assertInstanceOf(LazyCollection::class, $data); 147 | } 148 | 149 | /** 150 | * Test count() 151 | * 152 | * @return void 153 | * @throws RepositoryException 154 | */ 155 | public function testCount(): void 156 | { 157 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 158 | 159 | $this->assertEquals(1, $this->service->count(['prop_one' => 'one'])); 160 | $this->assertEquals(0, $this->service->count(['prop_one' => 'two'])); 161 | } 162 | 163 | /** 164 | * Test findOrFail() 165 | * 166 | * @return void 167 | */ 168 | public function testFindOrFail(): void 169 | { 170 | $model = $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 171 | 172 | $this->service->findOrFail($model->getKey()); 173 | $this->service->findOrFail(2, 'prop_two'); 174 | 175 | $this->expectException(ModelNotFoundException::class); 176 | $this->service->findOrFail(3, 'prop_two'); 177 | } 178 | 179 | /** 180 | * Test find() 181 | * 182 | * @return void 183 | */ 184 | public function testFind(): void 185 | { 186 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 187 | 188 | $this->assertNotEmpty($this->service->find([['prop_one', 'one']])); 189 | $this->assertNotEmpty($this->service->find([['prop_one', 'one'], ['prop_two', 2]])); 190 | $this->assertEmpty($this->service->find([['prop_one', 'two']])); 191 | } 192 | 193 | /** 194 | * Test create() 195 | * 196 | * @return void 197 | * @throws ServiceException 198 | */ 199 | public function testCreate(): void 200 | { 201 | $this->service->create(['prop_one' => 'one', 'prop_two' => 3]); 202 | 203 | $this->assertDatabaseHas( 204 | 'tests', [ 205 | 'prop_one' => 'one', 206 | 'prop_two' => 3 207 | ] 208 | ); 209 | } 210 | 211 | /** 212 | * Test insert() 213 | * 214 | * @return void 215 | */ 216 | public function testInsert(): void 217 | { 218 | $result = $this->service->insert([ 219 | ['prop_one' => 'val', 'prop_two' => 1], 220 | ['prop_one' => 'val1', 'prop_two' => 2], 221 | ]); 222 | 223 | $this->assertTrue($result); 224 | $this->assertDatabaseHas('tests', [ 225 | 'prop_one' => 'val', 226 | 'prop_two' => 1 227 | ]); 228 | $this->assertDatabaseHas('tests', [ 229 | 'prop_one' => 'val1', 230 | 'prop_two' => 2 231 | ]); 232 | } 233 | 234 | /** 235 | * Test createMany() 236 | * 237 | * @return void 238 | * @throws ServiceException 239 | */ 240 | public function testCreateMany(): void 241 | { 242 | $models = $this->service->createMany([ 243 | ['prop_one' => 'val', 'prop_two' => 1], 244 | ['prop_one' => 'val1', 'prop_two' => 2] 245 | ]); 246 | 247 | $this->assertCount(2, $models); 248 | $this->assertDatabaseHas('tests', [ 249 | 'prop_one' => 'val', 250 | 'prop_two' => 1 251 | ]); 252 | $this->assertDatabaseHas('tests', [ 253 | 'prop_one' => 'val1', 254 | 'prop_two' => 2 255 | ]); 256 | } 257 | 258 | /** 259 | * Test updateOrCreate() 260 | * 261 | * @return void 262 | * @throws ServiceException 263 | */ 264 | public function testUpdateOrCreate(): void 265 | { 266 | $model = $this->service->updateOrCreate( 267 | ['prop_one' => 'val'], 268 | ['prop_one' => 'val', 'prop_two' => 2] 269 | ); 270 | 271 | $this->assertDatabaseHas( 272 | 'tests', 273 | ['prop_one' => 'val', 'prop_two' => 2] 274 | ); 275 | 276 | $this->assertTrue($model->wasRecentlyCreated); 277 | 278 | $model = $this->service->updateOrCreate( 279 | ['prop_one' => 'val'], 280 | ['prop_one' => 'val', 'prop_two' => 5] 281 | ); 282 | 283 | $this->assertFalse($model->wasRecentlyCreated); 284 | $this->assertEquals(5, $model->getAttribute('prop_two')); 285 | } 286 | 287 | /** 288 | * Test update() 289 | * 290 | * @return void 291 | */ 292 | public function testUpdate(): void 293 | { 294 | $model = $this->createModel(['prop_one' => 'val', 'prop_two' => 5]); 295 | $model = $this->service->update($model->getKey(), ['prop_one' => 'test', 'prop_two' => 15]); 296 | 297 | $this->assertEquals('test', $model->getAttribute('prop_one')); 298 | $this->assertEquals(15, $model->getAttribute('prop_two')); 299 | } 300 | 301 | /** 302 | * Test delete() 303 | * 304 | * @return void 305 | * @throws Exception 306 | */ 307 | public function testDelete(): void 308 | { 309 | $model = $this->createModel(['prop_one' => 'test', 'prop_two' => 15]); 310 | 311 | $this->service->delete($model->getKey()); 312 | 313 | $this->assertDatabaseMissing( 314 | 'tests', 315 | ['prop_one' => 'test', 'prop_two' => 15] 316 | ); 317 | } 318 | 319 | /** 320 | * Test deleteMany() 321 | * 322 | * @return void 323 | * @throws Exception 324 | */ 325 | public function testDeleteMany(): void 326 | { 327 | $model = $this->createModel(['prop_one' => 'test', 'prop_two' => 15]); 328 | $modelTwo = $this->createModel(['prop_one' => 'val', 'prop_two' => 5]); 329 | 330 | $this->service->deleteMany([$model->getKey(), $modelTwo->getKey()]); 331 | 332 | $this->assertDatabaseMissing( 333 | 'tests', 334 | ['prop_one' => 'test', 'prop_two' => 15] 335 | ); 336 | 337 | $this->assertDatabaseMissing( 338 | 'tests', 339 | ['prop_one' => 'val', 'prop_two' => 5] 340 | ); 341 | } 342 | 343 | /** 344 | * Test softDelete() 345 | * 346 | * @return void 347 | */ 348 | public function testSoftDelete(): void 349 | { 350 | $model = $this->createModel(['prop_one' => 'test', 'prop_two' => 15]); 351 | 352 | $this->service->softDelete($model->getKey()); 353 | $model = $this->service->withTrashed()->find([['prop_one', 'test'], ['prop_two', 15]])->first(); 354 | $this->assertNotNull($model->deleted_at); 355 | 356 | $model = new class extends Model { 357 | }; 358 | 359 | // Model is not an entity of the repository model class 360 | $this->expectException(RepositoryException::class); 361 | $this->service->softDelete($model); 362 | } 363 | 364 | /** 365 | * Test restore() 366 | * 367 | * @return void 368 | * @throws RepositoryException 369 | */ 370 | public function testRestore(): void 371 | { 372 | $model = $this->createModel(['prop_one' => 'test', 'prop_two' => 15, 'deleted_at' => now()]); 373 | 374 | $this->service->restore($model->getKey()); 375 | 376 | $model = $this->service->findOrFail($model->getKey()); 377 | $this->assertNull($model->getAttribute('deleted_at')); 378 | } 379 | 380 | /** 381 | * Initialize service for testing 382 | * 383 | * @return void 384 | */ 385 | protected function initializeService(): void 386 | { 387 | $repository = $this->repository; 388 | 389 | $this->app->bind(get_class($repository), function () use ($repository) { 390 | return $repository; 391 | }); 392 | 393 | $this->service = new class($repository) extends BaseCrudService { 394 | private $tmpRepository; 395 | 396 | public function __construct(BaseRepositoryInterface $repository) 397 | { 398 | $this->tmpRepository = $repository; 399 | parent::__construct(); 400 | unset($this->tmpRepository); 401 | } 402 | 403 | protected function getRepositoryClass(): string 404 | { 405 | return get_class($this->tmpRepository); 406 | } 407 | }; 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /tests/Unit/BaseCacheableRepositoryTest.php: -------------------------------------------------------------------------------- 1 | createModel(['prop_one' => 'one', 'prop_two' => 2]); 29 | $collection = $this->repository->getAll($searchParams); 30 | 31 | $cacheKey = $this->repository->generateCacheKey( 32 | BaseCacheableRepository::KEY_ALL, 33 | array_merge( 34 | $searchParams, 35 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => null] 36 | ) 37 | ); 38 | $this->assertEquals($collection, $this->getCachedValue($cacheKey)); 39 | 40 | $searchParams = ['prop_one' => 'test']; 41 | 42 | $collection = $this->repository->getAll($searchParams); 43 | $anotherCacheKey = $this->repository->generateCacheKey( 44 | BaseCacheableRepository::KEY_ALL, 45 | array_merge( 46 | $searchParams, 47 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => null] 48 | ) 49 | ); 50 | 51 | $this->assertEquals($collection, $this->getCachedValue($anotherCacheKey)); 52 | $this->assertNotEquals($cacheKey, $anotherCacheKey); 53 | } 54 | 55 | /** 56 | * Testing getAllPaginated() 57 | * 58 | * @return void 59 | * @throws RepositoryException 60 | */ 61 | public function testGetAllPaginated(): void 62 | { 63 | $searchParams = []; 64 | 65 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 66 | $collection = $this->repository->getAllPaginated($searchParams); 67 | 68 | $cacheKey = $this->repository->generateCacheKey( 69 | BaseCacheableRepository::KEY_PAGINATED, 70 | array_merge( 71 | $searchParams, 72 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => null, 'pageSize' => 15, 'page' => 1] 73 | ) 74 | ); 75 | 76 | $this->assertEquals($collection, $this->getCachedValue($cacheKey)); 77 | 78 | $searchParams = ['prop_one' => 'test']; 79 | 80 | $collection = $this->repository->getAllPaginated($searchParams); 81 | $anotherCacheKey = $this->repository->generateCacheKey( 82 | BaseCacheableRepository::KEY_PAGINATED, 83 | array_merge( 84 | $searchParams, 85 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => null, 'pageSize' => 15, 'page' => 1] 86 | ) 87 | ); 88 | 89 | $this->assertEquals($collection, $this->getCachedValue($anotherCacheKey)); 90 | $this->assertNotEquals($cacheKey, $anotherCacheKey); 91 | } 92 | 93 | /** 94 | * Testing findFirst() 95 | * 96 | * @return void 97 | * @throws RepositoryException 98 | */ 99 | public function testFindFirst(): void 100 | { 101 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 102 | 103 | $attributes = ['prop_one' => 'one']; 104 | $model = $this->repository->findFirst($attributes); 105 | 106 | $cacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 107 | 108 | $this->assertEquals($model, $this->getCachedValue($cacheKey)); 109 | } 110 | 111 | /** 112 | * Testing find() 113 | * 114 | * @return void 115 | */ 116 | public function testFind(): void 117 | { 118 | $model = $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 119 | 120 | $data = $this->repository->find($model->getKey()); 121 | 122 | $cacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 123 | 124 | $this->assertEquals($data, $this->getCachedValue($cacheKey)); 125 | } 126 | 127 | /** 128 | * Testing findOrFail() 129 | * 130 | * @return void 131 | * @throws RepositoryException 132 | */ 133 | public function testFindOrFail(): void 134 | { 135 | $model = $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 136 | 137 | $data = $this->repository->findOrFail($model->getKey()); 138 | 139 | $cacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 140 | 141 | $this->assertEquals($data, $this->getCachedValue($cacheKey)); 142 | 143 | $this->expectException(ModelNotFoundException::class); 144 | $this->repository->findOrFail(123); 145 | } 146 | 147 | /** 148 | * Testing insert() 149 | * 150 | * @return void 151 | */ 152 | public function testInsert(): void 153 | { 154 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 155 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 156 | 157 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 158 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 159 | 160 | $this->repository->insert([ 161 | ['prop_one' => 'one', 'prop_two' => 2], 162 | ['prop_one' => 'two', 'prop_two' => 2], 163 | ]); 164 | 165 | $this->assertDatabaseHas('tests', [ 166 | 'prop_one' => 'one', 167 | 'prop_two' => 2 168 | ]); 169 | $this->assertDatabaseHas('tests', [ 170 | 'prop_one' => 'two', 171 | 'prop_two' => 2 172 | ]); 173 | 174 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 175 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 176 | } 177 | 178 | /** 179 | * Testing create() 180 | * 181 | * @return void 182 | */ 183 | public function testCreate(): void 184 | { 185 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 186 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 187 | 188 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 189 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 190 | 191 | $model = $this->repository->create(['prop_one' => 'one', 'prop_two' => 2]); 192 | 193 | $this->assertModelExists($model); 194 | 195 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 196 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 197 | } 198 | 199 | /** 200 | * Testing update() 201 | * 202 | * @return void 203 | * @throws RepositoryException 204 | */ 205 | public function testUpdate(): void 206 | { 207 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 208 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 209 | 210 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 211 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 212 | 213 | $model = $this->createModel(['prop_one' => 'one', 'prop_two' => 2]); 214 | $model = $this->repository->update($model, ['prop_one' => 'two', 'prop_two' => 3]); 215 | 216 | $this->assertEquals('two', $model->getAttribute('prop_one')); 217 | $this->assertEquals(3, $model->getAttribute('prop_two')); 218 | 219 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 220 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 221 | } 222 | 223 | /** 224 | * Testing updateOrCreate() 225 | * 226 | * @return void 227 | * @throws RepositoryException 228 | */ 229 | public function testUpdateOrCreate(): void 230 | { 231 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 232 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 233 | 234 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 235 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 236 | 237 | $model = $this->repository->updateOrCreate( 238 | ['prop_one' => 'val'], 239 | ['prop_one' => 'val', 'prop_two' => 2] 240 | ); 241 | 242 | $this->assertModelExists($model); 243 | 244 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 245 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 246 | } 247 | 248 | /** 249 | * Testing delete() 250 | * 251 | * @return void 252 | * @throws Exception 253 | */ 254 | public function testDelete(): void 255 | { 256 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 257 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 258 | 259 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 260 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 261 | 262 | $model = $this->createModel(['prop_one' => 'val', 'prop_two' => 2]); 263 | 264 | $modelCacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 265 | Cache::tags($modelCacheKey['tags'])->put($modelCacheKey['paramsKey'], 'test'); 266 | 267 | $this->repository->delete($model); 268 | $this->assertModelMissing($model); 269 | 270 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 271 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 272 | $this->assertNull($this->getCachedValue($modelCacheKey)); 273 | } 274 | 275 | /** 276 | * Testing softDelete() 277 | * 278 | * @return void 279 | * @throws Exception 280 | */ 281 | public function testSoftDelete(): void 282 | { 283 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 284 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 285 | 286 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 287 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 288 | 289 | $model = $this->createModel(['prop_one' => 'val', 'prop_two' => 2]); 290 | 291 | $modelCacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 292 | Cache::tags($modelCacheKey['tags'])->put($modelCacheKey['paramsKey'], 'test'); 293 | 294 | $this->repository->softDelete($model); 295 | $this->assertSoftDeleted($model); 296 | 297 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 298 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 299 | $this->assertNull($this->getCachedValue($modelCacheKey)); 300 | } 301 | 302 | /** 303 | * Testing restore() 304 | * 305 | * @return void 306 | * @throws Exception 307 | */ 308 | public function testRestore(): void 309 | { 310 | $cacheKeyForPaginated = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_PAGINATED, []); 311 | $cacheKeyForAll = $this->repository->generateCacheKey(BaseCacheableRepository::KEY_ALL, []); 312 | 313 | Cache::tags($cacheKeyForPaginated['tags'])->put('', 'test'); 314 | Cache::tags($cacheKeyForAll['tags'])->put('', 'test'); 315 | 316 | $model = $this->createModel(['prop_one' => 'val', 'prop_two' => 2, 'deleted_at' => now()]); 317 | 318 | $modelCacheKey = $this->repository->generateCacheKeyForModelInstance($model->getKey()); 319 | Cache::tags($modelCacheKey['tags'])->put($modelCacheKey['paramsKey'], 'test'); 320 | 321 | $this->repository->restore($model); 322 | $this->assertNotSoftDeleted($model); 323 | 324 | $this->assertNull($this->getCachedValue($cacheKeyForPaginated)); 325 | $this->assertNull($this->getCachedValue($cacheKeyForAll)); 326 | $this->assertEquals($model->refresh(), $this->getCachedValue($modelCacheKey)); 327 | } 328 | 329 | /** 330 | * Test soft deletes query cache 331 | * 332 | * @return void 333 | */ 334 | public function testSoftDeletesQuery(): void 335 | { 336 | $this->createModel(['prop_one' => 'one', 'prop_two' => 2, 'deleted_at' => now()]); 337 | $collection = $this->repository->onlyTrashed()->getAll(); 338 | 339 | $onlyTrashedCacheKey = $this->repository->generateCacheKey( 340 | BaseCacheableRepository::KEY_ALL, 341 | array_merge( 342 | [], 343 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => 3] 344 | ) 345 | ); 346 | 347 | $this->assertEquals($collection, $this->getCachedValue($onlyTrashedCacheKey)); 348 | 349 | $collection = $this->repository->withoutTrashed()->getAll(); 350 | 351 | $withoutTrashedCacheKey = $this->repository->generateCacheKey( 352 | BaseCacheableRepository::KEY_ALL, 353 | array_merge( 354 | [], 355 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => 1] 356 | ) 357 | ); 358 | 359 | $this->assertEquals($collection, $this->getCachedValue($withoutTrashedCacheKey)); 360 | $this->assertNotEquals($this->getCachedValue($withoutTrashedCacheKey), $this->getCachedValue($onlyTrashedCacheKey)); 361 | 362 | $collection = $this->repository->withTrashed()->getAll(); 363 | 364 | $withTrashedCacheKey = $this->repository->generateCacheKey( 365 | BaseCacheableRepository::KEY_ALL, 366 | array_merge( 367 | [], 368 | ['with' => [], 'withCount' => [], 'softDeleteQueryMode' => 2] 369 | ) 370 | ); 371 | 372 | $this->assertEquals($collection, $this->getCachedValue($withTrashedCacheKey)); 373 | } 374 | 375 | /** 376 | * @param array $keyParams 377 | * @return mixed 378 | */ 379 | protected function getCachedValue(array $keyParams) 380 | { 381 | return Cache::tags($keyParams['tags'])->get($keyParams['paramsKey']); 382 | } 383 | 384 | /** 385 | * @return void 386 | */ 387 | protected function initializeRepository(): void 388 | { 389 | $model = $this->model; 390 | 391 | $this->repository = new class($model) extends BaseCacheableRepository { 392 | public $cacheAlias = 'test'; 393 | protected $modelClass; 394 | 395 | public function __construct(Model $model) 396 | { 397 | $this->modelClass = get_class($model); 398 | parent::__construct(); 399 | } 400 | 401 | public function generateCacheKey(string $keyName, array $params = []): array 402 | { 403 | return parent::generateCacheKey($keyName, $params); 404 | } 405 | 406 | /** 407 | * Generate cache key for single model 408 | * 409 | * @param $primaryKey 410 | * @return array 411 | */ 412 | public function generateCacheKeyForModelInstance($primaryKey): array 413 | { 414 | return parent::generateCacheKeyForModelInstance($primaryKey); 415 | } 416 | 417 | protected function getModelClass(): string 418 | { 419 | return $this->modelClass; 420 | } 421 | }; 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/Traits/Queryable.php: -------------------------------------------------------------------------------- 1 | getQuery()->whereKey($key)->first(); 82 | } 83 | 84 | /** 85 | * Find or fail by primary key or custom column 86 | * 87 | * @param $value 88 | * @param string|null $column 89 | * @return Model 90 | * @throws RepositoryException 91 | */ 92 | public function findOrFail($value, ?string $column = null): Model 93 | { 94 | if (is_null($column)) { 95 | return is_array($value) 96 | ? $this->findMany([$value])->firstOrFail() 97 | : $this->getQuery()->findOrFail($value); 98 | } 99 | 100 | return $this->getQuery()->where($column, $value)->firstOrFail(); 101 | } 102 | 103 | /** 104 | * Find all models by params 105 | * 106 | * @param array $attributes 107 | * @return Collection 108 | * @throws RepositoryException 109 | */ 110 | public function findMany(array $attributes): Collection 111 | { 112 | return $this->applyFilterConditions($attributes)->get(); 113 | } 114 | 115 | /** 116 | * Find first model 117 | * 118 | * @param array $attributes 119 | * @return Model|null 120 | * @throws RepositoryException 121 | */ 122 | public function findFirst(array $attributes): ?Model 123 | { 124 | return $this->findMany($attributes)->first(); 125 | } 126 | 127 | /** 128 | * Get filtered collection 129 | * 130 | * @param array $search 131 | * @return Collection 132 | * @throws RepositoryException 133 | */ 134 | public function getAll(array $search = []): Collection 135 | { 136 | return $this->applyFilters($search)->get(); 137 | } 138 | 139 | /** 140 | * Get filtered collection as cursor output 141 | * 142 | * @param array $search 143 | * @return LazyCollection 144 | * @throws RepositoryException 145 | */ 146 | public function getAllCursor(array $search = []): LazyCollection 147 | { 148 | return $this->applyFilters($search)->cursor(); 149 | } 150 | 151 | /** 152 | * Get results count 153 | * 154 | * @throws RepositoryException 155 | */ 156 | public function count(array $search = []): int 157 | { 158 | return $this->applyFilters($search)->count(); 159 | } 160 | 161 | /** 162 | * Get paginated data 163 | * 164 | * @param array $search 165 | * @param int $pageSize 166 | * @return LengthAwarePaginator 167 | * @throws RepositoryException 168 | */ 169 | public function getAllPaginated(array $search = [], int $pageSize = 15): LengthAwarePaginator 170 | { 171 | return $this->applyFilters($search)->paginate( 172 | $pageSize, 173 | ['*'], 174 | 'page', 175 | $search['page'] ?? (request()?->input('page', 1) ?? 1) 176 | ); 177 | } 178 | 179 | /** 180 | * Set with 181 | * 182 | * @param array $with 183 | * @return BaseRepositoryInterface 184 | */ 185 | public function with(array $with): BaseRepositoryInterface 186 | { 187 | $this->with = $with; 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * Set global scopes to include 194 | * 195 | * @param array $withoutGlobalScopes 196 | * @return BaseRepositoryInterface 197 | */ 198 | public function withoutGlobalScopes(array $withoutGlobalScopes): BaseRepositoryInterface 199 | { 200 | $this->withoutGlobalScopes = $withoutGlobalScopes; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Set with count 207 | * 208 | * @param array $withCount 209 | * @return BaseRepositoryInterface 210 | */ 211 | public function withCount(array $withCount): BaseRepositoryInterface 212 | { 213 | $this->withCount = $withCount; 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Include soft deleted records to a query 220 | * 221 | * @return BaseRepositoryInterface 222 | */ 223 | public function withTrashed(): BaseRepositoryInterface 224 | { 225 | $this->softDeleteQueryMode = self::$INCLUDE_DELETED; 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * Show only soft deleted records in a query 232 | * 233 | * @return BaseRepositoryInterface 234 | */ 235 | public function onlyTrashed(): BaseRepositoryInterface 236 | { 237 | $this->softDeleteQueryMode = self::$ONLY_DELETED; 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * Exclude soft deleted records from a query 244 | * 245 | * @return BaseRepositoryInterface 246 | */ 247 | public function withoutTrashed(): BaseRepositoryInterface 248 | { 249 | $this->softDeleteQueryMode = self::$EXCLUDE_DELETED; 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * The purpose of this function is to apply filtering to the model by overriding this function inside child repository 256 | * 257 | * @param array $searchParams 258 | * @return Builder 259 | * @throws RepositoryException 260 | */ 261 | protected function applyFilters(array $searchParams = []): Builder 262 | { 263 | return $this 264 | ->applyFilterConditions($searchParams) 265 | ->when( 266 | !is_array(($orderByField = $this->defaultOrderBy ?? app($this->getModelClass())->getKeyName())), 267 | function (Builder $query) use ($orderByField) { 268 | $query->orderBy($orderByField, 'desc'); 269 | }); 270 | } 271 | 272 | /** 273 | * Apply filter conditions 274 | * 275 | * @param array $conditions 276 | * @return Builder 277 | * @throws RepositoryException 278 | */ 279 | protected function applyFilterConditions(array $conditions): Builder 280 | { 281 | $query = $this->getQuery(); 282 | 283 | if (empty($conditions)) { 284 | return $query; 285 | } 286 | 287 | $conditions = $this->parseConditions($conditions); 288 | 289 | if (!empty($conditions)) { 290 | $tableName = $query->getModel()->getTable(); 291 | $tableColumns = $this->getTableColumns($tableName); 292 | } 293 | 294 | foreach ($conditions as $data) { 295 | 296 | list ($field, $operator, $val) = $data; 297 | 298 | $relationConditions = ['HAS', 'DOESNT_HAVE', 'HAS_MORPH', 'DOESNT_HAVE_MORPH']; 299 | 300 | if (in_array(strtoupper($operator), $relationConditions)) { 301 | $this->validateClosureFunction($val); 302 | 303 | switch (strtoupper($operator)) { 304 | case 'HAS': 305 | $query->whereHas($field, $val); 306 | break; 307 | case 'DOESNT_HAVE': 308 | $query->whereDoesntHave($field, $val); 309 | break; 310 | case 'HAS_MORPH': 311 | $query->whereHasMorph($field, $val); 312 | break; 313 | case 'DOESNT_HAVE_MORPH': 314 | $query->whereDoesntHaveMorph($field, $val); 315 | break; 316 | } 317 | 318 | continue; 319 | } 320 | 321 | if (!in_array($field, $tableColumns)) { 322 | continue; 323 | } 324 | 325 | $operator = preg_replace('/\s\s+/', ' ', trim($operator)); 326 | 327 | $exploded = explode(' ', $operator); 328 | $condition = trim($exploded[0]); 329 | $operator = trim($exploded[1] ?? '='); 330 | 331 | switch (strtoupper($condition)) { 332 | case 'NULL': 333 | $query->whereNull($tableName . '.' . $field); 334 | break; 335 | case 'NOT_NULL': 336 | $query->whereNotNull($tableName . '.' . $field); 337 | break; 338 | case 'IN': 339 | $this->validateArrayData($val); 340 | $query->whereIn($tableName . '.' . $field, $val); 341 | break; 342 | case 'NOT_IN': 343 | $this->validateArrayData($val); 344 | $query->whereNotIn($tableName . '.' . $field, $val); 345 | break; 346 | case 'DATE': 347 | $query->whereDate($tableName . '.' . $field, $operator, $val); 348 | break; 349 | case 'DAY': 350 | $query->whereDay($tableName . '.' . $field, $operator, $val); 351 | break; 352 | case 'MONTH': 353 | $query->whereMonth($tableName . '.' . $field, $operator, $val); 354 | break; 355 | case 'YEAR': 356 | $query->whereYear($tableName . '.' . $field, $operator, $val); 357 | break; 358 | case 'BETWEEN': 359 | $this->validateArrayData($val); 360 | $query->whereBetween($tableName . '.' . $field, $val); 361 | break; 362 | case 'BETWEEN_COLUMNS': 363 | $this->validateArrayData($val); 364 | $query->whereBetweenColumns($tableName . '.' . $field, $val); 365 | break; 366 | case 'NOT_BETWEEN': 367 | $this->validateArrayData($val); 368 | $query->whereNotBetween($tableName . '.' . $field, $val); 369 | break; 370 | case 'NOT_BETWEEN_COLUMNS': 371 | $this->validateArrayData($val); 372 | $query->whereNotBetweenColumns($tableName . '.' . $field, $val); 373 | break; 374 | default: 375 | $query->where($tableName . '.' . $field, $condition, $val); 376 | } 377 | } 378 | 379 | return $query; 380 | } 381 | 382 | /** 383 | * @return Builder 384 | */ 385 | protected function getQuery(): Builder 386 | { 387 | /** @var Model $model */ 388 | $model = app($this->getModelClass()); 389 | 390 | $query = $this->isInstanceOfSoftDeletes($model) 391 | ? $model->newQueryWithoutScope(SoftDeletingScope::class) 392 | : $model->query(); 393 | 394 | return $query 395 | ->when(!empty($this->withoutGlobalScopes), function (Builder $query) { 396 | $query->withoutGlobalScopes($this->withoutGlobalScopes); 397 | }) 398 | ->when(!empty($this->with), function (Builder $query) { 399 | $query->with($this->with); 400 | }) 401 | ->when(!empty($this->withCount), function (Builder $query) { 402 | $query->withCount($this->withCount); 403 | }) 404 | ->when( 405 | $this->isInstanceOfSoftDeletes($model) || $this->isModelHasSoftDeleteColumn($model), 406 | function (Builder $query) { 407 | $mode = $this->softDeleteQueryMode ?? self::$EXCLUDE_DELETED; 408 | 409 | $query 410 | ->when($mode === self::$EXCLUDE_DELETED, function (Builder $query) { 411 | $query->whereNull($this->deletedAtColumnName); 412 | }) 413 | ->when($mode === self::$INCLUDE_DELETED, function (Builder $query) { 414 | $query->where(function (Builder $query) { 415 | $query 416 | ->whereNull($this->deletedAtColumnName) 417 | ->orWhereNotNull($this->deletedAtColumnName); 418 | }); 419 | }) 420 | ->when($mode === self::$ONLY_DELETED, function (Builder $query) { 421 | $query->whereNotNull($this->deletedAtColumnName); 422 | }); 423 | }); 424 | } 425 | 426 | /** 427 | * Get condition data for search 428 | * 429 | * @param array $conditions 430 | * @return array 431 | */ 432 | private function parseConditions(array $conditions): array 433 | { 434 | $processedConditions = []; 435 | 436 | if (isset($conditions[0]) && !is_array($conditions[0])) { 437 | $conditions = [$conditions]; 438 | } 439 | 440 | foreach ($conditions as $field => $condition) { 441 | // [field, operator, value] or [field, value] handler 442 | if (is_array($condition) && ($count = count($condition)) >= 2 && isset($condition[0])) { 443 | $processedConditions[] = [ 444 | $condition[0], 445 | $count === 2 ? (!in_array(strtoupper($condition[1]), [ 446 | 'NULL', 447 | 'NOT_NULL' 448 | ]) ? '=' : $condition[1]) : $condition[1], 449 | $condition[2] ?? $condition[1] 450 | ]; 451 | continue; 452 | } 453 | 454 | // 'key' => 'value' handler 455 | if (is_string($field) && !is_array($condition)) { 456 | $processedConditions[] = [$field, '=', $condition]; 457 | continue; 458 | } 459 | 460 | // ['key' => 'value'] handler 461 | if (is_numeric($field) && is_array($condition) && !isset($condition[0])) { 462 | $field = key($condition); 463 | 464 | if (!isset($condition[$field])) { 465 | continue; 466 | } 467 | 468 | $processedConditions[] = [$field, '=', $condition[$field]]; 469 | } 470 | } 471 | 472 | return $processedConditions; 473 | } 474 | 475 | /** 476 | * Validate array data 477 | * 478 | * @throws RepositoryException 479 | */ 480 | private function validateArrayData($data): void 481 | { 482 | if (!is_array($data)) { 483 | throw new RepositoryException("Invalid data provided, data must be an array."); 484 | } 485 | } 486 | 487 | /** 488 | * Validate closure data 489 | * 490 | * @throws RepositoryException 491 | */ 492 | private function validateClosureFunction($value): void 493 | { 494 | if (!$value instanceof Closure && !is_null($value)) { 495 | throw new RepositoryException("Invalid closure provided."); 496 | } 497 | } 498 | 499 | /** 500 | * Get the columns for the model's table. 501 | * 502 | * @param string $tableName 503 | * @return array 504 | */ 505 | protected function getTableColumns(string $tableName): array 506 | { 507 | static $schemaCache = []; 508 | 509 | if (!isset($schemaCache[$tableName])) { 510 | $schemaCache[$tableName] = Schema::getColumnListing($tableName); 511 | } 512 | 513 | return $schemaCache[$tableName]; 514 | } 515 | } 516 | --------------------------------------------------------------------------------