├── src ├── Exceptions │ └── RepositoryException.php ├── Commands │ └── stubs │ │ ├── scopes.stub │ │ ├── scope.stub │ │ └── repository.stub ├── Contracts │ ├── CriteriaInterface.php │ ├── CriterionInterface.php │ ├── ScopesInterface.php │ └── RepositoryInterface.php ├── Scopes │ ├── Clauses │ │ ├── WhereScope.php │ │ ├── OrWhereScope.php │ │ ├── WhereLikeScope.php │ │ ├── OrWhereLikeScope.php │ │ ├── WhereDateLessScope.php │ │ ├── WhereDateGreaterScope.php │ │ └── OrderByScope.php │ ├── ScopeAbstract.php │ ├── Scopes.php │ └── ScopesAbstract.php ├── Criteria │ └── FindWhere.php ├── Helper.php └── Eloquent │ ├── RepositoryAbstract.php │ └── BaseRepository.php ├── .github ├── FUNDING.yml └── workflows │ └── php.yml ├── tests ├── Scopes │ ├── NewsScope.php │ └── Clauses │ │ └── MyScope.php ├── Repository │ ├── NewsRepository.php │ └── NewsRepositoryScope.php ├── Models │ └── NewsModel.php ├── _support │ └── Database │ │ ├── Migrations │ │ └── 2020-09-18-124348_create_news_table.php │ │ └── Seeds │ │ └── NewsSeeder.php ├── Criteria │ └── SampleCriteria.php ├── NewsRepositoryRequestTest.php └── NewsRepositoryTest.php ├── LICENSE.md ├── composer.json ├── phpunit.xml.dist ├── .gitignore └── README.md /src/Exceptions/RepositoryException.php: -------------------------------------------------------------------------------- 1 | SearchScope::class, 11 | ]; 12 | } -------------------------------------------------------------------------------- /src/Commands/stubs/scope.stub: -------------------------------------------------------------------------------- 1 | where($scope, $value); 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Scopes/NewsScope.php: -------------------------------------------------------------------------------- 1 | MyScope::class 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Contracts/CriteriaInterface.php: -------------------------------------------------------------------------------- 1 | where($scope, $value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Contracts/ScopesInterface.php: -------------------------------------------------------------------------------- 1 | 'or', 12 | 'title' => 'like', 13 | 'description' => 'orLike', 14 | ]; 15 | 16 | public function entity() 17 | { 18 | return NewsModel::class; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/WhereScope.php: -------------------------------------------------------------------------------- 1 | where($scope, $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/OrWhereScope.php: -------------------------------------------------------------------------------- 1 | orWhere($scope, $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/WhereLikeScope.php: -------------------------------------------------------------------------------- 1 | like($scope, $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/OrWhereLikeScope.php: -------------------------------------------------------------------------------- 1 | orLike($scope, $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/WhereDateLessScope.php: -------------------------------------------------------------------------------- 1 | orWhere('created_at' . '<', $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/WhereDateGreaterScope.php: -------------------------------------------------------------------------------- 1 | orWhere('created_at' . '>', $value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Commands/stubs/repository.stub: -------------------------------------------------------------------------------- 1 | entity = (new DummyScope($request))->scope($this->entity); 27 | return $this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Models/NewsModel.php: -------------------------------------------------------------------------------- 1 | $faker->name, 26 | 'description' => $faker->text, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Scopes/ScopeAbstract.php: -------------------------------------------------------------------------------- 1 | mappings(), $key); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Repository/NewsRepositoryScope.php: -------------------------------------------------------------------------------- 1 | 'like', 14 | 'description' => 'orLike', 15 | ]; 16 | 17 | public function scope(IncomingRequest $request) 18 | { 19 | parent::scope($request); 20 | 21 | $this->entity = (new NewsScope($request))->scope($this->entity); 22 | 23 | return $this->entity; 24 | } 25 | 26 | public function entity() 27 | { 28 | return NewsModel::class; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | strategy: 15 | matrix: 16 | php-versions: [7.3', '7.4'] 17 | 18 | runs-on: ubuntu-latest 19 | 20 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | 25 | - name: setup PHP 26 | uses: shivammathur/setup-php@master 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | extension: intl, json, mbstring, xdebug, xml 30 | coverage: xdebug 31 | 32 | - name: Validate composer.json and composer.lock 33 | run: composer validate 34 | 35 | - name: Install dependencies 36 | run: composer install --prefer-source --no-progress --no-suggest 37 | 38 | - name: Run test suite 39 | run: composer test 40 | 41 | -------------------------------------------------------------------------------- /src/Scopes/Clauses/OrderByScope.php: -------------------------------------------------------------------------------- 1 | orderable ?? []; 23 | 24 | if ( 25 | array_pop($arr) == 'desc' 26 | && in_array($field = implode('_', $arr), $orderable) 27 | ) { 28 | return $builder->orderBy($field, 'desc'); 29 | } elseif (in_array($value, $orderable)) { 30 | return $builder->orderBy($value, 'asc'); 31 | } 32 | 33 | return $builder; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Criteria/FindWhere.php: -------------------------------------------------------------------------------- 1 | conditions = $conditions; 22 | } 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function apply(Model $entity) 28 | { 29 | foreach ($this->conditions as $field => $value) { 30 | if (is_array($value)) { 31 | list($field, $condition, $val) = $value; 32 | $entity = $entity->orWhere($field . $condition, $val); 33 | } else { 34 | $entity = $entity->where($field, $value); 35 | } 36 | } 37 | 38 | return $entity; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/_support/Database/Migrations/2020-09-18-124348_create_news_table.php: -------------------------------------------------------------------------------- 1 | forge->addField([ 12 | 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 13 | 'title' => ['type' => 'varchar', 'constraint' => 55, 'null' => true], 14 | 'description' => ['type' => 'varchar', 'constraint' => 255, 'null' => true], 15 | 'created_at' => ['type' => 'datetime', 'null' => true], 16 | 'updated_at' => ['type' => 'datetime', 'null' => true], 17 | ]); 18 | 19 | $this->forge->addPrimaryKey('id'); 20 | $this->forge->createTable('news', true); 21 | } 22 | 23 | //-------------------------------------------------------------------- 24 | 25 | public function down() 26 | { 27 | $this->forge->dropTable('news', true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Criteria/SampleCriteria.php: -------------------------------------------------------------------------------- 1 | conditions = $conditions; 22 | } 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function apply(Model $entity) 28 | { 29 | foreach ($this->conditions as $field => $value) { 30 | if (is_array($value)) { 31 | list($field, $condition, $val) = $value; 32 | $entity = $entity->orWhere($field . $condition, $val); 33 | } else { 34 | $entity = $entity->where($field, $value); 35 | } 36 | 37 | return $entity; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 Agung Sugiarto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/_support/Database/Seeds/NewsSeeder.php: -------------------------------------------------------------------------------- 1 | setOverrides($overrides); 24 | } 25 | 26 | return $fabricator->create($count); 27 | } 28 | } 29 | 30 | class NewsSeeder extends Seeder 31 | { 32 | public function run() 33 | { 34 | factory(NewsModel::class, 10); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agungsugiarto/codeigniter4-repository", 3 | "description": "Implementation of repository pattern for CodeIgniter 4. The package allows out-of-the-box filtering of data based on parameters in the request, and also allows you to quickly integrate the list filters and custom criteria.", 4 | "keywords": ["codeigniter4", "repository", "pattern", "repository-pattern", "filters", "criteria"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Agung Sugiarto", 9 | "email": "me.agungsugiarto@tgmail.com", 10 | "homepage": "https://agungsugiarto.github.io", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.3 || 8.0", 16 | "codeigniter4/framework": "^4.1" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Fluent\\Repository\\": "src" 21 | } 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.1", 25 | "fakerphp/faker": "^1.13" 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Fluent\\Repository\\Tests\\": "tests/" 30 | } 31 | }, 32 | "scripts": { 33 | "test": "phpunit --testdox --colors=always" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Helper.php: -------------------------------------------------------------------------------- 1 | OrderByScope::class, 19 | 'begin' => WhereDateGreaterScope::class, 20 | 'end' => WhereDateLessScope::class, 21 | ]; 22 | 23 | /** 24 | * Constructor scopes. 25 | * 26 | * @param \CodeIgniter\HTTP\IncomingRequest $request 27 | * @param array $searchable 28 | * @return void 29 | */ 30 | public function __construct(IncomingRequest $request, $searchable) 31 | { 32 | parent::__construct($request); 33 | 34 | foreach ($searchable as $key => $value) { 35 | if (is_string($key)) { 36 | $this->scopes[$key] = $this->mappings($value); 37 | } else { 38 | $this->scopes[$value] = WhereScope::class; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Mapping by scope. 45 | * 46 | * @param string $key 47 | * @return string 48 | */ 49 | protected function mappings(string $key) 50 | { 51 | $mappings = [ 52 | 'or' => OrWhereScope::class, 53 | 'like' => WhereLikeScope::class, 54 | 'orLike' => OrWhereLikeScope::class, 55 | ]; 56 | 57 | return $mappings[$key] ?? WhereScope::class; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Scopes/ScopesAbstract.php: -------------------------------------------------------------------------------- 1 | request = $request; 23 | } 24 | 25 | /** 26 | * In your repository define which fields can be used to scope your queries. 27 | * 28 | * @param \CodeIgniter\Database\BaseBuilder|\CodeIgniter\Model $builder 29 | * @return \CodeIgniter\Database\BaseBuilder $builder 30 | */ 31 | public function scope($builder) 32 | { 33 | $scopes = $this->getScopes(); 34 | 35 | foreach ($scopes as $scope => $value) { 36 | $builder = $this->resolveScope($scope)->scope($builder, $value, $scope); 37 | } 38 | 39 | return $builder; 40 | } 41 | 42 | /** 43 | * Resolve scope mapping. 44 | * 45 | * @param string $scope 46 | * @return object 47 | */ 48 | protected function resolveScope(string $scope) 49 | { 50 | return new $this->scopes[$scope](); 51 | } 52 | 53 | /** 54 | * Get scope mapping by request. 55 | * 56 | * @return object 57 | */ 58 | protected function getScopes() 59 | { 60 | return $this->filterScopes( 61 | $this->request->getGet(array_keys($this->scopes)) 62 | ); 63 | } 64 | 65 | /** 66 | * Filter scopes. 67 | * 68 | * @param array $scopes 69 | * @return array 70 | */ 71 | protected function filterScopes(array $scopes) 72 | { 73 | return array_filter($scopes, function ($scope) { 74 | return isset($scope); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | ./src 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #------------------------- 2 | # Operating Specific Junk Files 3 | #------------------------- 4 | 5 | # OS X 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # OS X Thumbnails 11 | ._* 12 | 13 | # Windows image file caches 14 | Thumbs.db 15 | ehthumbs.db 16 | Desktop.ini 17 | 18 | # Recycle Bin used on file shares 19 | $RECYCLE.BIN/ 20 | 21 | # Windows Installer files 22 | *.cab 23 | *.msi 24 | *.msm 25 | *.msp 26 | 27 | # Windows shortcuts 28 | *.lnk 29 | 30 | # Linux 31 | *~ 32 | 33 | # KDE directory preferences 34 | .directory 35 | 36 | # Linux trash folder which might appear on any partition or disk 37 | .Trash-* 38 | 39 | #------------------------- 40 | # Environment Files 41 | #------------------------- 42 | # These should never be under version control, 43 | # as it poses a security risk. 44 | .env 45 | .vagrant 46 | Vagrantfile 47 | 48 | #------------------------- 49 | # Temporary Files 50 | #------------------------- 51 | writable/cache/* 52 | !writable/cache/index.html 53 | 54 | writable/logs/* 55 | !writable/logs/index.html 56 | 57 | writable/session/* 58 | !writable/session/index.html 59 | 60 | writable/uploads/* 61 | !writable/uploads/index.html 62 | 63 | writable/debugbar/* 64 | 65 | php_errors.log 66 | 67 | #------------------------- 68 | # User Guide Temp Files 69 | #------------------------- 70 | user_guide_src/build/* 71 | user_guide_src/cilexer/build/* 72 | user_guide_src/cilexer/dist/* 73 | user_guide_src/cilexer/pycilexer.egg-info/* 74 | 75 | #------------------------- 76 | # Test Files 77 | #------------------------- 78 | tests/coverage* 79 | 80 | # Don't save phpunit under version control. 81 | phpunit 82 | 83 | #------------------------- 84 | # Composer 85 | #------------------------- 86 | vendor/ 87 | composer.lock 88 | 89 | #------------------------- 90 | # IDE / Development Files 91 | #------------------------- 92 | 93 | # Modules Testing 94 | _modules/* 95 | 96 | # phpenv local config 97 | .php-version 98 | 99 | # Jetbrains editors (PHPStorm, etc) 100 | .idea/ 101 | *.iml 102 | 103 | # Netbeans 104 | nbproject/ 105 | build/ 106 | nbbuild/ 107 | dist/ 108 | nbdist/ 109 | nbactions.xml 110 | nb-configuration.xml 111 | .nb-gradle/ 112 | 113 | # Sublime Text 114 | *.tmlanguage.cache 115 | *.tmPreferences.cache 116 | *.stTheme.cache 117 | *.sublime-workspace 118 | *.sublime-project 119 | .phpintel 120 | /api/ 121 | 122 | # Visual Studio Code 123 | .vscode/ 124 | 125 | /results/ 126 | /phpunit*.xml 127 | /.phpunit.*.cache 128 | -------------------------------------------------------------------------------- /src/Contracts/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | entity = $this->resolveEntity(); 25 | } 26 | 27 | /** 28 | * Abstact method to difine instance model. 29 | * 30 | * @return \CodeIgniter\Model; 31 | */ 32 | abstract public function entity(); 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | public function withCriteria(array $criteria) 38 | { 39 | foreach ($criteria as $criterion) { 40 | $this->entity = $criterion->apply($this->entity); 41 | } 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function scope(IncomingRequest $request) 50 | { 51 | $this->entity = (new Scopes($request, $this->searchable))->scope($this->entity); 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Reset model to new instance. 58 | * 59 | * @return void 60 | */ 61 | public function reset() 62 | { 63 | $this->entity = $this->resolveEntity(); 64 | } 65 | 66 | /** 67 | * Provides direct access to method in the builder (if available) 68 | * and the database connection. 69 | * 70 | * @return $this 71 | */ 72 | public function __call($method, $parameters) 73 | { 74 | if (method_exists($this->entity, 'scope' . ucfirst($method))) { 75 | $this->entity = $this->entity->{$method}(...$parameters); 76 | 77 | return $this; 78 | } 79 | 80 | $this->entity = call_user_func_array([$this->entity, $method], $parameters); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Provides/instantiates the from entity model. 87 | * 88 | * @return mixed 89 | */ 90 | public function __get($name) 91 | { 92 | return $this->entity->{$name}; 93 | } 94 | 95 | /** 96 | * Resolve entity. 97 | * 98 | * @return \CodeIgniter\Model 99 | * 100 | * @throws RepositoryException 101 | */ 102 | protected function resolveEntity() 103 | { 104 | $entity = $this->entity(); 105 | 106 | if (is_string($entity)) { 107 | return new $entity(); 108 | } elseif ($entity instanceof \CodeIgniter\Model) { 109 | return $entity; 110 | } 111 | 112 | throw new RepositoryException( 113 | "Class {$entity} must be an instance of CodeIgniter\\Model" 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Eloquent/BaseRepository.php: -------------------------------------------------------------------------------- 1 | entity->select($columns)->findAll($limit, $offset); 16 | 17 | $this->reset(); 18 | 19 | return $results; 20 | } 21 | 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function first($columns = ['*']) 26 | { 27 | $results = $this->entity->select($columns)->first(); 28 | 29 | $this->reset(); 30 | 31 | return $results; 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | public function find($id, $columns = ['*']) 38 | { 39 | $results = $this->entity->select($columns)->find($id); 40 | 41 | $this->reset(); 42 | 43 | return $results; 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function findWhere(array $conditions) 50 | { 51 | $this->withCriteria([ 52 | new FindWhere($conditions) 53 | ]); 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function paginate($perPage = null, $columns = ['*']) 62 | { 63 | $results = [ 64 | 'data' => $this->entity->select($columns)->paginate($perPage), 65 | 'paginate' => $this->entity->pager, 66 | ]; 67 | 68 | $this->reset(); 69 | 70 | return $results; 71 | } 72 | 73 | /** 74 | * @inheritdoc 75 | */ 76 | public function create(array $attributes) 77 | { 78 | $results = $this->entity->insert($attributes); 79 | 80 | $this->reset(); 81 | 82 | return $results; 83 | } 84 | 85 | /** 86 | * @inheritdoc 87 | */ 88 | public function createBatch(array $attributes) 89 | { 90 | $results = $this->entity->insertBatch($attributes); 91 | 92 | $this->reset(); 93 | 94 | return $results; 95 | } 96 | 97 | /** 98 | * @inheritdoc 99 | */ 100 | public function update(array $values, $id) 101 | { 102 | $results = $this->entity->update($id, $values); 103 | 104 | $this->reset(); 105 | 106 | return $results; 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | */ 112 | public function updateBatch(array $attributes, $id) 113 | { 114 | $results = $this->entity->updateBatch($attributes, $id); 115 | 116 | $this->reset(); 117 | 118 | return $results; 119 | } 120 | 121 | /** 122 | * @inheritdoc 123 | */ 124 | public function destroy($id) 125 | { 126 | $results = $this->entity->delete($id); 127 | 128 | $this->reset(); 129 | 130 | return $results; 131 | } 132 | 133 | /** 134 | * @inheritdoc 135 | */ 136 | public function orderBy($column, $direction = 'asc') 137 | { 138 | $this->entity = $this->entity->orderBy($column, $direction); 139 | 140 | return $this; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/NewsRepositoryRequestTest.php: -------------------------------------------------------------------------------- 1 | repository = new NewsRepositoryScope(); 30 | 31 | $this->request = Services::request(); 32 | } 33 | 34 | public function testRepositoryMyCustomScope() 35 | { 36 | $this->request->setMethod('get') 37 | ->setGlobal('get', [ 38 | 'id' => '1', 39 | ]); 40 | 41 | $this->assertNotEmpty($this->repository->scope(Services::request())->first()); 42 | } 43 | 44 | public function testRepositoryMyCustomScopeIsNull() 45 | { 46 | $this->request->setMethod('get') 47 | ->setGlobal('get', [ 48 | 'id' => '1000', 49 | ]); 50 | 51 | $this->assertNull($this->repository->scope(Services::request())->first()); 52 | } 53 | 54 | public function testRepositoryScopeTitle() 55 | { 56 | $this->request->setMethod('get') 57 | ->setGlobal('get', [ 58 | 'title' => 'A', 59 | ]); 60 | 61 | $this->assertNotEmpty($this->repository->scope(Services::request())->first()); 62 | } 63 | 64 | public function testRepositoryScopeTitleIsNull() 65 | { 66 | $this->request->setMethod('get') 67 | ->setGlobal('get', [ 68 | 'title' => 'Aaaaaa', 69 | ]); 70 | 71 | $this->assertNull($this->repository->scope(Services::request())->first()); 72 | } 73 | 74 | public function testRepositoryScopeDescription() 75 | { 76 | $this->request->setMethod('get') 77 | ->setGlobal('get', [ 78 | 'description' => 'A', 79 | ]); 80 | 81 | $this->assertNotEmpty($this->repository->scope(Services::request())->first()); 82 | } 83 | 84 | public function testRepositoryScopeDescriptionIsNull() 85 | { 86 | $this->request->setMethod('get') 87 | ->setGlobal('get', [ 88 | 'description' => 'Aaaaaa', 89 | ]); 90 | 91 | $this->assertNull($this->repository->scope(Services::request())->first()); 92 | } 93 | 94 | public function testRepositoryScopeRequestOrderByAsc() 95 | { 96 | $this->request->setMethod('get') 97 | ->setGlobal('get', [ 98 | 'orderBy' => 'title_asc', 99 | ]); 100 | 101 | $this->assertNotEmpty($this->repository->scope(Services::request())->paginate()); 102 | } 103 | 104 | public function testRepositoryScopeRequestOrderByDesc() 105 | { 106 | $this->request->setMethod('get') 107 | ->setGlobal('get', [ 108 | 'orderBy' => 'title_desc', 109 | ]); 110 | 111 | $this->assertNotEmpty($this->repository->scope(Services::request())->paginate()); 112 | } 113 | 114 | public function testRepositoryScopeRequestBeginEnd() 115 | { 116 | $this->request->setMethod('get') 117 | ->setGlobal('get', [ 118 | 'begin' => Time::now(), 119 | 'end' => Time::now(), 120 | ]); 121 | 122 | $this->assertNotNull($this->repository->scope(Services::request())->paginate()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/NewsRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repository = new NewsRepository(); 24 | } 25 | 26 | public function testRepositoryGet() 27 | { 28 | $this->assertNotEmpty($this->repository->get()); 29 | } 30 | 31 | public function testRepositoryGetWithLimitAndOffset() 32 | { 33 | $getLimitOfset = $this->repository->get(['*'], 5, 0); 34 | 35 | $this->assertNotEmpty($getLimitOfset); 36 | $this->assertCount(5, $getLimitOfset); 37 | } 38 | 39 | public function testRepositoryFirst() 40 | { 41 | $this->assertNotEmpty($this->repository->first()); 42 | } 43 | 44 | public function testRepositoryFind() 45 | { 46 | $this->assertNotEmpty($this->repository->find(1)); 47 | } 48 | 49 | public function testRepositoryFindWhere() 50 | { 51 | $this->assertNotEmpty( 52 | $this->repository->findWhere(['id' => 1])->get() 53 | ); 54 | } 55 | 56 | public function testRepositoryWithCriteria() 57 | { 58 | $this->assertNotEmpty( 59 | $this->repository->withCriteria([ 60 | new SampleCriteria([ 61 | 'id' => 1, 62 | ['id', '=', 2] 63 | ]), 64 | ]) 65 | ->get() 66 | ); 67 | } 68 | 69 | public function testRepositoryPaginate() 70 | { 71 | $resource = $this->repository->paginate(); 72 | 73 | $this->assertIsArray($resource['data']); 74 | $this->assertIsArray($resource['paginate']->getDetails()); 75 | $this->assertIsString($resource['paginate']->links()); 76 | } 77 | 78 | public function testRepositoryCreate() 79 | { 80 | $this->assertIsInt($this->repository->create([ 81 | 'title' => 'Sample title', 82 | 'description' => 'Sample Description' 83 | ])); 84 | } 85 | 86 | public function testRepositoryCreateBatch() 87 | { 88 | $data = [ 89 | [ 90 | 'title' => 'My title', 91 | 'description' => 'My Name', 92 | ], 93 | [ 94 | 'title' => 'Another title', 95 | 'description' => 'Another Name', 96 | ] 97 | ]; 98 | 99 | $this->assertIsInt($this->repository->createBatch($data)); 100 | } 101 | 102 | public function testRepositoryUpdate() 103 | { 104 | $this->assertTrue($this->repository->update([ 105 | 'title' => 'Sample title', 106 | 'description' => 'Sample Description' 107 | ], 1)); 108 | } 109 | 110 | public function testRepositoryUpdateBatch() 111 | { 112 | $data = [ 113 | [ 114 | 'title' => 'My title', 115 | 'description' => 'My Name', 116 | ], 117 | [ 118 | 'title' => 'Another title', 119 | 'description' => 'Another Name', 120 | ] 121 | ]; 122 | 123 | $this->assertIsInt($this->repository->updateBatch($data, 'title')); 124 | } 125 | 126 | public function testRepositoryDestory() 127 | { 128 | $this->assertIsObject($this->repository->destroy(1)); 129 | } 130 | 131 | public function testRepositoryOrderBy() 132 | { 133 | $this->assertNotEmpty($this->repository->orderBy('title', 'desc')->get()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter4 Repository Pattern 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/agungsugiarto/codeigniter4-repository/v)](https://github.com/agungsugiarto/codeigniter4-repository/releases) 4 | [![Total Downloads](https://poser.pugx.org/agungsugiarto/codeigniter4-repository/downloads)](https://packagist.org/packages/agungsugiarto/codeigniter4-repository) 5 | [![Latest Unstable Version](https://poser.pugx.org/agungsugiarto/codeigniter4-repository/v/unstable)](https://packagist.org/packages/agungsugiarto/codeigniter4-repository) 6 | [![License](https://poser.pugx.org/agungsugiarto/codeigniter4-repository/license)](https://packagist.org/packages/agungsugiarto/codeigniter4-repository) 7 | 8 | ## About 9 | Implementation of repository pattern for CodeIgniter 4. The package allows out-of-the-box filtering of data based on parameters in the request, and also allows you to quickly integrate the list filters and custom criteria. 10 | 11 | ## Table of Contents 12 | 13 | - Installation 14 | - Configuration 15 | - Overview 16 | - Usage 17 | - Create a Model 18 | - Create a Repository 19 | - Use built-in methods 20 | - Create a Criteria 21 | - Scope, Filter, and Order 22 | 23 | ## Installation 24 | 25 | Via Composer 26 | 27 | ``` bash 28 | $ composer require agungsugiarto/codeigniter4-repository 29 | ``` 30 | 31 | ## Overview 32 | 33 | 34 | ##### Package allows you to filter data based on incoming request parameters: 35 | 36 | ``` 37 | https://example.com/news?title=Title&custom=value&orderBy=name_desc 38 | ``` 39 | 40 | It will automatically apply built-in constraints onto the query as well as any custom scopes and criteria you need: 41 | 42 | ```php 43 | protected $searchable = [ 44 | // where 'title' equals 'Title' 45 | 'title', 46 | ]; 47 | 48 | protected $scopes = [ 49 | // and custom parameter used in your scope 50 | 'custom' => MyScope::class, 51 | ]; 52 | ``` 53 | 54 | ```php 55 | class MyScope extends ScopeAbstract 56 | { 57 | public function scope($builder, $value, $scope) 58 | { 59 | return $builder->where($scope, $value)->orWhere(...); 60 | } 61 | } 62 | ``` 63 | 64 | Ordering by any field is available: 65 | 66 | ```php 67 | protected $scopes = [ 68 | // orderBy field 69 | 'orderBy' => OrderByScope::class, 70 | ]; 71 | ``` 72 | 73 | Package can also apply any custom criteria: 74 | 75 | ```php 76 | return $this->news->withCriteria([ 77 | new MyCriteria([ 78 | 'category_id' => '1', 79 | 'name' => 'Name', 80 | ['created_at', '>', Time::now()], 81 | ]), 82 | ... 83 | ])->get(); 84 | ``` 85 | 86 | ## Usage 87 | 88 | ### Create a Model 89 | 90 | Create your model: 91 | 92 | ```php 93 | namespace App\Models; 94 | 95 | use CodeIgniter\Model; 96 | 97 | class News extends Model 98 | { 99 | ... 100 | } 101 | ``` 102 | 103 | ### Create a Repository 104 | 105 | Extend it from `Fluent\Repository\Eloquent\BaseRepository` and provide `entity()` method to return full model class name: 106 | 107 | ```php 108 | namespace App; 109 | 110 | use App\Models\News; 111 | use Fluent\Repository\Eloquent\BaseRepository; 112 | 113 | class NewsRepository extends BaseRepository 114 | { 115 | public function entity() 116 | { 117 | // Whatever choose one your style. 118 | 119 | return new News(); 120 | // or 121 | return 'App\Models\News'; 122 | // or 123 | return News::class; 124 | } 125 | } 126 | ``` 127 | 128 | ### Use built-in methods 129 | 130 | ```php 131 | use App\NewsRepository; 132 | 133 | class NewsController extends BaseController 134 | { 135 | protected $news; 136 | 137 | public function __construct() 138 | { 139 | $this->news = new NewsRepository(); 140 | } 141 | .... 142 | } 143 | ``` 144 | ### Available methods 145 | 146 | - Execute the query as a "select" statement or get all results: 147 | 148 | ```php 149 | /** 150 | * Get method implement parameter "select", "limit" and "offset". 151 | * The default will be select * and return all offset data. 152 | * 153 | * Example: $this->news->get(['*'], 50, 100); 154 | */ 155 | $news = $this->news->get(); 156 | ``` 157 | 158 | - Execute the query and get the first result: 159 | 160 | ```php 161 | $news = $this->news->first(); 162 | ``` 163 | 164 | - Find a model by its primary key: 165 | 166 | ```php 167 | $news = $this->news->find(1); 168 | ``` 169 | 170 | - Add basic where clauses and execute the query: 171 | 172 | ```php 173 | $news = $this->news->findWhere([ 174 | // where id equals 1 175 | 'id' => '1', 176 | // other "where" operations 177 | ['news_category_id', '<', '3'], 178 | ... 179 | ]); 180 | ``` 181 | 182 | - Paginate the given query: 183 | > Note: `"paginate": {}` avaliable methods see [docs](https://codeigniter4.github.io/userguide/libraries/pagination.html) 184 | 185 | ```php 186 | $news = $this->news->paginate(15); 187 | 188 | // return will be 189 | { 190 | "data": [ 191 | { 192 | "id": "3", 193 | "title": "Ms. Carole Wilderman DDS", 194 | "content": "Labore id aperiam ut voluptatem eos natus.", 195 | "created_at": "2020-08-05 17:07:16", 196 | "updated_at": "2020-08-05 17:07:16", 197 | "deleted_at": null 198 | }, 199 | ... 200 | ], 201 | "paginate": {} 202 | } 203 | ``` 204 | 205 | - Add an "order by" clause to the query: 206 | 207 | ```php 208 | $news = $this->news->orderBy('title', 'desc')->get(); 209 | ``` 210 | 211 | - Save a new model and return the instance: 212 | 213 | ```php 214 | $news = $this->news->create($this->request->getVar()); 215 | ``` 216 | 217 | - Save a batch new model and return instance: 218 | ```php 219 | $data = [ 220 | [ 221 | 'title' => 'My title', 222 | 'name' => 'My Name', 223 | 'date' => 'My date' 224 | ], 225 | [ 226 | 'title' => 'Another title', 227 | 'name' => 'Another Name', 228 | 'date' => 'Another date' 229 | ] 230 | ]; 231 | 232 | $news = $this->news->createBatch($data); 233 | ``` 234 | 235 | - Update a record: 236 | 237 | ```php 238 | $this->news->update($this->request->getVar(), $id); 239 | ``` 240 | 241 | - Update a batch record: 242 | ```php 243 | $data = [ 244 | [ 245 | 'title' => 'My title', 246 | 'name' => 'My Name', 247 | 'date' => 'My date' 248 | ], 249 | [ 250 | 'title' => 'Another title', 251 | 'name' => 'Another Name', 252 | 'date' => 'Another date' 253 | ] 254 | ]; 255 | 256 | $news = $this->news->updateBatch($data, 'title'); 257 | ``` 258 | 259 | - Delete a record by id: 260 | 261 | ```php 262 | $this->news->destroy($id); 263 | ``` 264 | 265 | ### Create a Criteria 266 | 267 | Criteria are a way to build up specific query conditions. 268 | 269 | ```php 270 | use Fluent\Repository\Contracts\CriterionInterface; 271 | 272 | class MyCriteria implements CriterionInterface 273 | { 274 | protected $conditions; 275 | 276 | public function __construct(array $conditions) 277 | { 278 | $this->conditions = $conditions; 279 | } 280 | 281 | public function apply($entity) 282 | { 283 | foreach ($this->conditions as $field => $value) { 284 | $entity = $entity->where($field, $value); 285 | } 286 | 287 | return $entity; 288 | } 289 | } 290 | ``` 291 | 292 | Multiple Criteria can be applied: 293 | 294 | ```php 295 | use App\NewsRepository; 296 | 297 | class NewsController extends BaseController 298 | { 299 | protected $news; 300 | 301 | public function __construct() 302 | { 303 | $this->news = new NewsRepository(); 304 | } 305 | 306 | public function index() 307 | { 308 | return $this->news->withCriteria([ 309 | new MyCriteria([ 310 | 'category_id' => '1', 'name' => 'Name' 311 | ]), 312 | new WhereAdmin(), 313 | ... 314 | ])->get(); 315 | } 316 | } 317 | ``` 318 | 319 | ### Scope, Filter and Order 320 | 321 | In your repository define which fields can be used to scope your queries by setting `$searchable` property. 322 | 323 | ```php 324 | protected $searchable = [ 325 | // where 'title' equals parameter value 326 | 'title', 327 | // orWhere equals 328 | 'body' => 'or', 329 | // where like 330 | 'author' => 'like', 331 | // orWhere like 332 | 'email' => 'orLike', 333 | ]; 334 | ``` 335 | 336 | Search by searchables: 337 | 338 | ```php 339 | public function index() 340 | { 341 | return $this->news->scope($this->request)->get(); 342 | } 343 | ``` 344 | 345 | ``` 346 | https://example.com/news?title=Title&body=Text&author=&email=gmail 347 | ``` 348 | 349 | Also several serchables enabled by default: 350 | 351 | ```php 352 | protected $scopes = [ 353 | // orderBy field 354 | 'orderBy' => OrderByScope::class, 355 | // where created_at date is after 356 | 'begin' => WhereDateGreaterScope::class, 357 | // where created_at date is before 358 | 'end' => WhereDateLessScope::class, 359 | ]; 360 | ``` 361 | 362 | ```php 363 | $this->news->scope($this->request)->get(); 364 | ``` 365 | 366 | Enable ordering for specific fields by adding `$orderable` property to your model class: 367 | 368 | ```php 369 | public $orderable = ['email']; 370 | ``` 371 | 372 | ``` 373 | https://example.com/news?orderBy=email_desc&begin=2019-01-24&end=2019-01-26 374 | ``` 375 | 376 | `orderBy=email_desc` will order by email in descending order, `orderBy=email` - in ascending 377 | 378 | You can also build your own custom scopes. In your repository override `scope()` method: 379 | 380 | ```php 381 | public function scope(IncomingRequest $request) 382 | { 383 | // apply build-in scopes 384 | parent::scope($request); 385 | 386 | // apply custom scopes 387 | $this->entity = (new NewsScopes($request))->scope($this->entity); 388 | 389 | return $this; 390 | } 391 | ``` 392 | 393 | Create your `scopes` class and extend `ScopesAbstract` 394 | 395 | ```php 396 | use Fluent\Repository\Scopes\ScopesAbstract; 397 | 398 | class NewsScopes extends ScopesAbstract 399 | { 400 | protected $scopes = [ 401 | // here you can add field-scope mappings 402 | 'field' => MyScope::class, 403 | ]; 404 | } 405 | ``` 406 | 407 | Now you can build any scopes you need: 408 | 409 | ```php 410 | use Fluent\Repository\Scopes\ScopeAbstract; 411 | 412 | class MyScope extends ScopeAbstract 413 | { 414 | public function scope($builder, $value, $scope) 415 | { 416 | return $builder->where($scope, $value); 417 | } 418 | } 419 | ``` 420 | 421 | ## License 422 | 423 | Released under the MIT License, see [LICENSE](https://github.com/agungsugiarto/codeigniter4-repository/blob/master/LICENSE.md). 424 | --------------------------------------------------------------------------------