├── .github
└── workflows
│ └── ci.yml
├── LICENSE.txt
├── README.md
├── composer.json
├── config
└── bootstrap.php
├── docs
├── Bake.md
├── Example.md
├── Ide-Helper.md
└── img
│ ├── ide-autocomplete.png
│ └── ide-typehinting.png
├── phpcs.xml
├── phpstan.neon
├── src
├── Annotator
│ └── ClassAnnotatorTask
│ │ └── ServiceAwareClassAnnotatorTask.php
├── Command
│ └── BakeServiceCommand.php
├── DomainModel
│ ├── DomainModelAwareTrait.php
│ └── DomainModelLocator.php
├── Filesystem
│ └── Folder.php
├── Generator
│ └── Task
│ │ └── ServiceTask.php
├── Plugin.php
└── Service
│ ├── ServiceAwareTrait.php
│ ├── ServiceLocator.php
│ └── ServicePaginatorTrait.php
├── templates
└── bake
│ └── Service
│ └── service.twig
└── tests
├── Fixture
└── ArticlesFixture.php
├── TestCase
├── Annotator
│ └── ClassAnnotatorTask
│ │ └── ServiceAwareClassAnnotatorTaskTest.php
├── DomainModel
│ └── DomainModelLocatorTest.php
├── Generator
│ └── Task
│ │ └── ServiceTaskTest.php
└── Service
│ ├── ServiceLocatorTest.php
│ └── ServicePaginatorTraitTest.php
├── bootstrap.php
└── schema.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | testsuite:
10 | runs-on: ubuntu-22.04
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | php-version: ['8.1', '8.2']
15 | db-type: [sqlite, mysql, pgsql]
16 | prefer-lowest: ['']
17 | include:
18 | - php-version: '8.1'
19 | db-type: 'sqlite'
20 | prefer-lowest: 'prefer-lowest'
21 |
22 | services:
23 | postgres:
24 | image: postgres
25 | ports:
26 | - 5432:5432
27 | env:
28 | POSTGRES_PASSWORD: postgres
29 |
30 | steps:
31 | - uses: actions/checkout@v3
32 |
33 | - name: Setup Service
34 | if: matrix.db-type == 'mysql'
35 | run: |
36 | sudo service mysql start
37 | mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE cakephp;'
38 | - name: Setup PHP
39 | uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: ${{ matrix.php-version }}
42 | extensions: mbstring, intl, pdo_${{ matrix.db-type }}
43 | coverage: pcov
44 |
45 | - name: Get composer cache directory
46 | id: composercache
47 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
48 |
49 | - name: Cache dependencies
50 | uses: actions/cache@v3
51 | with:
52 | path: ${{ steps.composercache.outputs.dir }}
53 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
54 | restore-keys: ${{ runner.os }}-composer-
55 |
56 | - name: Composer install --no-progress --prefer-dist --optimize-autoloader
57 | run: |
58 | composer --version
59 | if ${{ matrix.prefer-lowest == 'prefer-lowest' }}
60 | then
61 | composer update --prefer-lowest --prefer-stable
62 | composer require --dev dereuromark/composer-prefer-lowest:dev-master
63 | else
64 | composer install --no-progress --prefer-dist --optimize-autoloader
65 | fi
66 | - name: Run PHPUnit
67 | run: |
68 | if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi
69 | if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi
70 | if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi
71 | if [[ ${{ matrix.php-version }} == '8.1' && ${{ matrix.db-type }} == 'sqlite' ]]; then
72 | vendor/bin/phpunit --coverage-clover=coverage.xml
73 | else
74 | vendor/bin/phpunit
75 | fi
76 | - name: Validate prefer-lowest
77 | run: if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then vendor/bin/validate-prefer-lowest -m; fi
78 |
79 | - name: Code Coverage Report
80 | if: success() && matrix.php-version == '8.1' && matrix.db-type == 'sqlite'
81 | uses: codecov/codecov-action@v3
82 |
83 | validation:
84 | name: Coding Standard & Static Analysis
85 | runs-on: ubuntu-22.04
86 |
87 | steps:
88 | - uses: actions/checkout@v3
89 |
90 | - name: Setup PHP
91 | uses: shivammathur/setup-php@v2
92 | with:
93 | php-version: '8.1'
94 | extensions: mbstring, intl
95 | coverage: none
96 |
97 | - name: Get composer cache directory
98 | id: composercache
99 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
100 |
101 | - name: Cache dependencies
102 | uses: actions/cache@v3
103 | with:
104 | path: ${{ steps.composercache.outputs.dir }}
105 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
106 | restore-keys: ${{ runner.os }}-composer-
107 |
108 | - name: Composer Install
109 | run: composer install --no-progress --prefer-dist --optimize-autoloader
110 |
111 | - name: Run phpstan
112 | run: composer stan
113 |
114 | - name: Run phpcs
115 | run: composer cs-check
116 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Florian Krämer
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A Service Layer for CakePHP
2 |
3 | [](LICENSE.txt)
4 | [](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/)
5 | [](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/?branch=master)
6 | [](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/?branch=master)
7 | [](https://packagist.org/packages/burzum/cakephp-service-layer)
8 | [](https://php.net/)
9 |
10 | This is more a **design pattern** and **conceptual idea** than a lot of code and will *improve the maintainability* of your code base. This plugin just provides some classes to help you applying this concept in the CakePHP framework following the way of the framework of convention over configuration.
11 |
12 | ## Supported CakePHP Versions
13 |
14 | This branch is for use with **CakePHP 5.0+**. For details see [version map](https://github.com/burzum/cakephp-service-layer/wiki#cakephp-version-map).
15 |
16 | ## Introduction
17 |
18 | The rule of thumb in any MVC framework is basically "fat models, skinny controllers".
19 |
20 | While this works pretty well the abstraction can be done even better by separating for example the DB operations from the actual [business logic](https://en.wikipedia.org/wiki/Business_logic). Most Cake developers probably use the table objects as a bucket for everything. This is, strictly speaking, not correct. Business logic **doesn't** belong into the context of a DB table and should be separated from *any* persistence layer. CakePHP likes to mix persistence with business logic. Very well written business logic would be agnostic to any framework. You just use the framework to persists the results of your business logic.
21 |
22 | A table object should just encapsulate whatever is in the direct concern of that table. Queries related to that table, custom finders and so on. Some of the principles we want to follow are [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) and [single responsibility](https://en.wikipedia.org/wiki/Single_responsibility_principle). The `Model` folder in CakePHP represents the [Data Model](https://en.wikipedia.org/wiki/Data_model) and should not be used to add things outside of this conern to it. A service layer helps with that.
23 |
24 | The service class, a custom made class, not part of the CakePHP framework, would implement the real business logic and do any kind of calculations or whatever else logic operations need to be done and pass the result back to the controller which would then pass that result to the view.
25 |
26 | This ensures that each part of the code is easy to test and exchange. For example the service is as well usable in a shell app because it doesn't depend on the controller. If well separated you could, in theory, have a plugin with all your table objects and share it between two apps because the application logic, specific to each app, would be implemented in the service layer *not* in the table objects.
27 |
28 | [Martin Fowler's](https://en.wikipedia.org/wiki/Martin_Fowler) book "[Patterns of Enterprise Architecture](https://martinfowler.com/books/eaa.html)" states:
29 |
30 | > The easier question to answer is probably when not to use it. You probably don't need a Service Layer if your application's business logic will only have one kind of client - say, a user interface - and it's use case responses don't involve multiple transactional resources. [...]
31 | >
32 | > But as soon as you envision a second kind of client, or a second transactional resource in use case responses, it pays to design in a Service Layer from the beginning.
33 |
34 | ## It's opinionated
35 |
36 | There is a simple paragraph [on this page](https://blog.fedecarg.com/2009/03/11/domain-driven-design-and-mvc-architectures/) that explains pretty well why DDD in MVC is a pretty abstract and very opinionated topic:
37 |
38 | > According to Eric Evans, Domain-driven design (DDD) is not a technology or a methodology. It’s a different way of thinking about how to organize your applications and structure your code. This way of thinking complements very well the popular MVC architecture. The domain model provides a structural view of the system. Most of the time, applications don’t change, what changes is the domain. **MVC, however, doesn’t really tell you how your model should be structured. That’s why some frameworks don’t force you to use a specific model structure, instead, they let your model evolve as your knowledge and expertise grows.**
39 |
40 | CakePHP doesn't feature a template structure of any DDD or service layer architecture for that reason. It's basically up to you.
41 |
42 | This plugin provides you *one possible* implementation. It's not carved in stone, nor do you have to agree with it. Consider this plugin as a suggestion or template for the implementation and as a guidance for developers who care about maintainable code but don't know how to further improve their code base yet.
43 |
44 | ## How to use it
45 |
46 | CakePHP by default uses locators instead of a dependency injection container. This plugin gives you a CakePHP fashioned service locator and a trait so you can simply load services anywhere in your application by using the trait.
47 |
48 | The following example uses a `SomeServiceNameService` class:
49 | ```php
50 | use Burzum\CakeServiceLayer\Service\ServiceAwareTrait;
51 |
52 | class AppController extends Controller
53 | {
54 | use ServiceAwareTrait;
55 | }
56 |
57 | class FooController extends AppController
58 | {
59 | public function initialize()
60 | {
61 | parent::initialize();
62 | $this->loadService('Articles');
63 | }
64 |
65 | /**
66 | * Get a list of articles for the current logged in user
67 | */
68 | public function index()
69 | {
70 | $this->set('results', $this->Articles->getListingForUser(
71 | $this->Auth->user('id')
72 | $this->getRequest()->getQueryParams()
73 | ));
74 | }
75 | }
76 | ```
77 |
78 | If there is already a property with the name of the service used in the controller a warning will be thrown. In an ideal case your controller won't have to use any table instances anyway when using services. The tables are not a concern of the controller.
79 |
80 | The advantage of the above code is that the args passed to the service could come from shell input or any other source. The logic isn't tied to the controller nor the model. Using proper abstraction, the underlying data source, a repository that is used by the service, should be transparently replaceable with any interface that matches the required implementation.
81 |
82 | You can also load namespaced services:
83 | ```php
84 | // Loads BarService from MyPlugin and src/Service/Foo/
85 | $this->loadService('MyPlugin.Foo/Bar');
86 | ```
87 |
88 | Make sure to get IDE support using the documented IdeHelper enhancements.
89 |
90 | For details see **[docs](/docs)**.
91 |
92 | ## Why no DI container?
93 |
94 | You could achieve the very same by using a DI container of your choice but there was never really a need to do so before, the locators work just fine as well and they're less bloat than adding a full DI container lib. There was no need to add a DI container to any CakePHP app in the past ~10 years for me, not even in big projects with 500+ tables. One of the core concepts of CakePHP is to go by conventions over wiring things together in a huge DI config or using a container all over the place that is in most cases anyway just used like a super global bucket by many developers.
95 |
96 | This is of course a very opinionated topic, so if you disagree and want to go for a DI container, feel free to do so! It's awesome to have a choice!
97 |
98 | DI plugins for CakePHP:
99 |
100 | * [Piping Bag](https://github.com/lorenzo/piping-bag)
101 | * [Pimple DI](https://github.com/rochamarcelo/cake-pimple-di)
102 | * [CakePHP DI Generic PSR 11 Adapter](https://github.com/robotusers/cakephp-di)
103 |
104 | You might find more DI plugins in the [Awesome CakePHP list of plugins](https://github.com/FriendsOfCake/awesome-cakephp#dependency-injection).
105 |
106 | ## Demo
107 | The [sandbox](https://sandbox.dereuromark.de/sandbox/service-examples) showcases a live demo. Check the publically available code for details.
108 |
109 | ## License
110 |
111 | Copyright Florian Krämer
112 |
113 | Licensed under The MIT License Redistributions of files must retain the above copyright notice.
114 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "burzum/cakephp-service-layer",
3 | "type": "cakephp-plugin",
4 | "license": "MIT",
5 | "description": "Service layer and domain / business model implementation for CakePHP",
6 | "authors": [
7 | {
8 | "name": "Florian Krämer",
9 | "role": "Maintainer"
10 | },
11 | {
12 | "name": "Mark Scherer",
13 | "role": "Contributor"
14 | }
15 | ],
16 | "require": {
17 | "php": ">=8.1",
18 | "cakephp/cakephp": "^5.0.0"
19 | },
20 | "require-dev": {
21 | "phpunit/phpunit": "^10.2.0",
22 | "cakephp/cakephp-codesniffer": "^4.5",
23 | "dereuromark/cakephp-ide-helper": "^2.0.0",
24 | "phpstan/phpstan": "^1.0.0"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Burzum\\CakeServiceLayer\\": "src/"
29 | }
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "App\\": "tests/test_app/src/",
34 | "Burzum\\CakeServiceLayer\\Test\\": "tests/",
35 | "Cake\\Test\\Fixture\\": "vendor/cakephp/cakephp/tests/Fixture/"
36 | }
37 | },
38 | "scripts": {
39 | "test": "phpunit",
40 | "stan": "vendor/bin/phpstan analyze",
41 | "cs-check": "phpcs --colors --parallel=16",
42 | "cs-fix": "phpcbf --colors --parallel=16"
43 | },
44 | "config": {
45 | "allow-plugins": {
46 | "dealerdirect/phpcodesniffer-composer-installer": true
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/bootstrap.php:
--------------------------------------------------------------------------------
1 | on('Controller.initialize', function ($event) {
10 | $controller = $event->getSubject();
11 | $controller->getEventManager()->on('Service.afterPaginate', function ($event) use ($controller) {
12 | $controller->setRequest($event->getSubject()->addPagingParamToRequest($controller->getRequest()));
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/docs/Bake.md:
--------------------------------------------------------------------------------
1 | # Bake
2 |
3 | ## Install
4 | Install the plugin with composer from your CakePHP project's ROOT directory (where composer.json file is located)
5 | ```bash
6 | php composer.phar require burzum/cakephp-service-layer
7 | ```
8 | or
9 | ```bash
10 | composer require burzum/cakephp-service-layer
11 | ```
12 |
13 | ## Setup
14 | First, make sure the plugin is loaded.
15 |
16 | Inside your Application.php:
17 | ```php
18 | $this->addPlugin('Burzum/CakeServiceLayer');
19 | ```
20 |
21 | ## Bake your Service
22 |
23 | Once the plugin is loaded, you can bake your service using:
24 | ```
25 | bin/cake bake service MyFooBar
26 | ```
27 | This would create a service class `src/Service/MyFooBarService.php`.
28 |
29 | You can also bake inside sub namespaces (subfolders):
30 | ```
31 | bin/cake bake service My/Foo/Bar
32 | ```
33 | This would create a service class `src/Service/My/Foo/BarService.php`.
34 |
--------------------------------------------------------------------------------
/docs/Example.md:
--------------------------------------------------------------------------------
1 | # Simple Pagination Example
2 |
3 | ## Actual State
4 |
5 | This is a fat controller, not using a table or service for business logic to implement the listing of jobs for the current logged in user.
6 |
7 | ```php
8 | // App\Controller\JobsController.php
9 | public function index()
10 | {
11 | $query = $this->Jobs->find()
12 | ->where([
13 | 'Jobs.user_id' => $this->Auth->user('id'),
14 | 'Jobs.active' => true,
15 | ]);
16 |
17 | $this->set('jobs', $this->paginate($query));
18 | }
19 |
20 | // App\Shell\JobsShell.php
21 | public function listJobs()
22 | {
23 | $jobs = $this->Jobs->find()
24 | ->where([
25 | 'Jobs.user_id' => $this->Auth->user('id'),
26 | 'Jobs.active' => true,
27 | ])
28 | ->all();
29 |
30 | $this->printJobList($jobs);
31 | }
32 | ```
33 |
34 | Now lets refactor this to move the logic into the right places, the table and service:
35 |
36 | ## Refactoring
37 |
38 | ```php
39 | // App\Controller\JobsController.php
40 | public function index()
41 | {
42 | $this->set('jobs', $this->Jobs->getListForUser(
43 | $this->Auth->user('id',
44 | $this->request
45 | ));
46 | }
47 |
48 | // App\Model\Table\JobsTable.php
49 | public function findActive(Query $query)
50 | {
51 | return $query
52 | ->where([
53 | $this->aliasField('active') => true
54 | ]);
55 | }
56 |
57 | // App\Service\JobsService.php
58 | public function getListForUser($userId, $queryParams)
59 | {
60 | $query = $this->Jobs->find('active')
61 | ->where([
62 | 'Jobs.user_id' => $userId
63 | ]);
64 |
65 | return $this->paginate($query, $queryParams);
66 | }
67 |
68 | // App\Shell\JobsShell
69 | public function listJobs()
70 | {
71 | $jobs = $this->Jobs->getListForUser(
72 | $this->getParam('user',
73 | $this->getOptions()
74 | ));
75 |
76 | $this->printJobList($jobs);
77 | }
78 | ```
79 |
80 | ## This looks like a lot more code? Why is it better?
81 |
82 | - The controller doesn't need to know anymore anything about the used DB backend it doesn't access the DB directly through the table object anymore. If the underlying implementation changes, your controller doesn't need to be changed.
83 | - The controller doesn't implement anymore logic
84 | - No more duplicate code between controller and shell
85 | - The logic of finding only active records was moved into the table because it's a concern of the DB object and it's query
86 | - The actual logic of finding the jobs for a specific user and paginating it was moved into the service
87 | - Now you can use the same code from a shell, controller or whatever else place you want
88 | - Much better to test, especially controllers that contain a lot logic are not very nice to test
89 |
90 | All of the above makes the application easier to test and extend and by this maintain. You always want to aim for easy and low maintenance to keep the total cost of ownership of your product low.
91 |
--------------------------------------------------------------------------------
/docs/Ide-Helper.md:
--------------------------------------------------------------------------------
1 | # IDE support
2 |
3 | With [IdeHelper](https://github.com/dereuromark/cakephp-ide-helper/) plugin you can get typehinting and autocomplete for your `loadService()` calls.
4 | Especially if you use PHPStorm, this will make it possible to get support here.
5 |
6 | ## Autocomplete
7 | Include that plugin, set up your generator config and run e.g. `bin/cake phpstorm generate`.
8 |
9 | You can include the `ServiceTask` in your `config/app.php` on project level:
10 |
11 | ```php
12 | use Burzum\CakeServiceLayer\Generator\Task\ServiceTask;
13 |
14 | return [
15 | ...
16 | 'IdeHelper' => [
17 | 'generatorTasks' => [
18 | ServiceTask::class
19 | ],
20 | ],
21 | ];
22 | ```
23 |
24 | It should now add your service classes for autocomplete.
25 |
26 | 
27 |
28 | ## Typehinting
29 |
30 | If you also want to have typehinting and support for your IDE/PHPStan on those protected class properties, you also need the annotator:
31 |
32 | ```
33 | bin/cake annotations classes
34 | ```
35 |
36 | For this add this into your config to enable the `ServiceAwareClassAnnotatorTask`:
37 |
38 | ```php
39 | use Burzum\CakeServiceLayer\Annotator\ClassAnnotatorTask\ServiceAwareClassAnnotatorTask;
40 |
41 | return [
42 | ...
43 | 'IdeHelper' => [
44 | 'classAnnotatorTasks' => [
45 | ServiceAwareClassAnnotatorTask::class
46 | ],
47 | ],
48 | ];
49 | ```
50 |
51 | This will now make the Service typehinted.
52 |
53 | 
54 |
--------------------------------------------------------------------------------
/docs/img/ide-autocomplete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burzum/cakephp-service-layer/d0bb10b72c8631ac1cb8febcf6ed3f4ba9e20a6c/docs/img/ide-autocomplete.png
--------------------------------------------------------------------------------
/docs/img/ide-typehinting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burzum/cakephp-service-layer/d0bb10b72c8631ac1cb8febcf6ed3f4ba9e20a6c/docs/img/ide-typehinting.png
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | src/
6 | tests/
7 |
8 |
9 |
10 |
11 |
12 | 0
13 |
14 |
15 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | checkMissingIterableValueType: false
3 | checkGenericClassInNonGenericObjectType: false
4 | level: 8
5 | paths:
6 | - src/
7 | bootstrapFiles:
8 | - tests/bootstrap.php
9 |
--------------------------------------------------------------------------------
/src/Annotator/ClassAnnotatorTask/ServiceAwareClassAnnotatorTask.php:
--------------------------------------------------------------------------------
1 | getClassName($path, $content);
28 | if (!$className) {
29 | return false;
30 | }
31 |
32 | try {
33 | $object = new $className();
34 | if (method_exists($object, 'loadService')) {
35 | return true;
36 | }
37 | } catch (\Throwable $exception) {
38 | // Do nothing
39 | }
40 |
41 | return false;
42 | }
43 |
44 | /**
45 | * @param string $path Path
46 | * @return bool
47 | */
48 | public function annotate(string $path): bool
49 | {
50 | $services = $this->_getUsedServices($this->content);
51 |
52 | $annotations = $this->_getServiceAnnotations($services);
53 |
54 | return $this->annotateContent($path, $this->content, $annotations);
55 | }
56 |
57 | /**
58 | * @param string $content Content
59 | * @return array
60 | */
61 | protected function _getUsedServices(string $content): array
62 | {
63 | preg_match_all('/\$this-\>loadService\(\'([a-z.\\/]+)\'/i', $content, $matches);
64 | if (empty($matches[1])) {
65 | return [];
66 | }
67 |
68 | $services = $matches[1];
69 |
70 | return array_unique($services);
71 | }
72 |
73 | /**
74 | * @param array $usedServices Used services
75 | * @return \IdeHelper\Annotation\AbstractAnnotation[]
76 | */
77 | protected function _getServiceAnnotations(array $usedServices): array
78 | {
79 | $annotations = [];
80 |
81 | foreach ($usedServices as $usedService) {
82 | $className = App::className($usedService, 'Service', 'Service');
83 | if (!$className) {
84 | continue;
85 | }
86 | [, $name] = pluginSplit($usedService);
87 |
88 | if (strpos($name, '/') !== false) {
89 | $name = substr($name, strrpos($name, '/') + 1);
90 | }
91 |
92 | $annotations[] = AnnotationFactory::createOrFail('@property', '\\' . $className, '$' . $name);
93 | }
94 |
95 | return $annotations;
96 | }
97 |
98 | /**
99 | * @param string $path Path to PHP class file
100 | * @param string $content Content of PHP class file
101 | * @return string|null
102 | */
103 | protected function getClassName(string $path, string $content): ?string
104 | {
105 | preg_match('#^namespace (.+)\b#m', $content, $matches);
106 | if (!$matches) {
107 | return null;
108 | }
109 |
110 | $className = pathinfo($path, PATHINFO_FILENAME);
111 |
112 | return $matches[1] . '\\' . $className;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Command/BakeServiceCommand.php:
--------------------------------------------------------------------------------
1 | _name = $name;
54 |
55 | parent::bake($name, $args, $io);
56 | }
57 |
58 | /**
59 | * @inheritDoc
60 | */
61 | public function template(): string
62 | {
63 | return 'Burzum/CakeServiceLayer.Service/service';
64 | }
65 |
66 | /**
67 | * @inheritDoc
68 | */
69 | public function templateData(Arguments $arguments): array
70 | {
71 | $name = $this->_name;
72 | $namespace = Configure::read('App.namespace');
73 | $pluginPath = '';
74 | if ($this->plugin) {
75 | $namespace = $this->_pluginNamespace($this->plugin);
76 | $pluginPath = $this->plugin . '.';
77 | }
78 |
79 | $namespace .= '\\Service';
80 |
81 | $namespacePart = null;
82 | if (strpos($name, '/') !== false) {
83 | $parts = explode('/', $name);
84 | $name = array_pop($parts);
85 | $namespacePart = implode('\\', $parts);
86 | }
87 | if ($namespacePart) {
88 | $namespace .= '\\' . $namespacePart;
89 | }
90 |
91 | return [
92 | 'plugin' => $this->plugin,
93 | 'pluginPath' => $pluginPath,
94 | 'namespace' => $namespace,
95 | 'name' => $name,
96 | ];
97 | }
98 |
99 | /**
100 | * @inheritDoc
101 | */
102 | public function name(): string
103 | {
104 | return 'service';
105 | }
106 |
107 | /**
108 | * @inheritDoc
109 | */
110 | public function fileName(string $name): string
111 | {
112 | return $name . 'Service.php';
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/DomainModel/DomainModelAwareTrait.php:
--------------------------------------------------------------------------------
1 | getDomainModelLocator()->load($model, $constructorArgs);
52 |
53 | if (!$assignProperty) {
54 | return $domainModel;
55 | }
56 |
57 | [, $name] = pluginSplit($model);
58 |
59 | if (isset($this->{$name})) {
60 | trigger_error(static::class . '::$%s is already in use.', E_USER_WARNING);
61 | }
62 |
63 | $this->{$name} = $domainModel;
64 |
65 | return $domainModel;
66 | }
67 |
68 | /**
69 | * Get the service locator
70 | *
71 | * @return \Cake\Core\ObjectRegistry
72 | */
73 | public function getDomainModelLocator()
74 | {
75 | if (empty($this->domainModelLocator)) {
76 | $class = $this->defaultDomainModelLocator;
77 | $this->domainModelLocator = new $class();
78 | }
79 |
80 | return $this->domainModelLocator;
81 | }
82 |
83 | /**
84 | * Sets the Domain model locator
85 | *
86 | * @param \Cake\Core\ObjectRegistry $locator Locator
87 | * @return void
88 | */
89 | public function setDomainModelLocator(ObjectRegistry $locator)
90 | {
91 | $this->domainModelLocator = $locator;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/DomainModel/DomainModelLocator.php:
--------------------------------------------------------------------------------
1 |
92 | */
93 | protected array $_fsorts = [
94 | self::SORT_NAME => 'getPathname',
95 | self::SORT_TIME => 'getCTime',
96 | ];
97 |
98 | /**
99 | * Holds messages from last method.
100 | *
101 | * @var array
102 | */
103 | protected array $_messages = [];
104 |
105 | /**
106 | * Holds errors from last method.
107 | *
108 | * @var array
109 | */
110 | protected array $_errors = [];
111 |
112 | /**
113 | * Constructor.
114 | *
115 | * @param string|null $path Path to folder
116 | * @param bool $create Create folder if not found
117 | * @param int|null $mode Mode (CHMOD) to apply to created folder, false to ignore
118 | */
119 | public function __construct(?string $path = null, bool $create = false, ?int $mode = null)
120 | {
121 | if (empty($path)) {
122 | $path = TMP;
123 | }
124 | if ($mode) {
125 | $this->mode = $mode;
126 | }
127 |
128 | if (!file_exists($path) && $create === true) {
129 | $this->create($path, $this->mode);
130 | }
131 | if (!static::isAbsolute($path)) {
132 | $path = realpath($path);
133 | }
134 | if (!empty($path)) {
135 | $this->cd($path);
136 | }
137 | }
138 |
139 | /**
140 | * Return current path.
141 | *
142 | * @return string|null Current path
143 | */
144 | public function pwd(): ?string
145 | {
146 | return $this->path;
147 | }
148 |
149 | /**
150 | * Change directory to $path.
151 | *
152 | * @param string $path Path to the directory to change to
153 | * @return string|false The new path. Returns false on failure
154 | */
155 | public function cd(string $path)
156 | {
157 | $path = $this->realpath($path);
158 | if ($path !== false && is_dir($path)) {
159 | return $this->path = $path;
160 | }
161 |
162 | return false;
163 | }
164 |
165 | /**
166 | * Returns an array of the contents of the current directory.
167 | * The returned array holds two arrays: One of directories and one of files.
168 | *
169 | * @param string|bool $sort Whether you want the results sorted, set this and the sort property
170 | * to `false` to get unsorted results.
171 | * @param array|bool $exceptions Either an array or boolean true will not grab dot files
172 | * @param bool $fullPath True returns the full path
173 | * @return array> Contents of current directory as an array, an empty array on failure
174 | */
175 | public function read($sort = self::SORT_NAME, $exceptions = false, bool $fullPath = false): array
176 | {
177 | $dirs = $files = [];
178 |
179 | if (!$this->pwd()) {
180 | return [$dirs, $files];
181 | }
182 | if (is_array($exceptions)) {
183 | $exceptions = array_flip($exceptions);
184 | }
185 | $skipHidden = isset($exceptions['.']) || $exceptions === true;
186 |
187 | try {
188 | $iterator = new DirectoryIterator((string)$this->path);
189 | } catch (Exception $e) {
190 | return [$dirs, $files];
191 | }
192 |
193 | if (!is_bool($sort) && isset($this->_fsorts[$sort])) {
194 | $methodName = $this->_fsorts[$sort];
195 | } else {
196 | $methodName = $this->_fsorts[static::SORT_NAME];
197 | }
198 |
199 | foreach ($iterator as $item) {
200 | if ($item->isDot()) {
201 | continue;
202 | }
203 | $name = $item->getFilename();
204 | if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
205 | continue;
206 | }
207 | if ($fullPath) {
208 | $name = $item->getPathname();
209 | }
210 |
211 | if ($item->isDir()) {
212 | $dirs[$item->{$methodName}()][] = $name;
213 | } else {
214 | $files[$item->{$methodName}()][] = $name;
215 | }
216 | }
217 |
218 | if ($sort || $this->sort) {
219 | ksort($dirs);
220 | ksort($files);
221 | }
222 |
223 | if ($dirs) {
224 | $dirs = array_merge(...array_values($dirs));
225 | }
226 |
227 | if ($files) {
228 | $files = array_merge(...array_values($files));
229 | }
230 |
231 | return [$dirs, $files];
232 | }
233 |
234 | /**
235 | * Returns an array of all matching files in current directory.
236 | *
237 | * @param string $regexpPattern Preg_match pattern (Defaults to: .*)
238 | * @param string|bool $sort Whether results should be sorted.
239 | * @return array Files that match given pattern
240 | */
241 | public function find(string $regexpPattern = '.*', $sort = false): array
242 | {
243 | [, $files] = $this->read($sort);
244 |
245 | return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files) ?: []);
246 | }
247 |
248 | /**
249 | * Returns an array of all matching files in and below current directory.
250 | *
251 | * @param string $pattern Preg_match pattern (Defaults to: .*)
252 | * @param bool $sort Whether results should be sorted.
253 | * @return array Files matching $pattern
254 | */
255 | public function findRecursive(string $pattern = '.*', bool $sort = false): array
256 | {
257 | if (!$this->pwd()) {
258 | return [];
259 | }
260 | $startsOn = (string)$this->path;
261 | $out = $this->_findRecursive($pattern, $sort);
262 | $this->cd($startsOn);
263 |
264 | return $out;
265 | }
266 |
267 | /**
268 | * Private helper function for findRecursive.
269 | *
270 | * @param string $pattern Pattern to match against
271 | * @param bool $sort Whether results should be sorted.
272 | * @return array Files matching pattern
273 | */
274 | protected function _findRecursive(string $pattern, bool $sort = false): array
275 | {
276 | [$dirs, $files] = $this->read($sort);
277 | $found = [];
278 |
279 | foreach ($files as $file) {
280 | if (preg_match('/^' . $pattern . '$/i', $file)) {
281 | $found[] = static::addPathElement((string)$this->path, $file);
282 | }
283 | }
284 | $start = (string)$this->path;
285 |
286 | foreach ($dirs as $dir) {
287 | $this->cd(static::addPathElement($start, $dir));
288 | $found = array_merge($found, $this->findRecursive($pattern, $sort));
289 | }
290 |
291 | return $found;
292 | }
293 |
294 | /**
295 | * Returns true if given $path is a Windows path.
296 | *
297 | * @param string $path Path to check
298 | * @return bool true if windows path, false otherwise
299 | */
300 | public static function isWindowsPath(string $path): bool
301 | {
302 | return preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\';
303 | }
304 |
305 | /**
306 | * Returns true if given $path is an absolute path.
307 | *
308 | * @param string $path Path to check
309 | * @return bool true if path is absolute.
310 | */
311 | public static function isAbsolute(string $path): bool
312 | {
313 | if (empty($path)) {
314 | return false;
315 | }
316 |
317 | return $path[0] === '/' ||
318 | preg_match('/^[A-Z]:\\\\/i', $path) ||
319 | substr($path, 0, 2) === '\\\\' ||
320 | static::isRegisteredStreamWrapper($path);
321 | }
322 |
323 | /**
324 | * Returns true if given $path is a registered stream wrapper.
325 | *
326 | * @param string $path Path to check
327 | * @return bool True if path is registered stream wrapper.
328 | */
329 | public static function isRegisteredStreamWrapper(string $path): bool
330 | {
331 | return preg_match('/^[^:\/]+?(?=:\/\/)/', $path, $matches) &&
332 | in_array($matches[0], stream_get_wrappers(), true);
333 | }
334 |
335 | /**
336 | * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
337 | *
338 | * @param string $path Path to transform
339 | * @return string Path with the correct set of slashes ("\\" or "/")
340 | */
341 | public static function normalizeFullPath(string $path): string
342 | {
343 | $to = static::correctSlashFor($path);
344 | $from = ($to === '/' ? '\\' : '/');
345 |
346 | return str_replace($from, $to, $path);
347 | }
348 |
349 | /**
350 | * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
351 | *
352 | * @param string $path Path to check
353 | * @return string Set of slashes ("\\" or "/")
354 | */
355 | public static function correctSlashFor(string $path): string
356 | {
357 | return static::isWindowsPath($path) ? '\\' : '/';
358 | }
359 |
360 | /**
361 | * Returns $path with added terminating slash (corrected for Windows or other OS).
362 | *
363 | * @param string $path Path to check
364 | * @return string Path with ending slash
365 | */
366 | public static function slashTerm(string $path): string
367 | {
368 | if (static::isSlashTerm($path)) {
369 | return $path;
370 | }
371 |
372 | return $path . static::correctSlashFor($path);
373 | }
374 |
375 | /**
376 | * Returns $path with $element added, with correct slash in-between.
377 | *
378 | * @param string $path Path
379 | * @param array|string $element Element to add at end of path
380 | * @return string Combined path
381 | */
382 | public static function addPathElement(string $path, $element): string
383 | {
384 | $element = (array)$element;
385 | array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR));
386 |
387 | return implode(DIRECTORY_SEPARATOR, $element);
388 | }
389 |
390 | /**
391 | * Returns true if the Folder is in the given path.
392 | *
393 | * @param string $path The absolute path to check that the current `pwd()` resides within.
394 | * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`.
395 | * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path.
396 | * @return bool
397 | */
398 | public function inPath(string $path, bool $reverse = false): bool
399 | {
400 | if (!static::isAbsolute($path)) {
401 | throw new InvalidArgumentException('The $path argument is expected to be an absolute path.');
402 | }
403 |
404 | $dir = static::slashTerm($path);
405 | $current = static::slashTerm((string)$this->pwd());
406 |
407 | if (!$reverse) {
408 | $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current);
409 | } else {
410 | $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir);
411 | }
412 |
413 | return (bool)$return;
414 | }
415 |
416 | /**
417 | * Change the mode on a directory structure recursively. This includes changing the mode on files as well.
418 | *
419 | * @param string $path The path to chmod.
420 | * @param int|null $mode Octal value, e.g. 0755.
421 | * @param bool $recursive Chmod recursively, set to false to only change the current directory.
422 | * @param array $exceptions Array of files, directories to skip.
423 | * @return bool Success.
424 | */
425 | public function chmod(string $path, ?int $mode = null, bool $recursive = true, array $exceptions = []): bool
426 | {
427 | if (!$mode) {
428 | $mode = $this->mode;
429 | }
430 |
431 | if ($recursive === false && is_dir($path)) {
432 | // phpcs:disable
433 | if (@chmod($path, intval($mode, 8))) {
434 | // phpcs:enable
435 | $this->_messages[] = sprintf('%s changed to %s', $path, $mode);
436 |
437 | return true;
438 | }
439 |
440 | $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode);
441 |
442 | return false;
443 | }
444 |
445 | if (is_dir($path)) {
446 | $paths = $this->tree($path);
447 |
448 | foreach ($paths as $type) {
449 | foreach ($type as $fullpath) {
450 | $check = explode(DIRECTORY_SEPARATOR, $fullpath);
451 | $count = count($check);
452 |
453 | if (in_array($check[$count - 1], $exceptions, true)) {
454 | continue;
455 | }
456 |
457 | // phpcs:disable
458 | if (@chmod($fullpath, intval($mode, 8))) {
459 | // phpcs:enable
460 | $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode);
461 | } else {
462 | $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
463 | }
464 | }
465 | }
466 |
467 | if (empty($this->_errors)) {
468 | return true;
469 | }
470 | }
471 |
472 | return false;
473 | }
474 |
475 | /**
476 | * Returns an array of subdirectories for the provided or current path.
477 | *
478 | * @param string|null $path The directory path to get subdirectories for.
479 | * @param bool $fullPath Whether to return the full path or only the directory name.
480 | * @return array Array of subdirectories for the provided or current path.
481 | */
482 | public function subdirectories(?string $path = null, bool $fullPath = true): array
483 | {
484 | if (!$path) {
485 | $path = (string)$this->path;
486 | }
487 | $subdirectories = [];
488 |
489 | try {
490 | $iterator = new DirectoryIterator($path);
491 | } catch (Exception $e) {
492 | return [];
493 | }
494 |
495 | /** @var \DirectoryIterator<\SplFileInfo> $item */
496 | foreach ($iterator as $item) {
497 | if (!$item->isDir() || $item->isDot()) {
498 | continue;
499 | }
500 | $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename();
501 | }
502 |
503 | return $subdirectories;
504 | }
505 |
506 | /**
507 | * Returns an array of nested directories and files in each directory
508 | *
509 | * @param string|null $path the directory path to build the tree from
510 | * @param array|bool $exceptions Either an array of files/folder to exclude
511 | * or boolean true to not grab dot files/folders
512 | * @param string|null $type either 'file' or 'dir'. Null returns both files and directories
513 | * @return array Array of nested directories and files in each directory
514 | */
515 | public function tree(?string $path = null, $exceptions = false, ?string $type = null): array
516 | {
517 | if (!$path) {
518 | $path = (string)$this->path;
519 | }
520 | $files = [];
521 | $directories = [$path];
522 |
523 | if (is_array($exceptions)) {
524 | $exceptions = array_flip($exceptions);
525 | }
526 | $skipHidden = false;
527 | if ($exceptions === true) {
528 | $skipHidden = true;
529 | } elseif (isset($exceptions['.'])) {
530 | $skipHidden = true;
531 | unset($exceptions['.']);
532 | }
533 |
534 | $directory = $iterator = null;
535 | try {
536 | $directory = new RecursiveDirectoryIterator(
537 | $path,
538 | RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF,
539 | );
540 | $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
541 | } catch (Exception $e) {
542 | unset($directory, $iterator);
543 |
544 | if ($type === null) {
545 | return [[], []];
546 | }
547 |
548 | return [];
549 | }
550 |
551 | /**
552 | * @var string $itemPath
553 | * @var \RecursiveDirectoryIterator $fsIterator
554 | */
555 | foreach ($iterator as $itemPath => $fsIterator) {
556 | if ($skipHidden) {
557 | $subPathName = $fsIterator->getSubPathname();
558 | if ($subPathName[0] === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) {
559 | unset($fsIterator);
560 |
561 | continue;
562 | }
563 | }
564 | /** @var \FilesystemIterator $item */
565 | $item = $fsIterator->current();
566 | if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) {
567 | unset($fsIterator, $item);
568 |
569 | continue;
570 | }
571 |
572 | if ($item->isFile()) {
573 | $files[] = $itemPath;
574 | } elseif ($item->isDir() && !$item->isDot()) {
575 | $directories[] = $itemPath;
576 | }
577 |
578 | // inner iterators need to be unset too in order for locks on parents to be released
579 | unset($fsIterator, $item);
580 | }
581 |
582 | // unsetting iterators helps releasing possible locks in certain environments,
583 | // which could otherwise make `rmdir()` fail
584 | unset($directory, $iterator);
585 |
586 | if ($type === null) {
587 | return [$directories, $files];
588 | }
589 | if ($type === 'dir') {
590 | return $directories;
591 | }
592 |
593 | return $files;
594 | }
595 |
596 | /**
597 | * Create a directory structure recursively.
598 | *
599 | * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn`
600 | *
601 | * @param string $pathname The directory structure to create. Either an absolute or relative
602 | * path. If the path is relative and exists in the process' cwd it will not be created.
603 | * Otherwise, relative paths will be prefixed with the current pwd().
604 | * @param int|null $mode octal value 0755
605 | * @return bool Returns TRUE on success, FALSE on failure
606 | */
607 | public function create(string $pathname, ?int $mode = null): bool
608 | {
609 | if (is_dir($pathname) || empty($pathname)) {
610 | return true;
611 | }
612 |
613 | if (!static::isAbsolute($pathname)) {
614 | $pathname = static::addPathElement((string)$this->pwd(), $pathname);
615 | }
616 |
617 | if (!$mode) {
618 | $mode = $this->mode;
619 | }
620 |
621 | if (is_file($pathname)) {
622 | $this->_errors[] = sprintf('%s is a file', $pathname);
623 |
624 | return false;
625 | }
626 | $pathname = rtrim($pathname, DIRECTORY_SEPARATOR);
627 | $nextPathname = substr($pathname, 0, (int)strrpos($pathname, DIRECTORY_SEPARATOR));
628 |
629 | if ($this->create($nextPathname, $mode)) {
630 | if (!file_exists($pathname)) {
631 | $old = umask(0);
632 | if (mkdir($pathname, $mode, true)) {
633 | $this->_messages[] = sprintf('%s created', $pathname);
634 | umask($old);
635 |
636 | return true;
637 | }
638 | $this->_errors[] = sprintf('%s NOT created', $pathname);
639 | umask($old);
640 |
641 | return false;
642 | }
643 | }
644 |
645 | return false;
646 | }
647 |
648 | /**
649 | * Returns the size in bytes of this Folder and its contents.
650 | *
651 | * @return int size in bytes of current folder
652 | */
653 | public function dirsize(): int
654 | {
655 | $size = 0;
656 | $directory = static::slashTerm((string)$this->path);
657 | $stack = [$directory];
658 | $count = count($stack);
659 | for ($i = 0, $j = $count; $i < $j; $i++) {
660 | if (is_file($stack[$i])) {
661 | $size += filesize($stack[$i]);
662 | } elseif (is_dir($stack[$i])) {
663 | $dir = dir($stack[$i]);
664 | if ($dir) {
665 | while (($entry = $dir->read()) !== false) {
666 | if ($entry === '.' || $entry === '..') {
667 | continue;
668 | }
669 | $add = $stack[$i] . $entry;
670 |
671 | if (is_dir($stack[$i] . $entry)) {
672 | $add = static::slashTerm($add);
673 | }
674 | $stack[] = $add;
675 | }
676 | $dir->close();
677 | }
678 | }
679 | $j = count($stack);
680 | }
681 |
682 | return $size;
683 | }
684 |
685 | /**
686 | * Recursively Remove directories if the system allows.
687 | *
688 | * @param string|null $path Path of directory to delete
689 | * @return bool Success
690 | */
691 | public function delete(?string $path = null): bool
692 | {
693 | if (!$path) {
694 | $path = $this->pwd();
695 | }
696 | if (!$path) {
697 | return false;
698 | }
699 | $path = static::slashTerm($path);
700 | if (is_dir($path)) {
701 | $directory = $iterator = null;
702 | try {
703 | $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF);
704 | $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
705 | } catch (Exception $e) {
706 | unset($directory, $iterator);
707 |
708 | return false;
709 | }
710 |
711 | foreach ($iterator as $item) {
712 | $filePath = $item->getPathname();
713 | if ($item->isFile() || $item->isLink()) {
714 | // phpcs:disable
715 | if (@unlink($filePath)) {
716 | // phpcs:enable
717 | $this->_messages[] = sprintf('%s removed', $filePath);
718 | } else {
719 | $this->_errors[] = sprintf('%s NOT removed', $filePath);
720 | }
721 | } elseif ($item->isDir() && !$item->isDot()) {
722 | // phpcs:disable
723 | if (@rmdir($filePath)) {
724 | // phpcs:enable
725 | $this->_messages[] = sprintf('%s removed', $filePath);
726 | } else {
727 | $this->_errors[] = sprintf('%s NOT removed', $filePath);
728 |
729 | unset($directory, $iterator, $item);
730 |
731 | return false;
732 | }
733 | }
734 |
735 | // inner iterators need to be unset too in order for locks on parents to be released
736 | unset($item);
737 | }
738 |
739 | // unsetting iterators helps releasing possible locks in certain environments,
740 | // which could otherwise make `rmdir()` fail
741 | unset($directory, $iterator);
742 |
743 | $path = rtrim($path, DIRECTORY_SEPARATOR);
744 | // phpcs:disable
745 | if (@rmdir($path)) {
746 | // phpcs:enable
747 | $this->_messages[] = sprintf('%s removed', $path);
748 | } else {
749 | $this->_errors[] = sprintf('%s NOT removed', $path);
750 |
751 | return false;
752 | }
753 | }
754 |
755 | return true;
756 | }
757 |
758 | /**
759 | * Recursive directory copy.
760 | *
761 | * ### Options
762 | *
763 | * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
764 | * - `mode` The mode to copy the files/directories with as integer, e.g. 0775.
765 | * - `skip` Files/directories to skip.
766 | * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
767 | * - `recursive` Whether to copy recursively or not (default: true - recursive)
768 | *
769 | * @param string $to The directory to copy to.
770 | * @param array $options Array of options (see above).
771 | * @return bool Success.
772 | */
773 | public function copy(string $to, array $options = []): bool
774 | {
775 | if (!$this->pwd()) {
776 | return false;
777 | }
778 | $options += [
779 | 'from' => $this->path,
780 | 'mode' => $this->mode,
781 | 'skip' => [],
782 | 'scheme' => static::MERGE,
783 | 'recursive' => true,
784 | ];
785 |
786 | $fromDir = $options['from'];
787 | $toDir = $to;
788 | $mode = $options['mode'];
789 |
790 | if (!$this->cd($fromDir)) {
791 | $this->_errors[] = sprintf('%s not found', $fromDir);
792 |
793 | return false;
794 | }
795 |
796 | if (!is_dir($toDir)) {
797 | $this->create($toDir, $mode);
798 | }
799 |
800 | if (!is_writable($toDir)) {
801 | $this->_errors[] = sprintf('%s not writable', $toDir);
802 |
803 | return false;
804 | }
805 |
806 | $exceptions = array_merge(['.', '..', '.svn'], $options['skip']);
807 | // phpcs:disable
808 | if ($handle = @opendir($fromDir)) {
809 | // phpcs:enable
810 | while (($item = readdir($handle)) !== false) {
811 | $to = static::addPathElement($toDir, $item);
812 | if (($options['scheme'] !== static::SKIP || !is_dir($to)) && !in_array($item, $exceptions, true)) {
813 | $from = static::addPathElement($fromDir, $item);
814 | if (is_file($from) && (!is_file($to) || $options['scheme'] !== static::SKIP)) {
815 | if (copy($from, $to)) {
816 | chmod($to, intval($mode, 8));
817 | touch($to, filemtime($from) ?: null);
818 | $this->_messages[] = sprintf('%s copied to %s', $from, $to);
819 | } else {
820 | $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to);
821 | }
822 | }
823 |
824 | if (is_dir($from) && file_exists($to) && $options['scheme'] === static::OVERWRITE) {
825 | $this->delete($to);
826 | }
827 |
828 | if (is_dir($from) && $options['recursive'] === false) {
829 | continue;
830 | }
831 |
832 | if (is_dir($from) && !file_exists($to)) {
833 | $old = umask(0);
834 | if (mkdir($to, $mode, true)) {
835 | umask($old);
836 | $old = umask(0);
837 | chmod($to, $mode);
838 | umask($old);
839 | $this->_messages[] = sprintf('%s created', $to);
840 | $options = ['from' => $from] + $options;
841 | $this->copy($to, $options);
842 | } else {
843 | $this->_errors[] = sprintf('%s not created', $to);
844 | }
845 | } elseif (is_dir($from) && $options['scheme'] === static::MERGE) {
846 | $options = ['from' => $from] + $options;
847 | $this->copy($to, $options);
848 | }
849 | }
850 | }
851 | closedir($handle);
852 | } else {
853 | return false;
854 | }
855 |
856 | return empty($this->_errors);
857 | }
858 |
859 | /**
860 | * Recursive directory move.
861 | *
862 | * ### Options
863 | *
864 | * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
865 | * - `mode` The mode to copy the files/directories with as integer, e.g. 0775.
866 | * - `skip` Files/directories to skip.
867 | * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
868 | * - `recursive` Whether to copy recursively or not (default: true - recursive)
869 | *
870 | * @param string $to The directory to move to.
871 | * @param array $options Array of options (see above).
872 | * @return bool Success
873 | */
874 | public function move(string $to, array $options = []): bool
875 | {
876 | $options += ['from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true];
877 |
878 | if ($this->copy($to, $options) && $this->delete($options['from'])) {
879 | return (bool)$this->cd($to);
880 | }
881 |
882 | return false;
883 | }
884 |
885 | /**
886 | * get messages from latest method
887 | *
888 | * @param bool $reset Reset message stack after reading
889 | * @return array
890 | */
891 | public function messages(bool $reset = true): array
892 | {
893 | $messages = $this->_messages;
894 | if ($reset) {
895 | $this->_messages = [];
896 | }
897 |
898 | return $messages;
899 | }
900 |
901 | /**
902 | * get error from latest method
903 | *
904 | * @param bool $reset Reset error stack after reading
905 | * @return array
906 | */
907 | public function errors(bool $reset = true): array
908 | {
909 | $errors = $this->_errors;
910 | if ($reset) {
911 | $this->_errors = [];
912 | }
913 |
914 | return $errors;
915 | }
916 |
917 | /**
918 | * Get the real path (taking ".." and such into account)
919 | *
920 | * @param string $path Path to resolve
921 | * @return string|false The resolved path
922 | */
923 | public function realpath(string $path)
924 | {
925 | if (strpos($path, '..') === false) {
926 | if (!static::isAbsolute($path)) {
927 | $path = static::addPathElement((string)$this->path, $path);
928 | }
929 |
930 | return $path;
931 | }
932 | $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path));
933 | $parts = explode(DIRECTORY_SEPARATOR, $path);
934 | $newparts = [];
935 | $newpath = '';
936 | if ($path[0] === DIRECTORY_SEPARATOR) {
937 | $newpath = DIRECTORY_SEPARATOR;
938 | }
939 |
940 | while (($part = array_shift($parts)) !== null) {
941 | if ($part === '.' || $part === '') {
942 | continue;
943 | }
944 | if ($part === '..') {
945 | if (!empty($newparts)) {
946 | array_pop($newparts);
947 |
948 | continue;
949 | }
950 |
951 | return false;
952 | }
953 | $newparts[] = $part;
954 | }
955 | $newpath .= implode(DIRECTORY_SEPARATOR, $newparts);
956 |
957 | return static::slashTerm($newpath);
958 | }
959 |
960 | /**
961 | * Returns true if given $path ends in a slash (i.e. is slash-terminated).
962 | *
963 | * @param string $path Path to check
964 | * @return bool true if path ends with slash, false otherwise
965 | */
966 | public static function isSlashTerm(string $path): bool
967 | {
968 | $lastChar = $path[strlen($path) - 1];
969 |
970 | return $lastChar === '/' || $lastChar === '\\';
971 | }
972 | }
973 |
--------------------------------------------------------------------------------
/src/Generator/Task/ServiceTask.php:
--------------------------------------------------------------------------------
1 | collectServices();
60 | foreach ($services as $service => $className) {
61 | $map[$service] = '\\' . $className . '::class';
62 | }
63 |
64 | $result = [];
65 | foreach ($this->aliases as $alias) {
66 | $directive = new Override($alias, $map);
67 | $result[$directive->key()] = $directive;
68 | }
69 |
70 | return $result;
71 | }
72 |
73 | /**
74 | * @return string[]
75 | */
76 | protected function collectServices()
77 | {
78 | if (static::$services !== null) {
79 | return static::$services;
80 | }
81 |
82 | $services = [];
83 |
84 | $folders = App::classPath('Service');
85 | foreach ($folders as $folder) {
86 | $services = $this->addServices($services, $folder);
87 | }
88 |
89 | $plugins = Plugin::loaded();
90 | foreach ($plugins as $plugin) {
91 | $folders = App::classPath('Service', $plugin);
92 | foreach ($folders as $folder) {
93 | $services = $this->addServices($services, $folder, null, $plugin);
94 | }
95 | }
96 |
97 | static::$services = $services;
98 |
99 | return $services;
100 | }
101 |
102 | /**
103 | * @param string[] $services Services array
104 | * @param string $path Path
105 | * @param string|null $subFolder Sub folder
106 | * @param string|null $plugin Plugin
107 | * @return string[]
108 | */
109 | protected function addServices(
110 | array $services,
111 | string $path,
112 | ?string $subFolder = null,
113 | ?string $plugin = null
114 | ): array {
115 | $folderContent = (new Folder($path))->read(Folder::SORT_NAME, true);
116 |
117 | foreach ($folderContent[1] as $file) {
118 | preg_match('/^(.+)Service\.php$/', $file, $matches);
119 | if (!$matches) {
120 | continue;
121 | }
122 | $service = $matches[1];
123 | if ($subFolder) {
124 | $service = $subFolder . '/' . $service;
125 | }
126 |
127 | if ($plugin) {
128 | $service = $plugin . '.' . $service;
129 | }
130 |
131 | $className = App::className($service, 'Service', 'Service');
132 | if (!$className) {
133 | continue;
134 | }
135 |
136 | $services[$service] = $className;
137 | }
138 |
139 | foreach ($folderContent[0] as $subDirectory) {
140 | $nextSubFolder = $subFolder ? $subFolder . '/' . $subDirectory : $subDirectory;
141 | $services = $this->addServices($services, $path . $subDirectory . DS, $nextSubFolder, $plugin);
142 | }
143 |
144 | return $services;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/Plugin.php:
--------------------------------------------------------------------------------
1 | discoverPlugin($this->getName());
50 |
51 | return $collection->addMany($commands);
52 | }
53 |
54 | return $collection;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Service/ServiceAwareTrait.php:
--------------------------------------------------------------------------------
1 | {$name})) {
60 | return $this->{$name};
61 | }
62 |
63 | $serviceInstance = $this->getServiceLocator()->load($service, $constructorArgs);
64 |
65 | if (!$assignProperty) {
66 | return $serviceInstance;
67 | }
68 |
69 | $this->{$name} = $serviceInstance;
70 |
71 | return $serviceInstance;
72 | }
73 |
74 | /**
75 | * Get the service locator
76 | *
77 | * @return \Cake\Core\ObjectRegistry
78 | */
79 | public function getServiceLocator()
80 | {
81 | if (empty($this->serviceLocator)) {
82 | $class = $this->defaultServiceLocator;
83 | $this->serviceLocator = new $class();
84 | }
85 |
86 | return $this->serviceLocator;
87 | }
88 |
89 | /**
90 | * Sets the service locator
91 | *
92 | * @param \Cake\Core\ObjectRegistry $locator Locator
93 | * @return void
94 | */
95 | public function setServiceLocator(ObjectRegistry $locator)
96 | {
97 | $this->serviceLocator = $locator;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Service/ServiceLocator.php:
--------------------------------------------------------------------------------
1 | $config The Configuration settings for construction
75 | * @return object
76 | */
77 | protected function _create(object|string $class, string $alias, array $config): object
78 | {
79 | if (empty($config)) {
80 | return new $class();
81 | }
82 |
83 | $args = array_values((array)$config);
84 |
85 | return new $class(...$args);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Service/ServicePaginatorTrait.php:
--------------------------------------------------------------------------------
1 |
41 | * @var string
42 | */
43 | protected $_defaultPaginatorClass = NumericPaginator::class;
44 |
45 | /**
46 | * Set paginator instance.
47 | *
48 | * @param \Cake\Datasource\Paging\PaginatorInterface $paginator Paginator instance.
49 | * @return static
50 | */
51 | public function setPaginator(PaginatorInterface $paginator)
52 | {
53 | $this->_paginator = $paginator;
54 |
55 | return $this;
56 | }
57 |
58 | /**
59 | * Get paginator instance.
60 | *
61 | * @return \Cake\Datasource\Paging\PaginatorInterface
62 | */
63 | public function getPaginator()
64 | {
65 | if ($this->_paginator === null) {
66 | $class = $this->_defaultPaginatorClass;
67 | $this->setPaginator(new $class());
68 | }
69 |
70 | return $this->_paginator;
71 | }
72 |
73 | /**
74 | * Paginate
75 | *
76 | * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate.
77 | * @param array $params Request params
78 | * @param array $settings The settings/configuration used for pagination.
79 | * @return \Cake\Datasource\ResultSetInterface Query results
80 | */
81 | public function paginate($object, array $params = [], array $settings = [])
82 | {
83 | $event = $this->dispatchEvent('Service.beforePaginate', compact(
84 | 'object',
85 | 'params',
86 | 'settings'
87 | ));
88 |
89 | if ($event->isStopped()) {
90 | return $event->getResult();
91 | }
92 |
93 | $result = $this->getPaginator()->paginate($object, $params, $settings);
94 | $pagingParams = $this->getPaginator()->getPagingParams();
95 |
96 | $event = $this->dispatchEvent('Service.afterPaginate', compact(
97 | 'object',
98 | 'params',
99 | 'settings',
100 | 'result',
101 | 'pagingParams'
102 | ));
103 |
104 | if ($event->getResult() !== null) {
105 | return $event->getResult();
106 | }
107 |
108 | return $result;
109 | }
110 |
111 | /**
112 | * Adds the paginator params to the request objects params
113 | *
114 | * @param \Cake\Http\ServerRequest $request Request object
115 | * @return \Cake\Http\ServerRequest
116 | */
117 | public function addPagingParamToRequest(ServerRequest $request): ServerRequest
118 | {
119 | $paging = $this->getPaginator()->getPagingParams() + $request->getAttribute('paging', []);
120 |
121 | return $request->withAttribute('paging', $paging);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/templates/bake/Service/service.twig:
--------------------------------------------------------------------------------
1 | ['type' => 'integer'],
31 | 'author_id' => ['type' => 'integer', 'null' => true],
32 | 'title' => ['type' => 'string', 'null' => true],
33 | 'body' => 'text',
34 | 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'],
35 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
36 | ];
37 |
38 | /**
39 | * records property
40 | *
41 | * @var array
42 | */
43 | public array $records = [
44 | ['author_id' => 1, 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y'],
45 | ['author_id' => 3, 'title' => 'Second Article', 'body' => 'Second Article Body', 'published' => 'Y'],
46 | ['author_id' => 1, 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y'],
47 | ];
48 | }
49 |
--------------------------------------------------------------------------------
/tests/TestCase/Annotator/ClassAnnotatorTask/ServiceAwareClassAnnotatorTaskTest.php:
--------------------------------------------------------------------------------
1 | true,
35 | 'verbose' => true,
36 | ];
37 |
38 | $path = TEST_FILES . 'annotator' . DS . 'MyController.php';
39 | $content = file_get_contents($path);
40 | $task = new ServiceAwareClassAnnotatorTask($io, $config, $content);
41 |
42 | $result = $task->annotate($path);
43 | $this->assertTrue($result);
44 |
45 | $this->assertTextContains('-> 2 annotations added.', $out->output());
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/TestCase/DomainModel/DomainModelLocatorTest.php:
--------------------------------------------------------------------------------
1 | load('Article');
36 | $this->assertInstanceOf(Article::class, $service);
37 | }
38 |
39 | /**
40 | * testLocateClassNotFound
41 | *
42 | * @return void
43 | */
44 | public function testLocateClassNotFound(): void
45 | {
46 | $this->expectException(\RuntimeException::class);
47 | $this->expectExceptionMessage('Domain Model class `DoesNotExist` not found.');
48 | $locator = new DomainModelLocator();
49 | $locator->load('DoesNotExist');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/TestCase/Generator/Task/ServiceTaskTest.php:
--------------------------------------------------------------------------------
1 | collect();
33 |
34 | $this->assertCount(1, $result);
35 |
36 | /** @var \IdeHelper\Generator\Directive\Override $directive */
37 | $directive = array_shift($result);
38 | $this->assertSame('\Burzum\CakeServiceLayer\Service\ServiceAwareTrait::loadService(0)', $directive->toArray()['method']);
39 |
40 | $map = $directive->toArray()['map'];
41 | $expected = [
42 | 'Articles' => '\App\Service\ArticlesService::class',
43 | 'Test' => '\App\Service\TestService::class',
44 | 'Sub/Folder/Nested' => '\App\Service\Sub\Folder\NestedService::class',
45 | ];
46 | $this->assertSame($expected, $map);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/ServiceLocatorTest.php:
--------------------------------------------------------------------------------
1 | load('Test');
36 | $this->assertInstanceOf(TestService::class, $service);
37 | }
38 |
39 | /**
40 | * testLocate multiple
41 | *
42 | * @return void
43 | */
44 | public function testLocateMultiple()
45 | {
46 | $locator = new ServiceLocator();
47 | $service = $locator->load('Test');
48 | $service = $locator->load('Test');
49 |
50 | $this->assertInstanceOf(TestService::class, $service);
51 | }
52 |
53 | /**
54 | * testLocate
55 | *
56 | * @return void
57 | */
58 | public function testLocateNested()
59 | {
60 | $locator = new ServiceLocator();
61 | $service = $locator->load('Sub/Folder/Nested');
62 | $this->assertInstanceOf(NestedService::class, $service);
63 | }
64 |
65 | /**
66 | * testLocateClassNotFound
67 | *
68 | * @return void
69 | */
70 | public function testLocateClassNotFound()
71 | {
72 | $this->expectException(\RuntimeException::class);
73 | $this->expectExceptionMessage('Service class `DoesNotExist` not found.');
74 | $locator = new ServiceLocator();
75 | $locator->load('DoesNotExist');
76 | }
77 |
78 | /**
79 | * testPassingClassName
80 | *
81 | * @return void
82 | */
83 | public function testPassingClassName()
84 | {
85 | $locator = new ServiceLocator();
86 | $locator->load('Existing', [
87 | 'className' => TestService::class,
88 | ]);
89 |
90 | $this->assertInstanceOf(TestService::class, $locator->get('Existing'));
91 |
92 | $this->expectException(\RuntimeException::class);
93 | $locator->get('Test');
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/ServicePaginatorTraitTest.php:
--------------------------------------------------------------------------------
1 | listing($request);
57 |
58 | $this->assertInstanceOf(ResultSet::class, $result);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | 'App',
33 | 'encoding' => 'UTF-8',
34 | ]);
35 | Configure::write('debug', true);
36 |
37 | $cache = [
38 | 'default' => [
39 | 'engine' => 'File',
40 | ],
41 | '_cake_core_' => [
42 | 'className' => 'File',
43 | 'prefix' => 'crud_myapp_cake_core_',
44 | 'path' => CACHE . 'persistent/',
45 | 'serialize' => true,
46 | 'duration' => '+10 seconds',
47 | ],
48 | '_cake_model_' => [
49 | 'className' => 'File',
50 | 'prefix' => 'crud_my_app_cake_model_',
51 | 'path' => CACHE . 'models/',
52 | 'serialize' => 'File',
53 | 'duration' => '+10 seconds',
54 | ],
55 | ];
56 | Cache::setConfig($cache);
57 |
58 | // Ensure default test connection is defined
59 | if (!getenv('db_class')) {
60 | putenv('db_class=Cake\Database\Driver\Sqlite');
61 | putenv('db_dsn=sqlite::memory:');
62 | }
63 |
64 | Cake\Datasource\ConnectionManager::setConfig('test', [
65 | 'className' => 'Cake\Database\Connection',
66 | 'driver' => getenv('db_class') ?: null,
67 | 'dsn' => getenv('db_dsn') ?: null,
68 | 'timezone' => 'UTC',
69 | 'quoteIdentifiers' => true,
70 | 'cacheMetadata' => true,
71 | ]);
72 |
73 | if (env('FIXTURE_SCHEMA_METADATA')) {
74 | $loader = new Cake\TestSuite\Fixture\SchemaLoader();
75 | $loader->loadInternalFile(env('FIXTURE_SCHEMA_METADATA'));
76 | }
77 |
--------------------------------------------------------------------------------
/tests/schema.php:
--------------------------------------------------------------------------------
1 | $iterator
10 | */
11 | $iterator = new DirectoryIterator(__DIR__ . DS . 'Fixture');
12 | foreach ($iterator as $file) {
13 | if (!preg_match('/(\w+)Fixture.php$/', (string)$file, $matches)) {
14 | continue;
15 | }
16 |
17 | $name = $matches[1];
18 | $tableName = null;
19 | $class = 'Burzum\\CakeServiceLayer\\Test\\Fixture\\' . $name . 'Fixture';
20 | try {
21 | $fieldsObject = (new ReflectionClass($class))->getProperty('fields');
22 | $tableObject = (new ReflectionClass($class))->getProperty('table');
23 | $tableName = $tableObject->getDefaultValue();
24 | } catch (ReflectionException $e) {
25 | continue;
26 | }
27 |
28 | if (!$tableName) {
29 | $tableName = Inflector::underscore($name);
30 | }
31 |
32 | $array = $fieldsObject->getDefaultValue();
33 | $constraints = $array['_constraints'] ?? [];
34 | $indexes = $array['_indexes'] ?? [];
35 | unset($array['_constraints'], $array['_indexes'], $array['_options']);
36 | $table = [
37 | 'table' => $tableName,
38 | 'columns' => $array,
39 | 'constraints' => $constraints,
40 | 'indexes' => $indexes,
41 | ];
42 | $tables[$tableName] = $table;
43 | }
44 |
45 | return $tables;
46 |
--------------------------------------------------------------------------------