├── .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 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 4 | [![Build Status](https://img.shields.io/scrutinizer/build/g/burzum/cakephp-service-layer/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/) 5 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/burzum/cakephp-service-layer/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/?branch=master) 6 | [![Code Quality](https://img.shields.io/scrutinizer/g/burzum/cakephp-service-layer/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/burzum/cakephp-service-layer/?branch=master) 7 | [![Latest Stable Version](https://poser.pugx.org/burzum/cakephp-service-layer/v/stable.svg)](https://packagist.org/packages/burzum/cakephp-service-layer) 8 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](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 | ![Autocomplete](img/ide-autocomplete.png) 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 | ![Typehinting](img/ide-typehinting.png) 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 | --------------------------------------------------------------------------------