├── 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 | [](https://github.com/agungsugiarto/codeigniter4-repository/releases)
4 | [](https://packagist.org/packages/agungsugiarto/codeigniter4-repository)
5 | [](https://packagist.org/packages/agungsugiarto/codeigniter4-repository)
6 | [](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 |
--------------------------------------------------------------------------------