├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── php.yml ├── CHANGELOG.md ├── LICENSE ├── composer.json ├── composer.lock ├── docs ├── .gitkeep ├── README.md ├── examples │ └── user │ │ ├── ProfileRecord.php │ │ ├── UserEntity.php │ │ ├── UserQuery.php │ │ ├── UserRecord.php │ │ └── UserRepository.php └── logo.png ├── specs ├── Base │ └── Spec.php ├── Unit │ ├── Base │ │ └── EntitySpec.php │ ├── Core │ │ └── DataMapperTest.php │ ├── Data │ │ └── EntityProviderTest.php │ └── Stubs │ │ ├── Base │ │ ├── Record.php │ │ └── RecordQuery.php │ │ └── Models │ │ ├── Dummy │ │ ├── DummyEntity.php │ │ ├── DummyQuery.php │ │ ├── DummyRecord.php │ │ └── DummyRepository.php │ │ └── Tmux │ │ ├── TmuxEntity.php │ │ ├── TmuxQuery.php │ │ ├── TmuxRecord.php │ │ └── TmuxRepository.php ├── bootstrap.php └── phpunit.xml └── src ├── Base ├── Component.php ├── CompositeStrategy.php ├── DataMapper.php ├── Entity.php ├── MagicObject.php ├── ModelEvent.php └── Strategy.php ├── Contracts ├── DomainEntity.php ├── EntityController.php ├── EntityCrudController.php ├── EntityDataSource.php ├── Finder.php ├── LoggerAware.php ├── Record.php ├── RecordQuery.php ├── RecoverableRepository.php ├── Repository.php ├── ResponseHttpStatus.php ├── Specification.php ├── Strategy.php ├── StrategyContext.php └── TransactionAware.php ├── DB ├── Base │ └── Repository.php ├── Condition.php ├── EntitiesRepository.php ├── Finder.php ├── Mixins │ ├── EntityRecovering.php │ ├── QueryConditionBuilderAccess.php │ └── RecordQueryFunctions.php ├── QueryConditionBuilder.php ├── Record.php ├── RecordQuery.php ├── RecordsRepository.php └── SearchResult.php ├── Data ├── EntitiesProvider.php └── RecordsProvider.php ├── Exceptions └── UnableToSaveEntityException.php ├── Log └── Logger.php ├── Mixins ├── LoggerAccess.php ├── StaticSelfAccess.php ├── StrategiesComposingAlgorithm.php └── TransactionAccess.php └── Web ├── Actions ├── AddEntity.php ├── CallableAction.php ├── DeleteEntity.php ├── EditEntity.php ├── ListRecords.php ├── RecoverEntity.php └── ServiceAction.php ├── Base ├── Action.php ├── Actions │ ├── Action.php │ └── EntityModificationAction.php ├── EntityModificationAction.php ├── ListingModel.php ├── Mixins │ ├── EntityActionHooks.php │ ├── RepositoryAccess.php │ ├── ResponseManagement.php │ └── SessionMessagesManagement.php ├── Models │ ├── ListingModel.php │ ├── RecoverableEntitiesListModel.php │ └── ViewModel.php ├── RecoverableEntitiesListModel.php └── ViewModel.php ├── Contracts └── RepositoryAware.php └── Mixins ├── ControllerActionsManagement.php ├── EntityManagement.php ├── ModelSearching.php └── ViewModelManagement.php /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Yii2Domain 2 | ===================== 3 | 4 | If you want to ask any questions, suggest improvements or just to talk with community and developers, [join our server at Discord](https://discord.gg/Ez5VZhC) 5 | 6 | For contributing to the codebase read [organization contributing rules](https://github.com/php-kitchen/conventions/blob/master/CONTRIBUTING.md). 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What is the problem? 2 | 3 | ### What steps will reproduce the problem? 4 | 5 | ### What is the expected result? 6 | 7 | ### What do you get instead? 8 | 9 | 10 | ### Additional info 11 | 12 | | Question | Answer 13 | | -------------------- | --- 14 | | Yii2Domain version | 15 | | PHP version | 16 | | Yii version | 17 | | Operating system | 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Question | Answer 2 | | ------------- | --- 3 | | Is bugfix? | yes/no 4 | | New feature? | yes/no 5 | | Breaks BC? | yes/no 6 | | Specs pass? | yes/no 7 | | Fixed issues | comma-separated list of tickets # fixed by the PR, if any 8 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test Latest Changes 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.operating-system }} 9 | strategy: 10 | matrix: 11 | operating-system: [ubuntu-latest, macOS-latest] 12 | php-versions: ['7.1', '7.2', '7.3', '7.4'] 13 | 14 | name: PHP ${{ matrix.php-versions }} Test changes on ${{ matrix.operating-system }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Install PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-versions }} 23 | 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress --no-suggest 29 | 30 | - name: Run test suite 31 | run: php vendor/bin/phpunit --configuration specs/phpunit.xml specs 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [Release 0.0.31](https://github.com/php-kitchen/yii2-domain/releases/tag/v0.0.31) 2 | 3 | This release concentrated on a refactoring of `PHPKitchen\Domain\Web\Base` namespace. 4 | 5 | ## BC BREAKS 6 | 7 | Reorganized `PHPKitchen\Domain\Web\Base` namespace. 8 | - `Action` moved to `PHPKitchen\Domain\Web\Base\Actions` 9 | - `EntityModificationAction` moved to `PHPKitchen\Domain\Web\Base\Actions` 10 | - `ListingModel` moved to `PHPKitchen\Domain\Web\Base\Models` 11 | - `RecoverableEntitiesListModel` moved to `PHPKitchen\Domain\Web\Base\Models` 12 | - `ViewModel` moved to `PHPKitchen\Domain\Web\Base\Models` 13 | 14 | ## DEPRECATIONS 15 | 16 | To prevend immediate failure of existing applications, following classes are kept for temporarly BC compatibiity and maked as deprecated at `PHPKitchen\Domain\Web\Base` namespace: 17 | - `Action` 18 | - `EntityModificationAction` 19 | - `ListingModel` 20 | - `RecoverableEntitiesListModel` 21 | - `ViewModel` 22 | 23 | ## NEW FEATURES 24 | 25 | ### Updated actions hierarchy to become more flexible 26 | 27 | Split response, repository and session related actions to mixins: 28 | - `PHPKitchen\Domain\Web\Base\Mixins\RepositoryAccess`: provides generic repository management methods 29 | - `PHPKitchen\Domain\Web\Base\Mixins\ResponseManagement`: provides generic response management methods 30 | - `PHPKitchen\Domain\Web\Base\Mixins\SessionMessagesManagement`:provides generic session and flashes management methods 31 | 32 | Extracted action hooks for successful and failed processing to mixin `PHPKitchen\Domain\Web\Base\Mixins\EntityActionHooks` 33 | 34 | ### Added new base actions 35 | - `CallableAction`: for running strategies and callbacks 36 | - `ServiceAction`: for running services 37 | 38 | ### `Web\Base\Actions\Action` improvements 39 | 40 | Added new rendering methods to utilize a controller's rendering functionality: 41 | - `renderViewFileForAjax` 42 | - `renderFile` 43 | - `renderPartial` 44 | - `renderAjax` 45 | 46 | Added `printable` to enable/disable rendering in action through `printView` method. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 PHP Kitchen 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-kitchen/yii2-domain", 3 | "description": "Implementation of DDD key concepts for Yii2.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "DDD", 8 | "Domain Driven Design", 9 | "Entity", 10 | "Repository", 11 | "Strategy", 12 | "Finder", 13 | "Specification", 14 | "Yii2", 15 | "Clean code" 16 | ], 17 | "authors": [ 18 | { 19 | "name": "Dmitry Kolodko", 20 | "email": "prowwid@gmail.com" 21 | } 22 | ], 23 | "minimum-stability": "stable", 24 | "require": { 25 | "php": "^7.1", 26 | "yiisoft/yii2": "^2.0", 27 | "php-kitchen/yii2-di": "^0.1" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^6.5", 31 | "php-kitchen/code-specs": "^4.0", 32 | "satooshi/php-coveralls": "^1.0" 33 | }, 34 | "suggest": { 35 | "yii2tech/ar-softdelete": "Needed to support 'Recoverable' functionality." 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "PHPKitchen\\Domain\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "PHPKitchen\\Domain\\Specs\\": "specs" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-kitchen/yii2-domain/683826fd9518c675b152c1d53a48a144bd584ec3/docs/.gitkeep -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Go to [examples](examples/user/UserRepository.php) directory to see how an actual model looks like. -------------------------------------------------------------------------------- /docs/examples/user/ProfileRecord.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ProfileRecord extends Record { 19 | /** 20 | * @override 21 | * @inheritdoc 22 | */ 23 | public static function tableName() { 24 | return 'UserProfile'; 25 | } 26 | 27 | /** 28 | * @override 29 | * @inheritdoc 30 | */ 31 | public function rules() { 32 | return [ 33 | [ 34 | [ 35 | 'id', 36 | 'userId', 37 | 'fullName', 38 | 'dateOfBirth', 39 | ], 40 | 'required', 41 | ], 42 | ]; 43 | } 44 | } -------------------------------------------------------------------------------- /docs/examples/user/UserEntity.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class UserEntity extends Entity { 23 | const STATUS_ACTIVE = 1; 24 | const STATUS_INACTIVE = 2; 25 | 26 | public function activate() { 27 | $this->status = self::STATUS_ACTIVE; 28 | } 29 | 30 | public function deActivate() { 31 | $this->status = self::STATUS_INACTIVE; 32 | } 33 | 34 | public function isActive() { 35 | return $this->status === self::STATUS_ACTIVE; 36 | } 37 | 38 | public function isInactive() { 39 | return $this->status === self::STATUS_INACTIVE; 40 | } 41 | } -------------------------------------------------------------------------------- /docs/examples/user/UserQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserQuery extends RecordQuery { 14 | public function active() { 15 | return $this->andWhere('status=:status', ['status' => UserEntity::STATUS_ACTIVE]); 16 | } 17 | 18 | public function inactive() { 19 | return $this->andWhere('status=:status', ['status' => UserEntity::STATUS_INACTIVE]); 20 | } 21 | } -------------------------------------------------------------------------------- /docs/examples/user/UserRecord.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class UserRecord extends Record { 23 | public function behaviors() { 24 | return [ 25 | 'role' => [ 26 | // see https://github.com/yii2tech/ar-role 27 | 'class' => \yii2tech\ar\role\RoleBehavior::class, 28 | 'roleRelation' => 'profile', 29 | ], 30 | ]; 31 | } 32 | 33 | /** 34 | * @override 35 | * @inheritdoc 36 | */ 37 | public static function tableName() { 38 | return 'User'; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function rules() { 45 | return [ 46 | [ 47 | [ 48 | 'id', 49 | 'status', 50 | ], 51 | 'required', 52 | ], 53 | ]; 54 | } 55 | 56 | public function getProfile() { 57 | return $this->hasOne(ProfileRecord::class, ['userId' => 'id'])->alias('profile'); 58 | } 59 | } -------------------------------------------------------------------------------- /docs/examples/user/UserRepository.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserRepository extends EntitiesRepository { 14 | public function init() { 15 | $this->on(self::EVENT_BEFORE_SAVE, function () { 16 | $this->log('here we can handle events'); 17 | }); 18 | } 19 | } -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-kitchen/yii2-domain/683826fd9518c675b152c1d53a48a144bd584ec3/docs/logo.png -------------------------------------------------------------------------------- /specs/Base/Spec.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class Spec extends Specification { 14 | } -------------------------------------------------------------------------------- /specs/Unit/Base/EntitySpec.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class EntitySpec extends Spec { 15 | /** 16 | * @test 17 | */ 18 | public function phpEmptyOnNullAttributeBehavior() { 19 | $entity = $this->createEmptyEntity(); 20 | $I = $this->tester; 21 | 22 | $I->expectThat('if data source attribute is null, then PHP constructions `isset` and `empty` can see it'); 23 | $I->seeBool(empty($entity->id)) 24 | ->isTrue(); 25 | $I->seeBool(isset($entity->id)) 26 | ->isFalse(); 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function phpEmptyOnFilledAttributeBehavior() { 33 | $entity = $this->createEmptyEntity(); 34 | $I = $this->tester; 35 | 36 | $entity->id = 1; 37 | 38 | $I->expectThat('if data source attribute is filled with value, then PHP constructions `isset` and `empty` can see it'); 39 | $I->seeBool(empty($entity->id)) 40 | ->isFalse(); 41 | $I->seeBool(isset($entity->id)) 42 | ->isTrue(); 43 | } 44 | 45 | private function createEmptyEntity(): DummyEntity { 46 | $repository = new DummyRepository(); 47 | 48 | return $repository->createNewEntity(); 49 | } 50 | } -------------------------------------------------------------------------------- /specs/Unit/Core/DataMapperTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DataMapperTest extends Spec { 21 | const DUMMY_RECORD_ID = 123; 22 | protected $record; 23 | 24 | /** 25 | * @test 26 | */ 27 | public function constructorBehavior() { 28 | $this->tester 29 | ->seeObject($this->createMapper()) 30 | ->isInstanceOf(DataMapper::class); 31 | } 32 | 33 | /** 34 | * @test 35 | * @covers ::get 36 | * @covers ::canGet 37 | * @covers ::getPropertyFromDataSource 38 | * @covers ::findRepositoryForRecord 39 | */ 40 | public function getEntityBehavior() { 41 | $mapper = $this->createMapper(); 42 | $dummyEntity = $mapper->get('dummy'); 43 | $I = $this->tester; 44 | $I->describe('trying to get related entity object from data source'); 45 | 46 | $I->expectThat('mapper creates entity from given record as data source'); 47 | $I->seeObject($dummyEntity) 48 | ->isInstanceOf(DummyEntity::class); 49 | $I->see($dummyEntity->id) 50 | ->isEqualTo(self::DUMMY_RECORD_ID); 51 | } 52 | 53 | /** 54 | * @test 55 | * @covers ::get 56 | * @covers ::propertyIsAnArrayOfRecords 57 | * @covers ::arrayHasOnlyRecords 58 | */ 59 | public function getEntitiesArrayBehavior() { 60 | $mapper = $this->createMapper(); 61 | $records = $mapper->get('listOfSelfRecords'); 62 | $I = $this->tester; 63 | $I->describe('trying to get related entities list from data source'); 64 | $I->expectThat('mapper creates a list of entities from given records as data source'); 65 | $I->seeArray($records) 66 | ->isNotEmpty() 67 | ->isEqualTo($this->getEntitiesListFromRecord()); 68 | } 69 | 70 | protected function getEntitiesListFromRecord() { 71 | $tmuxRepository = new TmuxRepository(); 72 | $tmuxEntity = $tmuxRepository->createEntityFromSource($this->record); 73 | 74 | return [ 75 | $tmuxEntity, 76 | $tmuxEntity, 77 | $tmuxEntity, 78 | ]; 79 | } 80 | 81 | /** 82 | * @test 83 | * @covers ::get 84 | */ 85 | public function getArrayOfMixedValuesBehavior() { 86 | $mapper = $this->createMapper(); 87 | $records = $mapper->get('listOfMixedValues'); 88 | 89 | $I = $this->tester; 90 | $I->describe('trying to get list of mixed values from data source'); 91 | $I->expectThat('mapper creates a list of entities from given records as data source'); 92 | $I->seeArray($records) 93 | ->isEqualTo($this->getListOfMixedValuesFromRecord()); 94 | } 95 | 96 | protected function getListOfMixedValuesFromRecord() { 97 | return [ 98 | $this->record, 99 | 'value', 100 | 1, 101 | ]; 102 | } 103 | 104 | protected function createMapper(): DataMapper { 105 | $tmuxRecord = new TmuxRecord(); 106 | $dummyRecord = new DummyRecord(); 107 | $dummyRecord->id = self::DUMMY_RECORD_ID; 108 | $tmuxRecord->dummyRecord = $dummyRecord; 109 | $this->record = $tmuxRecord; 110 | 111 | return new DataMapper($tmuxRecord); 112 | } 113 | } -------------------------------------------------------------------------------- /specs/Unit/Data/EntityProviderTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class EntityProviderTest extends Spec { 21 | const STUBBED_RECORDS_COUNT = 5; 22 | 23 | /** 24 | * @test 25 | * @covers ::getModels 26 | */ 27 | public function getDataAsEntitiesBehavior() { 28 | $dataProvider = $this->createDatProviderWithStubbedRecordsData(); 29 | $count = $dataProvider->getCount(); 30 | $I = $this->tester; 31 | $I->expectThat('data provider correctly calculates entities number'); 32 | $I->see($count) 33 | ->isEqualTo(self::STUBBED_RECORDS_COUNT); 34 | $models = $dataProvider->getModels(); 35 | $I->expectThat('data provider correctly calculates entities number'); 36 | $I->seeArray($models) 37 | ->countIsEqualToCountOf(self::STUBBED_RECORDS_COUNT); 38 | foreach ($models as $model) { 39 | $I->expectThat('data provider have converted record to entity'); 40 | $I->seeObject($model) 41 | ->isInstanceOf(DummyEntity::class); 42 | } 43 | } 44 | 45 | /** 46 | * @test 47 | * @covers ::getModels 48 | */ 49 | public function getDataAsArray() { 50 | $dataProvider = $this->createDatProviderWithStubbedArrayData(); 51 | $count = $dataProvider->getCount(); 52 | $I = $this->tester; 53 | $I->expectThat('data provider correctly calculates entities number'); 54 | $I->see($count) 55 | ->isEqualTo(self::STUBBED_RECORDS_COUNT); 56 | $models = $dataProvider->getModels(); 57 | $I->expectThat('data provider correctly calculates entities number'); 58 | $I->seeArray($models) 59 | ->countIsEqualToCountOf(self::STUBBED_RECORDS_COUNT); 60 | foreach ($models as $model) { 61 | $I->expectThat('data provider have converted record to entity'); 62 | $I->see($model) 63 | ->isInternalType('array'); 64 | } 65 | } 66 | 67 | protected function createDatProviderWithStubbedRecordsData() { 68 | $repository = new DummyRepository(); 69 | $query = new DummyQuery(DummyRecord::class); 70 | $query->records = [ 71 | new DummyRecord(), 72 | new DummyRecord(), 73 | new DummyRecord(), 74 | new DummyRecord(), 75 | new DummyRecord(), 76 | ]; 77 | 78 | return new EntitiesProvider([ 79 | 'query' => $query, 80 | 'repository' => $repository, 81 | 'pagination' => false, 82 | 'sort' => false, 83 | ]); 84 | } 85 | 86 | protected function createDatProviderWithStubbedArrayData() { 87 | $repository = new DummyRepository(); 88 | $query = new DummyQuery(DummyRecord::class); 89 | $query->records = [ 90 | ['id' => 1], 91 | ['id' => 1], 92 | ['id' => 1], 93 | ['id' => 1], 94 | ['id' => 1], 95 | ]; 96 | 97 | return new EntitiesProvider([ 98 | 'query' => $query, 99 | 'repository' => $repository, 100 | 'pagination' => false, 101 | 'sort' => false, 102 | ]); 103 | } 104 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Base/Record.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Record extends \PHPKitchen\Domain\DB\Record { 12 | protected $saveResult = true; 13 | protected $deleteResult = true; 14 | protected $emulatedErrors = []; 15 | 16 | public function attributes() { 17 | return [ 18 | 'id', 19 | 'fieldOne', 20 | 'fieldTwo', 21 | ]; 22 | } 23 | 24 | public function save($runValidation = true, $attributes = null) { 25 | return $this->saveResult; 26 | } 27 | 28 | public function delete() { 29 | return $this->deleteResult; 30 | } 31 | 32 | public function getErrors($attribute = null) { 33 | return $this->emulatedErrors; 34 | } 35 | 36 | public function emulateSuccessSaveResult() { 37 | $this->saveResult = true; 38 | 39 | return $this; 40 | } 41 | 42 | public function emulateFailedSaveResult($errors = []) { 43 | $this->saveResult = false; 44 | $this->emulatedErrors = $errors; 45 | 46 | return $this; 47 | } 48 | 49 | public function emulateSuccessDeleteResult() { 50 | $this->deleteResult = true; 51 | 52 | return $this; 53 | } 54 | 55 | public function emulateFailedDeleteResult($errors = []) { 56 | $this->saveResult = false; 57 | $this->emulatedErrors = $errors; 58 | 59 | return $this; 60 | } 61 | 62 | public static function primaryKey() { 63 | return ['id']; 64 | } 65 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Base/RecordQuery.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class RecordQuery extends \PHPKitchen\Domain\DB\RecordQuery { 12 | public $records = []; 13 | public $singleRecord = []; 14 | 15 | public function all($db = null) { 16 | return $this->records; 17 | } 18 | 19 | public function one($db = null) { 20 | return $this->singleRecord; 21 | } 22 | 23 | public function batch($batchSize = 100, $db = null) { 24 | return array_chunk($this->records, $batchSize); 25 | } 26 | 27 | public function each($batchSize = 100, $db = null) { 28 | return $this->records; 29 | } 30 | 31 | // @todo implement 32 | /*public function scalar($db = null) { 33 | return parent::scalar($db); 34 | }*/ 35 | 36 | public function count($q = '*', $db = null) { 37 | return count($this->records); 38 | } 39 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Dummy/DummyEntity.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DummyEntity extends Entity { 14 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Dummy/DummyQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DummyQuery extends RecordQuery { 14 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Dummy/DummyRecord.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DummyRecord extends Record { 14 | public $id; 15 | 16 | public function getPrimaryKey($asBool = false) { 17 | return $this->id; 18 | } 19 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Dummy/DummyRepository.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DummyRepository extends EntitiesRepository { 14 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Tmux/TmuxEntity.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class TmuxEntity extends Entity { 18 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Tmux/TmuxQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TmuxQuery extends RecordQuery { 14 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Tmux/TmuxRecord.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TmuxRecord extends Record { 14 | public $dummyRecord; 15 | public $id; 16 | 17 | public function getDummy() { 18 | return $this->dummyRecord; 19 | } 20 | 21 | public function getPrimaryKey($asBool = false) { 22 | return $this->id; 23 | } 24 | 25 | public function getListOfSelfRecords() { 26 | return [ 27 | $this, 28 | $this, 29 | $this, 30 | ]; 31 | } 32 | 33 | public function getListOfMixedValues() { 34 | return [ 35 | $this, 36 | 'value', 37 | 1, 38 | ]; 39 | } 40 | } -------------------------------------------------------------------------------- /specs/Unit/Stubs/Models/Tmux/TmuxRepository.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TmuxRepository extends EntitiesRepository { 14 | } -------------------------------------------------------------------------------- /specs/bootstrap.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ../src 13 | 14 | ../vendor 15 | ../tests 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Base/Component.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Component extends \yii\base\Component implements ContainerAware, ServiceLocatorAware, LoggerAware { 20 | use ServiceLocatorAccess; 21 | use ContainerAccess; 22 | use LoggerAccess; 23 | } -------------------------------------------------------------------------------- /src/Base/CompositeStrategy.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CompositeStrategy extends Strategy { 13 | use StrategiesComposingAlgorithm; 14 | } -------------------------------------------------------------------------------- /src/Base/DataMapper.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DataMapper extends Component { 16 | /** 17 | * @var \PHPKitchen\Domain\DB\Record 18 | */ 19 | protected $dataSource; 20 | protected $relatedEntities; 21 | 22 | /** 23 | * DataMapper constructor. 24 | */ 25 | public function __construct($dataSource, $config = []) { 26 | $this->dataSource = $dataSource; 27 | parent::__construct($config); 28 | } 29 | 30 | public function canGet($name) { 31 | $dataSource = $this->dataSource; 32 | 33 | return $dataSource->canGetProperty($name); 34 | } 35 | 36 | public function canSet($name) { 37 | $dataSource = $this->dataSource; 38 | 39 | return $dataSource->canSetProperty($name); 40 | } 41 | 42 | public function isPropertySet($name) { 43 | return isset($this->dataSource->$name); 44 | } 45 | 46 | public function getDataSource() { 47 | return $this->dataSource; 48 | } 49 | 50 | public function get($name) { 51 | if (isset($this->relatedEntities[$name])) { 52 | $property = $this->relatedEntities[$name]; 53 | } else { 54 | $property = $this->getPropertyFromDataSource($name); 55 | } 56 | 57 | return $property; 58 | } 59 | 60 | public function refresh(): bool { 61 | $this->clearRelatedEntities(); 62 | 63 | return $this->getDataSource()->refresh(); 64 | } 65 | 66 | protected function getPropertyFromDataSource($propertyName) { 67 | $property = $this->canGet($propertyName) ? $this->dataSource->$propertyName : null; 68 | 69 | if ($property instanceof Record && ($repository = $this->findRepositoryForRecord($property))) { 70 | $property = $repository->createEntityFromSource($property); 71 | $this->relatedEntities[$propertyName] = $property; 72 | } elseif ($this->propertyIsAnArrayOfRecords($property)) { 73 | $repository = $this->findRepositoryForRecord($property[0]); 74 | if ($repository) { 75 | $entities = []; 76 | foreach ($property as $key => $item) { 77 | $entities[$key] = $repository->createEntityFromSource($item); 78 | } 79 | $property = &$entities; 80 | $this->relatedEntities[$propertyName] = &$entities; 81 | } 82 | } 83 | 84 | return $property; 85 | } 86 | 87 | protected function propertyIsAnArrayOfRecords($property) { 88 | return is_array($property) && isset($property[0]) && ($property[0] instanceof Record) && $this->arrayHasOnlyRecords($property); 89 | } 90 | 91 | protected function arrayHasOnlyRecords(&$array) { 92 | return array_reduce( 93 | $array, 94 | function ($result, $element) { 95 | return ($element instanceof Record); 96 | } 97 | ); 98 | } 99 | 100 | /** 101 | * @param $record 102 | * 103 | * @return null|\PHPKitchen\Domain\DB\EntitiesRepository 104 | */ 105 | protected function findRepositoryForRecord($record) { 106 | $recordClass = get_class($record); 107 | $repositoryClass = strstr($recordClass, 'Record') ? str_replace('Record', 'Repository', $recordClass) : null; 108 | $container = $this->container; 109 | try { 110 | $repository = $repositoryClass ? $container->create($repositoryClass) : null; 111 | } catch (\Exception $e) { 112 | $repository = null; 113 | } 114 | 115 | return $repository; 116 | } 117 | 118 | protected function clearRelatedEntities(): void { 119 | $this->relatedEntities = []; 120 | } 121 | 122 | public function set($name, $value) { 123 | return $this->canSet($name) ? $this->dataSource->$name = $value : null; 124 | } 125 | 126 | public function unSetProperty($name) { 127 | if ($this->isPropertySet($name)) { 128 | unset($this->dataSource->$name); 129 | } 130 | } 131 | 132 | public function isRecordNew() { 133 | return $this->dataSource->isNew(); 134 | } 135 | 136 | public function getPrimaryKey() { 137 | return $this->dataSource->primaryKey; 138 | } 139 | 140 | public function load($data) { 141 | return $this->dataSource->load($data, ''); 142 | } 143 | 144 | public function getAttributes() { 145 | return $this->dataSource->attributes; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Base/Entity.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Entity extends Component implements DomainEntity, \IteratorAggregate, \ArrayAccess, Arrayable { 20 | use ArrayableTrait; 21 | use ArrayAccessTrait; 22 | /** 23 | * @var \PHPKitchen\Domain\Base\DataMapper 24 | */ 25 | private $_dataMapper; 26 | 27 | public function getId() { 28 | return $this->dataMapper->primaryKey; 29 | } 30 | 31 | /** 32 | * Populates the {@lint _dataSource} with input data. 33 | * 34 | * This method provides a convenient shortcut for: 35 | * 36 | * ```php 37 | * if (isset($_POST['FormName'])) { 38 | * $model->attributes = $_POST['FormName']; 39 | * if ($model->save()) { 40 | * // handle success 41 | * } 42 | * } 43 | * ``` 44 | * 45 | * which, with `load()` can be written as: 46 | * 47 | * ```php 48 | * if ($model->load($_POST) && $model->save()) { 49 | * // handle success 50 | * } 51 | * ``` 52 | * 53 | * `load()` gets the `'FormName'` from the model's [[formName()]] method (which you may override), unless the 54 | * `$formName` parameter is given. If the form name is empty, `load()` populates the model with the whole of `$data`, 55 | * instead of `$data['FormName']`. 56 | * 57 | * Note, that the data being populated is subject to the safety check by [[setAttributes()]]. 58 | * 59 | * @param array $data the data array to load, typically `$_POST` or `$_GET`. 60 | * @param string $formName the form name to use to load the data into the model. 61 | * If not set, [[formName()]] is used. 62 | * 63 | * @return boolean whether `load()` found the expected form in `$data`. 64 | */ 65 | public function load($data) { 66 | return $this->dataMapper->load($this->convertDataToSourceAttributes($data)); 67 | } 68 | 69 | /** 70 | * Converts data passed to {@link load()} into {@link _dataSource} attributes. 71 | * Override this method to implement specific logic for your entity. 72 | * 73 | * @param mixed $data traversable data of {@link _dataSource}. 74 | * 75 | * @return mixed converted data. By default returns the same data as passed. 76 | */ 77 | protected function convertDataToSourceAttributes(&$data) { 78 | return $data; 79 | } 80 | 81 | public function isNew() { 82 | return $this->dataMapper->isRecordNew(); 83 | } 84 | 85 | public function isNotNew() { 86 | return !$this->dataMapper->isRecordNew(); 87 | } 88 | 89 | public function hasAttribute($name) { 90 | return $this->dataMapper->canGet($name); 91 | } 92 | 93 | public function getAttribute($name) { 94 | return $this->dataMapper->get($name); 95 | } 96 | 97 | // -------------- MAGIC ACCESS TO DATA SOURCE ATTRIBUTES -------------- 98 | 99 | public function __get($name) { 100 | try { 101 | $result = parent::__get($name); 102 | } catch (\Exception $e) { 103 | $dataMapper = $this->getDataMapper(); 104 | if ($dataMapper && $dataMapper->canGet($name)) { 105 | $result = $dataMapper->get($name); 106 | } else { 107 | throw $e; 108 | } 109 | } 110 | 111 | return $result; 112 | } 113 | 114 | public function __set($name, $value) { 115 | try { 116 | parent::__set($name, $value); 117 | } catch (\Exception $e) { 118 | $dataMapper = $this->getDataMapper(); 119 | if ($dataMapper && $dataMapper->canSet($name)) { 120 | $dataMapper->set($name, $value); 121 | } else { 122 | throw $e; 123 | } 124 | } 125 | } 126 | 127 | public function __isset($name) { 128 | $dataMapper = $this->getDataMapper(); 129 | 130 | return parent::__isset($name) || ($dataMapper && $dataMapper->isPropertySet($name)); 131 | } 132 | 133 | public function __unset($name) { 134 | try { 135 | parent::__unset($name); 136 | } catch (\Exception $e) { 137 | $dataMapper = $this->getDataMapper(); 138 | if ($dataMapper && $dataMapper->isPropertySet($name)) { 139 | $dataMapper->unSetProperty($name); 140 | } else { 141 | throw $e; 142 | } 143 | } 144 | } 145 | 146 | public function hasProperty($name, $checkVars = true, $checkBehaviors = true) { 147 | $result = parent::hasProperty($name, $checkVars, $checkBehaviors); 148 | if (!$result) { 149 | $dataMapper = $this->getDataMapper(); 150 | $result = $dataMapper && ($dataMapper->canGet($name) || $dataMapper->canSet($name)); 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) { 157 | $result = parent::canGetProperty($name, $checkVars, $checkBehaviors); 158 | if (!$result) { 159 | $dataMapper = $this->getDataMapper(); 160 | $result = $dataMapper && $dataMapper->canGet($name); 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { 167 | $result = parent::canSetProperty($name, $checkVars, $checkBehaviors); 168 | if (!$result) { 169 | $dataMapper = $this->getDataMapper(); 170 | $result = $dataMapper && $dataMapper->canSet($name); 171 | } 172 | 173 | return $result; 174 | } 175 | 176 | // -------------- GETTERS/SETTERS -------------- 177 | 178 | public function getDataMapper() { 179 | return $this->_dataMapper; 180 | } 181 | 182 | public function setDataMapper(DataMapper $source) { 183 | $this->_dataMapper = $source; 184 | } 185 | 186 | protected function getData() { 187 | return $this->dataMapper->getAttributes(); 188 | } 189 | } -------------------------------------------------------------------------------- /src/Base/MagicObject.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class MagicObject extends BaseObject implements ContainerAware, ServiceLocatorAware { 19 | use ServiceLocatorAccess; 20 | use ContainerAccess; 21 | use LoggerAccess; 22 | } -------------------------------------------------------------------------------- /src/Base/ModelEvent.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ModelEvent extends Event { 15 | /** 16 | * @var DomainEntity 17 | */ 18 | protected $_entity; 19 | protected $_valid = true; 20 | 21 | public function __construct(DomainEntity $entity, $config = []) { 22 | $this->_entity = $entity; 23 | parent::__construct($config); 24 | } 25 | 26 | public function isValid(): bool { 27 | return $this->_valid; 28 | } 29 | 30 | public function failAndMarkHandled(): void { 31 | $this->fail()->markHandled(); 32 | } 33 | 34 | public function fail(): self { 35 | $this->_valid = false; 36 | 37 | return $this; 38 | } 39 | 40 | public function markHandled(): self { 41 | $this->handled = true; 42 | 43 | return $this; 44 | } 45 | 46 | public function getEntity(): DomainEntity { 47 | return $this->_entity; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Base/Strategy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class Strategy extends Component implements Contracts\Strategy { 14 | abstract protected function executeCallAction(); 15 | 16 | /** 17 | * @param array ...$params algorithm params. 18 | * 19 | * @return mixed strategy result. 20 | */ 21 | public function __invoke(...$params) { 22 | return $this->call(...$params); 23 | } 24 | 25 | public function call() { 26 | $this->executeBeforeCall(); 27 | 28 | $result = $this->executeCallAction(); 29 | 30 | $this->executeAfterCall(); 31 | 32 | return $result; 33 | } 34 | 35 | protected function executeBeforeCall() { 36 | $this->trigger(self::EVENT_BEFORE_CALL, new Event()); 37 | } 38 | 39 | protected function executeAfterCall() { 40 | $this->trigger(self::EVENT_AFTER_CALL, new Event()); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Contracts/DomainEntity.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface DomainEntity { 16 | public function isNew(); 17 | 18 | public function isNotNew(); 19 | } -------------------------------------------------------------------------------- /src/Contracts/EntityController.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface EntityController { 14 | /** 15 | * @return \PHPKitchen\Domain\DB\EntitiesRepository 16 | */ 17 | public function getRepository(); 18 | 19 | public function setRepository(Repository $repository); 20 | 21 | public function findEntityByPk($pk); 22 | } -------------------------------------------------------------------------------- /src/Contracts/EntityCrudController.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface EntityCrudController { 18 | /** 19 | * @return EntitiesRepository 20 | */ 21 | public function getRepository(); 22 | 23 | public function setRepository($repository); 24 | 25 | public function findEntityByPk($pk); 26 | 27 | public function createListingDataProvider(); 28 | } -------------------------------------------------------------------------------- /src/Contracts/EntityDataSource.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface EntityDataSource { 12 | /** 13 | * Saves the current record. 14 | * 15 | * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] 16 | * when [[isNewRecord]] is false. 17 | * 18 | * For example, to save a customer record: 19 | * 20 | * ```php 21 | * $customer = new Customer; // or $customer = Customer::findOne($id); 22 | * $customer->name = $name; 23 | * $customer->email = $email; 24 | * $customer->save(); 25 | * ``` 26 | * 27 | * @param boolean $runValidation whether to perform validation (calling [[validate()]]) 28 | * before saving the record. Defaults to `true`. If the validation fails, the record 29 | * will not be saved to the database and this method will return `false`. 30 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, 31 | * meaning all attributes that are loaded from DB will be saved. 32 | * 33 | * @return boolean whether the saving succeeded (i.e. no validation errors occurred). 34 | */ 35 | public function validateAndSave($attributeNames = null); 36 | 37 | /** 38 | * Saves the current record. 39 | * 40 | * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] 41 | * when [[isNewRecord]] is false. 42 | * 43 | * For example, to save a customer record: 44 | * 45 | * ```php 46 | * $customer = new Customer; // or $customer = Customer::findOne($id); 47 | * $customer->name = $name; 48 | * $customer->email = $email; 49 | * $customer->save(); 50 | * ``` 51 | * 52 | * @param boolean $runValidation whether to perform validation (calling [[validate()]]) 53 | * before saving the record. Defaults to `true`. If the validation fails, the record 54 | * will not be saved to the database and this method will return `false`. 55 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, 56 | * meaning all attributes that are loaded from DB will be saved. 57 | * 58 | * @return boolean whether the saving succeeded (i.e. no validation errors occurred). 59 | */ 60 | public function saveWithoutValidation($attributeNames = null); 61 | 62 | /** 63 | * Deletes the table row corresponding to this active record. 64 | * 65 | * @return integer|false the number of rows deleted, or false if the deletion is unsuccessful for some reason. 66 | * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. 67 | */ 68 | public function deleteRecord(); 69 | 70 | /** 71 | * Returns the named attribute value. 72 | * If this record is the result of a query and the attribute is not loaded, 73 | * null will be returned. 74 | * 75 | * @param string $name the attribute name 76 | * 77 | * @return mixed the attribute value. Null if the attribute is not set or does not exist. 78 | */ 79 | public function getAttribute($name); 80 | } -------------------------------------------------------------------------------- /src/Contracts/Finder.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface Finder { 12 | } -------------------------------------------------------------------------------- /src/Contracts/LoggerAware.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface LoggerAware { 12 | /** 13 | * Logs an informative message. 14 | * An informative message is typically logged by an application to keep record of 15 | * something important (e.g. an administrator logs in). 16 | * 17 | * @param string $message the message to be logged. 18 | * @param string $category the category of the message. 19 | */ 20 | public function logInfo($message, $category = ''); 21 | 22 | /** 23 | * Logs a warning message. 24 | * A warning message is typically logged when an error occurs while the execution 25 | * can still continue. 26 | * 27 | * @param string $message the message to be logged. 28 | * @param string $category the category of the message. 29 | */ 30 | public function logWarning($message, $category = ''); 31 | 32 | /** 33 | * Logs an error message. 34 | * An error message is typically logged when an unrecoverable error occurs 35 | * during the execution of an application. 36 | * 37 | * @param string $message the message to be logged. 38 | * @param string $category the category of the message. 39 | */ 40 | public function logError($message, $category = ''); 41 | 42 | /** 43 | * Marks the beginning of a code block for profiling. 44 | * This has to be matched with a call to [[endProfile]] with the same category name. 45 | * The begin- and end- calls must also be properly nested. For example, 46 | * 47 | * ```php 48 | * $this->beginProfile('block1'); 49 | * // some code to be profiled 50 | * $this->beginProfile('block2'); 51 | * // some other code to be profiled 52 | * $this->endProfile('block2'); 53 | * $this->endProfile('block1'); 54 | * ``` 55 | * 56 | * @param string $token token for the code block 57 | * @param string $category the category of this log message 58 | * 59 | * @see endProfile() 60 | */ 61 | public function beginProfile($token, $category = ''); 62 | 63 | /** 64 | * Marks the end of a code block for profiling. 65 | * This has to be matched with a previous call to [[beginProfile]] with the same category name. 66 | * 67 | * @param string $token token for the code block 68 | * @param string $category the category of this log message 69 | * 70 | * @see beginProfile() 71 | */ 72 | public function endProfile($token, $category = ''); 73 | 74 | /** 75 | * Logs a trace message. 76 | * Trace messages are logged mainly for development purpose to see 77 | * the execution work flow of some code. 78 | * 79 | * @param string $message the message to be logged. 80 | * @param string $category the category of the message. 81 | */ 82 | public function trace($message, $category = ''); 83 | } -------------------------------------------------------------------------------- /src/Contracts/Record.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface Record { 12 | } -------------------------------------------------------------------------------- /src/Contracts/RecordQuery.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | interface RecordQuery extends ActiveQueryInterface { 17 | public function find(); 18 | 19 | /** 20 | * @param $pk 21 | * 22 | * @return \yii\db\ActiveRecord|array|null 23 | */ 24 | public function oneWithPk($pk); 25 | } -------------------------------------------------------------------------------- /src/Contracts/RecoverableRepository.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface RecoverableRepository { 12 | public function recover(DomainEntity $entity); 13 | } -------------------------------------------------------------------------------- /src/Contracts/Repository.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface Repository { 12 | const EVENT_BEFORE_SAVE = 'beforeSave'; 13 | const EVENT_AFTER_SAVE = 'afterSave'; 14 | const EVENT_BEFORE_ADD = 'beforeAdd'; 15 | const EVENT_BEFORE_UPDATE = 'beforeUpdate'; 16 | /** 17 | * @deprecated use EVENT_AFTER_ADD if you used this one 18 | */ 19 | const AFTER_BEFORE_ADD = 'afterAdd'; 20 | const EVENT_AFTER_ADD = 'afterAdd'; 21 | const EVENT_AFTER_UPDATE = 'afterUpdate'; 22 | const EVENT_BEFORE_DELETE = 'beforeDelete'; 23 | const EVENT_AFTER_DELETE = 'afterDelete'; 24 | 25 | public function validateAndSave(DomainEntity $entity, ?array $attributes = null); 26 | 27 | public function saveWithoutValidation(DomainEntity $entity, ?array $attributes = null); 28 | 29 | public function delete(DomainEntity $entity): bool; 30 | 31 | public function validate(DomainEntity $entity): bool; 32 | 33 | public function refresh(DomainEntity $entity): bool; 34 | 35 | public function findOneWithPk($pk); 36 | 37 | public function findAll(); 38 | 39 | public function each(); 40 | 41 | public function find(); 42 | 43 | public function createNewEntity(); 44 | 45 | public function getEntitiesProvider(); 46 | 47 | public function isNewOrJustAdded(DomainEntity $entity): bool; 48 | 49 | public function isJustUpdated(DomainEntity $entity): bool; 50 | 51 | public function isJustAdded(DomainEntity $entity): bool; 52 | 53 | public function getDirtyAttributes(DomainEntity $entity, array $names = null): array; 54 | 55 | public function getOldAttributes(DomainEntity $entity): array; 56 | 57 | public function getOldAttribute(DomainEntity $entity, string $name); 58 | 59 | public function isAttributeChanged(DomainEntity $entity, string $name, bool $identical = true): bool; 60 | 61 | public function setChangedAttributes(DomainEntity $entity, array $changedAttributes): void; 62 | 63 | public function getChangedAttributes(DomainEntity $entity): array; 64 | 65 | public function getChangedAttribute(DomainEntity $entity, string $name); 66 | 67 | public function wasAttributeChanged(DomainEntity $entity, string $name): bool; 68 | 69 | public function wasAttributeValueChanged(DomainEntity $entity, string $name): bool; 70 | } -------------------------------------------------------------------------------- /src/Contracts/ResponseHttpStatus.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ResponseHttpStatus { 12 | public const OK = 200; 13 | public const FOUND = 302; 14 | } -------------------------------------------------------------------------------- /src/Contracts/Specification.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface Specification extends ActiveQueryInterface { 14 | public function bySearchModel($model); 15 | } -------------------------------------------------------------------------------- /src/Contracts/Strategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Strategy { 11 | const EVENT_BEFORE_CALL = 'beforeCall'; 12 | const EVENT_AFTER_CALL = 'afterCall'; 13 | 14 | public function call(); 15 | } -------------------------------------------------------------------------------- /src/Contracts/StrategyContext.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface StrategyContext { 11 | } -------------------------------------------------------------------------------- /src/Contracts/TransactionAware.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface TransactionAware { 12 | } -------------------------------------------------------------------------------- /src/DB/Base/Repository.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | abstract class Repository extends Component implements Contracts\Repository { 27 | use TransactionAccess; 28 | /** 29 | * @var array Stores errors which could occur during save process 30 | */ 31 | public $errors = []; 32 | /** 33 | * @var int indicates whether to throw exception or handle it 34 | */ 35 | public $throwExceptions = false; 36 | /** 37 | * @var bool indicates whether to use DB transaction or not. 38 | */ 39 | public $useTransactions = true; 40 | /** 41 | * @var string entities provider class name. Change it in {@link init()} method if you need 42 | * custom provider. 43 | */ 44 | public $entitiesProviderClassName; 45 | /** 46 | * @var string class name of an event that being triggered on each important action. Change it in {@link init()} method 47 | * if you need custom event. 48 | */ 49 | public $modelEventClassName = Domain\Base\ModelEvent::class; 50 | /** 51 | * @var string records query class name. This class being used if no query specified in morel directory. Change it 52 | * in {@link init()} method if you need custom default query. 53 | */ 54 | private $_defaultQueryClassName = Domain\DB\RecordQuery::class; 55 | private $_className; 56 | /** 57 | * @var string indicates what entity to use. By default equal following template "{model name}Entity" where model name is equal to 58 | * the repository class name without "Repository" suffix. 59 | */ 60 | private $_entityClassName; 61 | /** 62 | * @var string indicates what records query to use. By default equal following template "{model name}Query" where model name is equal to 63 | * the repository class name without "Repository" suffix. 64 | */ 65 | private $_queryClassName; 66 | /** 67 | * @var string indicates what record to use. By default equal following template "{model name}Record" where model name is equal to 68 | * the repository class name without "Repository" suffix. 69 | */ 70 | private $_recordClassName; 71 | 72 | /** 73 | * @return Domain\DB\Finder|Domain\DB\RecordQuery 74 | */ 75 | abstract public function find(); 76 | 77 | abstract protected function saveEntityInternal(Contracts\DomainEntity $entity, bool $runValidation, ?array $attributes): bool; 78 | 79 | //region ----------------------- ENTITY MANIPULATION METHODS ------------------------ 80 | 81 | public function validateAndSave(Contracts\DomainEntity $entity, ?array $attributes = null) { 82 | $this->clearErrors(); 83 | 84 | return $this->useTransactions ? $this->saveEntityUsingTransaction($entity, $runValidation = true, $attributes) : $this->saveEntityInternal($entity, $runValidation = true, $attributes); 85 | } 86 | 87 | public function saveWithoutValidation(Contracts\DomainEntity $entity, ?array $attributes = null) { 88 | $this->clearErrors(); 89 | 90 | return $this->useTransactions ? $this->saveEntityUsingTransaction($entity, $runValidation = false, $attributes) : $this->saveEntityInternal($entity, $runValidation = false, $attributes); 91 | } 92 | 93 | protected function saveEntityUsingTransaction(Contracts\DomainEntity $entity, bool $runValidation, ?array $attributes) { 94 | $this->beginTransaction(); 95 | $exception = null; 96 | try { 97 | $result = $this->saveEntityInternal($entity, $runValidation, $attributes); 98 | $result ? $this->commitTransaction() : null; 99 | } catch (\Exception $e) { 100 | $result = false; 101 | $exception = $e; 102 | $this->addError($e->getMessage()); 103 | } 104 | if (!$result) { 105 | $this->rollbackTransaction(); 106 | } 107 | if ($exception && $this->throwExceptions) { 108 | throw $e; 109 | } 110 | 111 | return $result; 112 | } 113 | 114 | /** 115 | * This method is called at the beginning of inserting or updating a record. 116 | * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is `true`, 117 | * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is `false`. 118 | * When overriding this method, make sure you call the parent implementation like the following: 119 | * 120 | * ```php 121 | * public function beforeSave($insert) 122 | * { 123 | * if (parent::beforeSave($insert)) { 124 | * // ...custom code here... 125 | * return true; 126 | * } else { 127 | * return false; 128 | * } 129 | * } 130 | * ``` 131 | * 132 | * @param boolean $insert whether this method called while inserting a record. 133 | * If `false`, it means the method is called while updating a record. 134 | * 135 | * @return boolean whether the insertion or updating should continue. 136 | * If `false`, the insertion or updating will be cancelled. 137 | */ 138 | protected function triggerModelEvent($eventName, $entity) { 139 | /** 140 | * @var domain\Base\ModelEvent $event 141 | */ 142 | $event = $this->container->create($this->modelEventClassName, [$entity]); 143 | $this->trigger($eventName, $event); 144 | 145 | return $event->isValid(); 146 | } 147 | 148 | /** 149 | * @return EntitiesProvider an instance of data provider. 150 | */ 151 | public function getEntitiesProvider() { 152 | return $this->container->create([ 153 | 'class' => $this->entitiesProviderClassName, 154 | 'query' => $this->createQuery(), 155 | 'repository' => $this, 156 | ]); 157 | } 158 | //endregion- 159 | 160 | //region ----------------------- SEARCH METHODS ------------------------------------- 161 | 162 | /** 163 | * @param mixed $pk primary key of the entity 164 | * 165 | * @return Domain\Base\Entity 166 | */ 167 | public function findOneWithPk($pk) { 168 | return $this->find()->oneWithPk($pk); 169 | } 170 | 171 | /** 172 | * @return Domain\Base\Entity[] 173 | */ 174 | public function findAll() { 175 | return $this->find()->all(); 176 | } 177 | 178 | /** 179 | * @param int $batchSize 180 | * 181 | * @return Domain\Base\Entity[] 182 | */ 183 | public function each($batchSize = 100) { 184 | return $this->find()->each($batchSize); 185 | } 186 | 187 | /** 188 | * @param int $batchSize 189 | * 190 | * @return Domain\Base\Entity[][] 191 | */ 192 | public function getBatchIterator($batchSize = 100) { 193 | return $this->find()->each($batchSize); 194 | } 195 | 196 | public function createQuery() { 197 | return $this->container->create($this->queryClassName, [$recordClass = $this->recordClassName]); 198 | } 199 | //endregion 200 | 201 | //region ----------------------- GETTERS/SETTERS ------------------------------------ 202 | 203 | /** 204 | * @return array 205 | */ 206 | public function getErrors(): array { 207 | return $this->errors; 208 | } 209 | 210 | /** 211 | * @param array $errors 212 | */ 213 | public function setErrors(array $errors): void { 214 | $this->errors = $errors; 215 | } 216 | 217 | /** 218 | * Adds error to the errors array 219 | * 220 | * @param $error 221 | */ 222 | public function addError($error): void { 223 | $this->errors[] = $error; 224 | } 225 | 226 | /** 227 | * Clears errors 228 | */ 229 | public function clearErrors(): void { 230 | $this->setErrors([]); 231 | } 232 | 233 | public function getDefaultQueryClassName() { 234 | return $this->_defaultQueryClassName; 235 | } 236 | 237 | public function setDefaultQueryClassName($defaultQueryClass) { 238 | if (!class_exists($defaultQueryClass) && !interface_exists($defaultQueryClass)) { 239 | throw new InvalidConfigException('Default query class should be an existing class or interface!'); 240 | } 241 | $this->_defaultQueryClassName = $defaultQueryClass; 242 | } 243 | 244 | public function getClassName() { 245 | if (null === $this->_className) { 246 | $this->_className = static::class; 247 | } 248 | 249 | return $this->_className; 250 | } 251 | 252 | public function setClassName($className) { 253 | $this->_className = $className; 254 | } 255 | 256 | public function getEntityClassName() { 257 | if (null === $this->_entityClassName) { 258 | $this->_entityClassName = $this->buildModelElementClassName('Entity'); 259 | } 260 | 261 | return $this->_entityClassName; 262 | } 263 | 264 | public function setEntityClassName($entityClassName) { 265 | $this->_entityClassName = $entityClassName; 266 | } 267 | 268 | public function getQueryClassName() { 269 | if (null === $this->_queryClassName) { 270 | $this->_queryClassName = $this->buildModelElementClassName('Query', $this->defaultQueryClassName); 271 | } 272 | 273 | return $this->_queryClassName; 274 | } 275 | 276 | public function setQueryClassName($queryClassName) { 277 | $this->_queryClassName = $queryClassName; 278 | } 279 | 280 | public function getRecordClassName() { 281 | if (null === $this->_recordClassName) { 282 | $this->_recordClassName = $this->buildModelElementClassName('Record'); 283 | } 284 | 285 | return $this->_recordClassName; 286 | } 287 | 288 | public function setRecordClassName($recordClassName) { 289 | $this->_recordClassName = $recordClassName; 290 | } 291 | 292 | protected function buildModelElementClassName($modelElement, $defaultClass = null) { 293 | $selfClassName = $this->className; 294 | $elementClassName = str_replace('Repository', $modelElement, $selfClassName); 295 | if (!class_exists($elementClassName) && !interface_exists($elementClassName)) { 296 | if ($defaultClass) { 297 | $elementClassName = $defaultClass; 298 | } else { 299 | throw new InvalidConfigException("{$modelElement} class should be an existing class or interface!"); 300 | } 301 | } 302 | 303 | return $elementClassName; 304 | } 305 | //endregion 306 | } -------------------------------------------------------------------------------- /src/DB/Condition.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Condition extends MagicObject { 14 | public function __call($name, $params) { 15 | if ($name === 'and') { 16 | $result = $this->callAnd(); 17 | } elseif ($name === 'or') { 18 | $result = $this->callAnd(); 19 | } else { 20 | $result = parent::__call($name, $params); 21 | } 22 | 23 | return $result; 24 | } 25 | } -------------------------------------------------------------------------------- /src/DB/EntitiesRepository.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class EntitiesRepository extends Base\Repository { 21 | /** 22 | * @var string data mapper class name. Required to map data from record to entity. Change it in {@link init()} method 23 | * if you need custom mapper. But be aware - data mapper is internal class and it is strongly advised to not 24 | * touch this property. 25 | */ 26 | public $dataMapperClassName = Domain\Base\DataMapper::class; 27 | /** 28 | * @var string indicates what finder to use. By default equal following template "{model name}Finder" where model name is equal to 29 | * the repository class name without "Repository" suffix. 30 | */ 31 | private $_finderClassName; 32 | /** 33 | * @var string entities finder class name. This class being used if no finder specified in morel directory. Change it 34 | * in {@link init()} method if you need custom default finder. 35 | */ 36 | private $_defaultFinderClassName = Finder::class; 37 | 38 | public function __construct($config = []) { 39 | $this->entitiesProviderClassName = EntitiesProvider::class; 40 | parent::__construct($config); 41 | } 42 | 43 | //region ---------------------- ENTITY MANIPULATION METHODS ------------------- 44 | 45 | /** 46 | * @param Contracts\DomainEntity $entity 47 | * @param bool $runValidation 48 | * @param array $attributes 49 | * 50 | * @return bool 51 | * @throws UnableToSaveEntityException 52 | */ 53 | protected function saveEntityInternal(Contracts\DomainEntity $entity, bool $runValidation, ?array $attributes): bool { 54 | $isEntityNew = $entity->isNew(); 55 | $dataSource = $entity->getDataMapper()->getDataSource(); 56 | 57 | if ($this->triggerModelEvent($isEntityNew ? self::EVENT_BEFORE_ADD : self::EVENT_BEFORE_UPDATE, $entity) && $this->triggerModelEvent(self::EVENT_BEFORE_SAVE, $entity)) { 58 | $result = $runValidation ? $dataSource->validateAndSave($attributes) : $dataSource->saveWithoutValidation($attributes); 59 | } else { 60 | $result = false; 61 | } 62 | if ($result) { 63 | $this->triggerModelEvent($isEntityNew ? self::EVENT_AFTER_ADD : self::EVENT_AFTER_UPDATE, $entity); 64 | $this->triggerModelEvent(self::EVENT_AFTER_SAVE, $entity); 65 | } else { 66 | $exception = new UnableToSaveEntityException('Failed to save entity ' . get_class($entity)); 67 | $exception->errorsList = $dataSource->getErrors(); 68 | throw $exception; 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | public function delete(Contracts\DomainEntity $entity): bool { 75 | if ($this->triggerModelEvent(self::EVENT_BEFORE_DELETE, $entity)) { 76 | $result = $entity->getDataMapper()->getDataSource()->deleteRecord(); 77 | } else { 78 | $result = false; 79 | } 80 | if ($result) { 81 | $this->triggerModelEvent(self::EVENT_AFTER_DELETE, $entity); 82 | } 83 | 84 | return $result; 85 | } 86 | 87 | public function validate(Contracts\DomainEntity $entity): bool { 88 | $dataSource = $entity->getDataMapper()->getDataSource(); 89 | 90 | return $dataSource->validate(); 91 | } 92 | 93 | public function refresh(Contracts\DomainEntity $entity): bool { 94 | return $entity->getDataMapper()->refresh(); 95 | } 96 | //endregion 97 | 98 | //region ----------------------- ENTITY DATA METHODS -------------------------- 99 | public function isNewOrJustAdded(Contracts\DomainEntity $entity): bool { 100 | return $entity->isNew() || $this->isJustAdded($entity); 101 | } 102 | 103 | public function isJustUpdated(Contracts\DomainEntity $entity): bool { 104 | return !$this->isJustAdded($entity); 105 | } 106 | 107 | public function isJustAdded(Contracts\DomainEntity $entity): bool { 108 | $dataSource = $entity->getDataMapper()->getDataSource(); 109 | 110 | return $dataSource->isJustAdded(); 111 | } 112 | 113 | public function getDirtyAttributes(Contracts\DomainEntity $entity, array $names = null): array { 114 | $dataSource = $entity->getDataMapper()->getDataSource(); 115 | 116 | return $dataSource->getDirtyAttributes($names); 117 | } 118 | 119 | public function getOldAttributes(Contracts\DomainEntity $entity): array { 120 | $dataSource = $entity->getDataMapper()->getDataSource(); 121 | 122 | return $dataSource->getOldAttributes(); 123 | } 124 | 125 | public function getOldAttribute(Contracts\DomainEntity $entity, string $name) { 126 | $dataSource = $entity->getDataMapper()->getDataSource(); 127 | 128 | return $dataSource->getOldAttribute($name); 129 | } 130 | 131 | public function isAttributeChanged(Contracts\DomainEntity $entity, string $name, bool $identical = true): bool { 132 | $dataSource = $entity->getDataMapper()->getDataSource(); 133 | 134 | return $dataSource->isAttributeChanged($name, $identical); 135 | } 136 | 137 | public function setChangedAttributes(Contracts\DomainEntity $entity, array $changedAttributes): void { 138 | $dataSource = $entity->getDataMapper()->getDataSource(); 139 | 140 | $dataSource->setChangedAttributes($changedAttributes); 141 | } 142 | 143 | public function getChangedAttributes(Contracts\DomainEntity $entity): array { 144 | $dataSource = $entity->getDataMapper()->getDataSource(); 145 | 146 | return $dataSource->getChangedAttributes(); 147 | } 148 | 149 | public function getChangedAttribute(Contracts\DomainEntity $entity, string $name) { 150 | $dataSource = $entity->getDataMapper()->getDataSource(); 151 | 152 | return $dataSource->getChangedAttribute($name); 153 | } 154 | 155 | /** 156 | * Method returns the result of checking whether the attribute was changed during 157 | * the saving of the entity. 158 | * Be aware! False positive possible because of Yii BaseActiveRecord::getDirtyAttributes() 159 | * method compares values with type matching 160 | * 161 | * @param Contracts\DomainEntity $entity 162 | * @param string $name 163 | * 164 | * @return bool 165 | */ 166 | public function wasAttributeChanged(Contracts\DomainEntity $entity, string $name): bool { 167 | $dataSource = $entity->getDataMapper()->getDataSource(); 168 | 169 | return $dataSource->wasAttributeChanged($name); 170 | } 171 | 172 | /** 173 | * Method returns the result of checking whether the attribute value was changed during 174 | * the saving of the entity. 175 | * Be aware! This method compare old value with new without type comparison. 176 | * 177 | * @param Contracts\DomainEntity $entity 178 | * @param string $name 179 | * 180 | * @return bool 181 | */ 182 | public function wasAttributeValueChanged(Contracts\DomainEntity $entity, string $name): bool { 183 | return $this->getChangedAttribute($entity, $name) != $entity->{$name}; 184 | } 185 | //endregion 186 | 187 | //region ----------------------- INSTANTIATION METHODS ------------------------ 188 | public function createNewEntity() { 189 | $container = $this->container; 190 | 191 | return $container->create([ 192 | 'class' => $this->entityClassName, 193 | 'dataMapper' => $container->create($this->dataMapperClassName, [$this->createRecord()]), 194 | ]); 195 | } 196 | 197 | private function createRecord() { 198 | return $this->container->create($this->recordClassName); 199 | } 200 | 201 | public function createEntityFromSource(Contracts\EntityDataSource $record) { 202 | $container = $this->container; 203 | 204 | return $container->create([ 205 | 'class' => $this->entityClassName, 206 | 'dataMapper' => $container->create($this->dataMapperClassName, [$record]), 207 | ]); 208 | } 209 | //endregion 210 | 211 | //region ----------------------- SEARCH METHODS ------------------------------- 212 | /** 213 | * @return Finder|RecordQuery 214 | */ 215 | public function find() { 216 | return $this->createFinder(); 217 | } 218 | 219 | protected function createFinder() { 220 | return $this->container->create($this->finderClassName, [ 221 | $query = $this->createQuery(), 222 | $repository = $this, 223 | ]); 224 | } 225 | //endregion 226 | 227 | //region ----------------------- GETTERS/SETTERS ------------------------------ 228 | protected function getFinderClassName() { 229 | if (null === $this->_finderClassName) { 230 | $this->_finderClassName = $this->buildModelElementClassName('Finder', $this->defaultFinderClassName); 231 | } 232 | 233 | return $this->_finderClassName; 234 | } 235 | 236 | public function setFinderClassName($finderClassName): void { 237 | $this->_finderClassName = $finderClassName; 238 | } 239 | 240 | public function getDefaultFinderClassName(): string { 241 | return $this->_defaultFinderClassName; 242 | } 243 | 244 | public function setDefaultFinderClassName($defaultFinderClass): void { 245 | if (!class_exists($defaultFinderClass) && !interface_exists($defaultFinderClass)) { 246 | throw new InvalidConfigException('Default finder class should be an existing class or interface!'); 247 | } 248 | $this->_defaultFinderClassName = $defaultFinderClass; 249 | } 250 | //endregion 251 | } -------------------------------------------------------------------------------- /src/DB/Finder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Finder extends MagicObject { 15 | /** 16 | * @var RecordQuery 17 | */ 18 | private $_query; 19 | /** 20 | * @var EntitiesRepository 21 | */ 22 | private $_repository; 23 | 24 | public function __construct(Contracts\Specification $query, Contracts\Repository $repository, $config = []) { 25 | $this->_query = $query; 26 | $this->_repository = $repository; 27 | parent::__construct($config); 28 | } 29 | 30 | public function asArray() { 31 | return $this->getQuery()->asArray(); 32 | } 33 | 34 | public function all() { 35 | $queryResult = $this->getQuery()->all(); 36 | $entities = []; 37 | foreach ($queryResult as $key => $record) { 38 | $entities[$key] = $this->createEntityFromRecord($record); 39 | } 40 | 41 | return $entities; 42 | } 43 | 44 | public function one() { 45 | $queryResult = $this->getQuery()->one(); 46 | 47 | return $this->createEntityFromRecord($queryResult); 48 | } 49 | 50 | public function oneWithPk($pk) { 51 | $queryResult = $this->getQuery()->oneWithPk($pk); 52 | 53 | return $this->createEntityFromRecord($queryResult); 54 | } 55 | 56 | public function batch($batchSize = 100) { 57 | $iterator = $this->getQuery()->batch($batchSize); 58 | 59 | return $this->container->create(SearchResult::class, [$iterator, $this->getRepository()]); 60 | } 61 | 62 | public function each($batchSize = 100) { 63 | $iterator = $this->getQuery()->each($batchSize); 64 | 65 | return $this->container->create(SearchResult::class, [$iterator, $this->getRepository()]); 66 | } 67 | 68 | protected function createEntityFromRecord($record) { 69 | if ($record instanceof Contracts\Record) { 70 | $entity = $this->getRepository()->createEntityFromSource($record); 71 | } else { 72 | $entity = $record; 73 | } 74 | 75 | return $entity; 76 | } 77 | 78 | public function __call($name, $params) { 79 | $query = $this->getQuery(); 80 | if ($query->hasMethod($name)) { 81 | $result = call_user_func_array([$query, $name], $params); 82 | $queryClassName = get_class($query); 83 | if (is_object($result) && is_a($result, $queryClassName)) { 84 | $result = $this; 85 | } 86 | } else { 87 | $result = parent::__call($name, $params); 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | public function getQuery() { 94 | return $this->_query; 95 | } 96 | 97 | protected function getRepository() { 98 | return $this->_repository; 99 | } 100 | } -------------------------------------------------------------------------------- /src/DB/Mixins/EntityRecovering.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | trait EntityRecovering { 14 | /** 15 | * @param DomainEntity $entity 16 | * 17 | * @return bool result. 18 | */ 19 | public function recover(DomainEntity $entity) { 20 | $result = false; 21 | if ($this->triggerModelEvent(self::EVENT_BEFORE_DELETE, $entity)) { 22 | $dataSource = $entity->getDataMapper()->getDataSource(); 23 | if ($dataSource->hasMethod('restore')) { 24 | $result = $dataSource->restore(); 25 | } 26 | } 27 | if ($result) { 28 | $this->triggerModelEvent(self::EVENT_AFTER_DELETE, $entity); 29 | } 30 | 31 | return $result; 32 | } 33 | } -------------------------------------------------------------------------------- /src/DB/Mixins/QueryConditionBuilderAccess.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | trait QueryConditionBuilderAccess { 18 | protected $conditionBuilderClassName = QueryConditionBuilder::class; 19 | private $_conditionBuilder; 20 | 21 | /** 22 | * Alias of {@link QueryConditionBuilder::buildAliasedNameOfField} 23 | * 24 | * @param string $field field name. 25 | * @param null $alias optional alias. If not used query alias will be used. 26 | * 27 | * @return string 28 | */ 29 | public function buildAliasedNameOfField($field, $alias = null) { 30 | return $this->conditionBuilder->buildAliasedNameOfField($field, $alias); 31 | } 32 | 33 | public function buildAliasedNameOfParam($param, $alias = null) { 34 | return $this->conditionBuilder->buildAliasedNameOfParam($param, $alias); 35 | } 36 | 37 | protected function getConditionBuilder() { 38 | if (null === $this->_conditionBuilder) { 39 | $this->_conditionBuilder = $this->container->create($this->conditionBuilderClassName, [$query = $this]); 40 | } 41 | 42 | return $this->_conditionBuilder; 43 | } 44 | } -------------------------------------------------------------------------------- /src/DB/Mixins/RecordQueryFunctions.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | trait RecordQueryFunctions { 18 | public $primaryKeyName = 'id'; 19 | private $_alias; 20 | private $_mainTableName; 21 | //region ------------------- SEARCH METHODS -------------------- 22 | 23 | /** 24 | * Method designed to make chain of query methods more accurate if query used as a stored object and not as a part 25 | * of active record. 26 | * Example: 27 | *
28 |      * $finder = new ActiveQuery();
29 |      *     $resultSet = $finder->find()
30 |      *       ->active()
31 |      *       ->withSomeRelation()
32 |      *       ->all();
33 |      *     $record = $finder->find()->one();
34 |      * 
35 | * 36 | * @return $this 37 | */ 38 | public function find() { 39 | $clone = clone $this; 40 | foreach ($this->getBehaviors() as $name => $behavior) { 41 | $clone->attachBehavior($name, clone $behavior); 42 | } 43 | 44 | return $clone; 45 | } 46 | 47 | /** 48 | * @param $pk 49 | * 50 | * @return \PHPKitchen\Domain\DB\Record|array|null 51 | */ 52 | public function oneWithPk($pk) { 53 | $pkParam = $this->buildAliasedNameOfParam('pk'); 54 | $primaryKey = $this->buildAliasedNameOfField($this->primaryKeyName); 55 | $this->andWhere("{$primaryKey}={$pkParam}", [$pkParam => $pk]); 56 | 57 | return $this->one(); 58 | } 59 | 60 | /** 61 | * @override 62 | * @inheritdoc 63 | */ 64 | public function alias($alias) { 65 | $this->_alias = $alias; 66 | 67 | return parent::alias($alias); 68 | } 69 | 70 | public function bySearchModel($model) { 71 | return $this; 72 | } 73 | 74 | //endregion 75 | 76 | //region ------------------- GETTERS/SETTERS ------------------- 77 | 78 | public function getMainTableName() { 79 | if ($this->_mainTableName == null) { 80 | $method = new \ReflectionMethod($this->modelClass, 'tableName'); 81 | $this->_mainTableName = $method->invoke(null); 82 | } 83 | 84 | return $this->_mainTableName; 85 | } 86 | 87 | public function setMainTableName($mainTableName) { 88 | $this->_mainTableName = $mainTableName; 89 | } 90 | 91 | public function getAlias() { 92 | if ($this->_alias === null) { 93 | $this->_alias = $this->getMainTableName(); 94 | } 95 | 96 | return $this->_alias; 97 | } 98 | //endregion 99 | } -------------------------------------------------------------------------------- /src/DB/QueryConditionBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class QueryConditionBuilder extends MagicObject { 15 | /** 16 | * @var RecordQuery 17 | */ 18 | protected $query; 19 | private $_paramNamesCounters = []; 20 | 21 | public function __construct(Contracts\RecordQuery $query, $config = []) { 22 | $this->query = $query; 23 | parent::__construct($config); 24 | } 25 | 26 | public function buildAliasedNameOfField($field, $alias = null) { 27 | $alias = $alias ? $alias : $this->query->alias; 28 | 29 | return "[[$alias]].[[$field]]"; 30 | } 31 | 32 | public function buildAliasedNameOfParam($param, $alias = null) { 33 | $alias = $alias ? $alias : $this->query->alias; 34 | $paramName = ":{$alias}_{$param}"; 35 | if ($this->isParamNameUsed($paramName)) { 36 | $index = $this->getParamNameNextIndexAndIncreaseCurrent($paramName); 37 | $paramName = "{$paramName}_{$index}"; 38 | } else { 39 | $this->addParamNameToUsed($paramName); 40 | } 41 | 42 | return $paramName; 43 | } 44 | 45 | protected function isParamNameUsed($paramName) { 46 | return isset($this->_paramNamesCounters[$paramName]); 47 | } 48 | 49 | protected function addParamNameToUsed($paramName) { 50 | $this->_paramNamesCounters[$paramName] = 0; 51 | } 52 | 53 | protected function getParamNameNextIndexAndIncreaseCurrent($paramName) { 54 | $this->_paramNamesCounters[$paramName]++; 55 | 56 | return $this->_paramNamesCounters[$paramName]; 57 | } 58 | } -------------------------------------------------------------------------------- /src/DB/Record.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Record extends ActiveRecord implements Contracts\Record, ContainerAware, ServiceLocatorAware, LoggerAware, EntityDataSource { 25 | use LoggerAccess; 26 | use ServiceLocatorAccess; 27 | use ContainerAccess; 28 | use StaticSelfAccess; 29 | /** 30 | * @var bool flag that record was just inserted 31 | */ 32 | private $justAdded = false; 33 | /** 34 | * @var array attribute values that were changed after inser or update 35 | */ 36 | private $_changedAttributes = []; 37 | /** 38 | * @var EntitiesRepository[] repositories of related entities 39 | */ 40 | private $relatedRepositories = []; 41 | 42 | public function init() { 43 | parent::init(); 44 | 45 | $this->on(static::EVENT_BEFORE_INSERT, [$this, 'markAsJustAdded']); 46 | $this->on(static::EVENT_BEFORE_UPDATE, [$this, 'markAsJustUpdated']); 47 | $this->on(static::EVENT_AFTER_INSERT, [$this, 'initChangedAttributes']); 48 | $this->on(static::EVENT_AFTER_UPDATE, [$this, 'initChangedAttributes']); 49 | } 50 | 51 | /** 52 | * @override 53 | * @inheritdoc 54 | */ 55 | public static function instantiate($row) { 56 | return \Yii::$container->create(static::class); 57 | } 58 | 59 | /** 60 | * @override 61 | * @inheritdoc 62 | * @return RecordQuery the newly created query instance. 63 | */ 64 | public static function find() { 65 | return static::getInstance()->createQuery(RecordQuery::class); 66 | } 67 | 68 | /** 69 | * @param $class 70 | * 71 | * @return RecordQuery 72 | */ 73 | public function createQuery($class) { 74 | /** 75 | * @var RecordQuery $finder 76 | */ 77 | $finder = $this->getContainer()->create($class, [static::class]); 78 | $finder->setMainTableName(static::tableName()); 79 | 80 | return $finder; 81 | } 82 | 83 | public function isNew() { 84 | return $this->isNewRecord; 85 | } 86 | 87 | public function isNotNew() { 88 | return !$this->isNewRecord; 89 | } 90 | 91 | public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) { 92 | return $this->hasAttribute($name) || parent::canGetProperty($name, $checkVars, $checkBehaviors); 93 | } 94 | 95 | public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) { 96 | return $this->hasAttribute($name) || parent::canSetProperty($name, $checkVars, $checkBehaviors); 97 | } 98 | 99 | public function setChangedAttributes(array $changedAttributes) { 100 | $this->_changedAttributes = $changedAttributes; 101 | } 102 | 103 | public function getChangedAttributes() { 104 | return $this->_changedAttributes; 105 | } 106 | 107 | public function getChangedAttribute($name) { 108 | if ($this->wasAttributeChanged($name)) { 109 | return $this->_changedAttributes[$name]; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | public function wasAttributeChanged($name) { 116 | return (array_key_exists($name, $this->_changedAttributes)); 117 | } 118 | 119 | public function isJustAdded() { 120 | return $this->justAdded; 121 | } 122 | 123 | /** 124 | * Saves the current record. 125 | * 126 | * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] 127 | * when [[isNewRecord]] is false. 128 | * 129 | * For example, to save a customer record: 130 | * 131 | * ```php 132 | * $customer = new Customer; // or $customer = Customer::findOne($id); 133 | * $customer->name = $name; 134 | * $customer->email = $email; 135 | * $customer->save(); 136 | * ``` 137 | * 138 | * before saving the record. Defaults to `true`. If the validation fails, the record 139 | * will not be saved to the database and this method will return `false`. 140 | * 141 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, 142 | * meaning all attributes that are loaded from DB will be saved. 143 | * 144 | * @return boolean whether the saving succeeded (i.e. no validation errors occurred). 145 | */ 146 | public function validateAndSave($attributeNames = null) { 147 | if ($this->getIsNewRecord()) { 148 | return $this->insert($runValidation = true, $attributeNames); 149 | } else { 150 | return $this->update($runValidation = true, $attributeNames) !== false; 151 | } 152 | } 153 | 154 | /** 155 | * Saves the current record. 156 | * 157 | * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] 158 | * when [[isNewRecord]] is false. 159 | * 160 | * For example, to save a customer record: 161 | * 162 | * ```php 163 | * $customer = new Customer; // or $customer = Customer::findOne($id); 164 | * $customer->name = $name; 165 | * $customer->email = $email; 166 | * $customer->save(); 167 | * ``` 168 | * 169 | * before saving the record. Defaults to `true`. If the validation fails, the record 170 | * will not be saved to the database and this method will return `false`. 171 | * 172 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, 173 | * meaning all attributes that are loaded from DB will be saved. 174 | * 175 | * @return boolean whether the saving succeeded (i.e. no validation errors occurred). 176 | */ 177 | public function saveWithoutValidation($attributeNames = null) { 178 | if ($this->getIsNewRecord()) { 179 | return $this->insert($runValidation = false, $attributeNames); 180 | } else { 181 | return $this->update($runValidation = false, $attributeNames) !== false; 182 | } 183 | } 184 | 185 | /** 186 | * Deletes the table row corresponding to this active record. 187 | * 188 | * This method performs the following steps in order: 189 | * 190 | * 1. call [[beforeDelete()]]. If the method returns false, it will skip the 191 | * rest of the steps; 192 | * 2. delete the record from the database; 193 | * 3. call [[afterDelete()]]. 194 | * 195 | * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] 196 | * will be raised by the corresponding methods. 197 | * 198 | * @return integer|false the number of rows deleted, or false if the deletion is unsuccessful for some reason. 199 | * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. 200 | * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data 201 | * being deleted is outdated. 202 | * @throws Exception in case delete failed. 203 | */ 204 | public function deleteRecord() { 205 | return parent::delete(); 206 | } 207 | 208 | protected function markAsJustAdded() { 209 | $this->justAdded = true; 210 | } 211 | 212 | protected function markAsJustUpdated() { 213 | $this->justAdded = false; 214 | } 215 | 216 | protected function initChangedAttributes(AfterSaveEvent $event) { 217 | $this->setChangedAttributes($event->changedAttributes); 218 | } 219 | 220 | /** 221 | * Creates a query instance for `has-one` or `has-many` relation. 222 | * 223 | * @param string $class the class name of the related record. 224 | * @param array $link the primary-foreign key constraint. 225 | * @param bool $multiple whether this query represents a relation to more than one record. 226 | * 227 | * @return ActiveQueryInterface the relational query object. 228 | */ 229 | protected function createRelationQuery($class, $link, $multiple) { 230 | $repository = $this->getRelatedRepository($byRecordClass = $class); 231 | if (!$repository) { 232 | return parent::createRelationQuery($class, $link, $multiple); 233 | } 234 | 235 | $query = $this->getQueryFromRepository($repository); 236 | 237 | $query->primaryModel = $this; 238 | $query->link = $link; 239 | $query->multiple = $multiple; 240 | 241 | return $query; 242 | } 243 | 244 | /** 245 | * @deprecated this method should use only for domestic purposes 246 | * 247 | * @param EntitiesRepository $repository 248 | * 249 | * @return ActiveQueryInterface 250 | */ 251 | protected function getQueryFromRepository($repository) { 252 | return $repository->find()->getQuery(); 253 | } 254 | 255 | /** 256 | * @param string $byRecordClass 257 | * 258 | * @return false|EntitiesRepository 259 | */ 260 | protected function getRelatedRepository(string $byRecordClass) { 261 | return $this->relatedRepositories[$byRecordClass] ?? $this->initRelatedRepository($byRecordClass); 262 | } 263 | 264 | protected function initRelatedRepository(string $byRecordClass) { 265 | $repositoryClass = false !== strpos($byRecordClass, 'Record') ? str_replace('Record', 'Repository', $byRecordClass) : null; 266 | $container = $this->container; 267 | try { 268 | $repository = $repositoryClass ? $container->create($repositoryClass) : false; 269 | } catch (\Exception $e) { 270 | $repository = false; 271 | } 272 | $this->relatedRepositories[$byRecordClass] = $repository; 273 | 274 | return $this->relatedRepositories[$byRecordClass]; 275 | } 276 | } -------------------------------------------------------------------------------- /src/DB/RecordQuery.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RecordQuery extends ActiveQuery implements Contracts\Specification, Contracts\RecordQuery { 19 | use QueryConditionBuilderAccess; 20 | use RecordQueryFunctions; 21 | use ContainerAccess; 22 | use ServiceLocatorAccess; 23 | } -------------------------------------------------------------------------------- /src/DB/RecordsRepository.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class RecordsRepository extends Base\Repository { 15 | public function __construct($config = []) { 16 | $this->entitiesProviderClassName = Domain\Data\RecordsProvider::class; 17 | parent::__construct($config); 18 | } 19 | 20 | //----------------------- ENTITY MANIPULATION METHODS -----------------------// 21 | 22 | /** 23 | * @param Record|Contracts\DomainEntity $entity 24 | * @param bool $runValidation 25 | * @param array $attributes 26 | * 27 | * @return bool result. 28 | * @throws Domain\Exceptions\UnableToSaveEntityException 29 | */ 30 | protected function saveEntityInternal(Contracts\DomainEntity $entity, bool $runValidation, ?array $attributes): bool { 31 | $isEntityNew = $entity->isNew(); 32 | if ($this->triggerModelEvent($isEntityNew ? self::EVENT_BEFORE_ADD : self::EVENT_BEFORE_UPDATE, $entity) && $this->triggerModelEvent(self::EVENT_BEFORE_SAVE, $entity)) { 33 | $result = $runValidation ? $entity->validateAndSave($attributes) : $entity->saveWithoutValidation($attributes); 34 | } else { 35 | $result = false; 36 | } 37 | if ($result) { 38 | $this->triggerModelEvent($isEntityNew ? self::EVENT_BEFORE_ADD : self::EVENT_AFTER_UPDATE, $entity); 39 | $this->triggerModelEvent(self::EVENT_AFTER_SAVE, $entity); 40 | } else { 41 | $exception = new Domain\Exceptions\UnableToSaveEntityException('Failed to save entity ' . get_class($entity)); 42 | $exception->errorsList = $entity->getErrors(); 43 | throw $exception; 44 | } 45 | 46 | return $result; 47 | } 48 | 49 | /** 50 | * @param Record|Contracts\DomainEntity $entity 51 | * 52 | * @return bool result. 53 | */ 54 | public function delete(Contracts\DomainEntity $entity): bool { 55 | if ($this->triggerModelEvent(self::EVENT_BEFORE_DELETE, $entity)) { 56 | $result = $entity->deleteRecord(); 57 | } else { 58 | $result = false; 59 | } 60 | if ($result) { 61 | $this->triggerModelEvent(self::EVENT_AFTER_DELETE, $entity); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | /** 68 | * @param Record|Contracts\DomainEntity $entity 69 | * 70 | * @return bool result. 71 | */ 72 | public function validate(Contracts\DomainEntity $entity): bool { 73 | return $entity->validate(); 74 | } 75 | 76 | //----------------------- INSTANTIATION METHODS -----------------------// 77 | 78 | public function createNewEntity() { 79 | return $this->container->create([ 80 | 'class' => $this->entityClassName, 81 | ]); 82 | } 83 | 84 | //----------------------- SEARCH METHODS -----------------------// 85 | 86 | /** 87 | * @return RecordQuery 88 | */ 89 | public function find() { 90 | return $this->createQuery(); 91 | } 92 | 93 | //----------------------- GETTERS/SETTERS -----------------------// 94 | 95 | public function getRecordClassName() { 96 | return $this->getEntityClassName(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/DB/SearchResult.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class SearchResult extends MagicObject implements \Iterator { 16 | private $_queryResultIterator; 17 | /** 18 | * @var Base\Repository|Contracts\Repository 19 | */ 20 | private $_repository; 21 | 22 | public function __construct(BatchQueryResult $queryResult, Contracts\Repository $repository, $config = []) { 23 | $this->_queryResultIterator = $queryResult; 24 | $this->setRepository($repository); 25 | parent::__construct($config); 26 | } 27 | 28 | public function current() { 29 | $iterator = $this->getQueryResultIterator(); 30 | $value = $iterator->current(); 31 | if ($iterator->each && $value instanceof Contracts\Record) { 32 | $entity = $this->getRepository()->createEntityFromSource($value); 33 | } elseif (!$iterator->each) { 34 | foreach ($value as $record) { 35 | $entity[] = $this->getRepository()->createEntityFromSource($record); 36 | } 37 | } else { 38 | $entity = null; 39 | } 40 | 41 | return $entity; 42 | } 43 | 44 | public function next() { 45 | $this->getQueryResultIterator()->next(); 46 | } 47 | 48 | public function key() { 49 | return $this->getQueryResultIterator()->key(); 50 | } 51 | 52 | public function valid() { 53 | return $this->getQueryResultIterator()->valid(); 54 | } 55 | 56 | public function rewind() { 57 | $this->getQueryResultIterator()->rewind(); 58 | } 59 | 60 | protected function getQueryResultIterator() { 61 | return $this->_queryResultIterator; 62 | } 63 | 64 | public function getRepository() { 65 | return $this->_repository; 66 | } 67 | 68 | public function setRepository($repository) { 69 | $this->_repository = $repository; 70 | } 71 | } -------------------------------------------------------------------------------- /src/Data/EntitiesProvider.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class EntitiesProvider extends RecordsProvider { 17 | protected function prepareModels() { 18 | $result = parent::prepareModels(); 19 | if (isset($result[0]) && is_object($result[0]) && $result[0] instanceof Record) { 20 | $repository = $this->repository; 21 | foreach ($result as $key => $record) { 22 | $newResult[$key] = $repository->createEntityFromSource($record); 23 | } 24 | } else { 25 | $newResult = &$result; 26 | } 27 | 28 | return $newResult; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Data/RecordsProvider.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RecordsProvider extends ActiveDataProvider { 18 | /** 19 | * @var \PHPKitchen\Domain\DB\EntitiesRepository|\PHPKitchen\Domain\DB\RecordsRepository 20 | */ 21 | protected $_repository; 22 | 23 | public function getRepository() { 24 | return $this->_repository; 25 | } 26 | 27 | public function setRepository(Contracts\Repository $repository) { 28 | $this->_repository = $repository; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Exceptions/UnableToSaveEntityException.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UnableToSaveEntityException extends \Exception { 12 | public $errorsList = []; 13 | } -------------------------------------------------------------------------------- /src/Log/Logger.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Logger extends BaseLogger { 17 | public $defaultTraceLevel = 7; 18 | 19 | public function logWithTrace($message, $level, $category = 'application') { 20 | if (!$this->traceLevel && $this->defaultTraceLevel) { 21 | $oldTraceLevel = $this->traceLevel; 22 | $this->traceLevel = $this->defaultTraceLevel; 23 | parent::log($message, $level, $category); 24 | $this->traceLevel = $oldTraceLevel; 25 | } else { 26 | parent::log($message, $level, $category); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Mixins/LoggerAccess.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | trait LoggerAccess { 14 | /** 15 | * @var \yii\log\Logger logger component. 16 | */ 17 | protected static $_logger; 18 | 19 | /** 20 | * Logs a message with the given type and category. 21 | * If [[traceLevel]] is greater than 0, additional call stack information about 22 | * the application code will be logged as well. 23 | * 24 | * @param string|array $message the message to be logged. This can be a simple string or a more 25 | * complex data structure that will be handled by a [[Target|log target]]. 26 | * @param integer $level the level of the message. This must be one of the following: 27 | * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, 28 | * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`. 29 | * @param string $category the category of the message. 30 | */ 31 | public function log($message, $level, $category = '') { 32 | if (empty($category)) { 33 | $category = static::class; 34 | } 35 | static::getLogger()->log($message, $level, $category); 36 | } 37 | 38 | /** 39 | * Logs an informative message. 40 | * An informative message is typically logged by an application to keep record of 41 | * something important (e.g. an administrator logs in). 42 | * 43 | * @param string $message the message to be logged. 44 | * @param string $category the category of the message. 45 | */ 46 | public function logInfo($message, $category = '') { 47 | static::log($message, \yii\log\Logger::LEVEL_INFO, $category); 48 | } 49 | 50 | /** 51 | * Logs a warning message. 52 | * A warning message is typically logged when an error occurs while the execution 53 | * can still continue. 54 | * 55 | * @param string $message the message to be logged. 56 | * @param string $category the category of the message. 57 | */ 58 | public function logWarning($message, $category = '') { 59 | static::log($message, \yii\log\Logger::LEVEL_WARNING, $category); 60 | } 61 | 62 | /** 63 | * Logs an error message. 64 | * An error message is typically logged when an unrecoverable error occurs 65 | * during the execution of an application. 66 | * 67 | * @param string $message the message to be logged. 68 | * @param string $category the category of the message. 69 | */ 70 | public function logError($message, $category = '') { 71 | static::log($message, \yii\log\Logger::LEVEL_ERROR, $category); 72 | } 73 | 74 | /** 75 | * Marks the beginning of a code block for profiling. 76 | * This has to be matched with a call to [[endProfile]] with the same category name. 77 | * The begin- and end- calls must also be properly nested. For example, 78 | * 79 | * ```php 80 | * $this->beginProfile('block1'); 81 | * // some code to be profiled 82 | * $this->beginProfile('block2'); 83 | * // some other code to be profiled 84 | * $this->endProfile('block2'); 85 | * $this->endProfile('block1'); 86 | * ``` 87 | * 88 | * @param string $token token for the code block 89 | * @param string $category the category of this log message 90 | * 91 | * @see endProfile() 92 | */ 93 | public function beginProfile($token, $category = '') { 94 | static::getLogger()->log($token, \yii\log\Logger::LEVEL_PROFILE_BEGIN, $category); 95 | } 96 | 97 | /** 98 | * Marks the end of a code block for profiling. 99 | * This has to be matched with a previous call to [[beginProfile]] with the same category name. 100 | * 101 | * @param string $token token for the code block 102 | * @param string $category the category of this log message 103 | * 104 | * @see beginProfile() 105 | */ 106 | public function endProfile($token, $category = '') { 107 | static::getLogger()->log($token, \yii\log\Logger::LEVEL_PROFILE_END, $category); 108 | } 109 | 110 | /** 111 | * Logs a trace message. 112 | * Trace messages are logged mainly for development purpose to see 113 | * the execution work flow of some code. 114 | * 115 | * @param string $message the message to be logged. 116 | * @param string $category the category of the message. 117 | */ 118 | public function trace($message, $category = '') { 119 | static::getLogger()->log($message, \yii\log\Logger::LEVEL_TRACE, $category); 120 | } 121 | 122 | /** 123 | * @return \yii\log\Logger 124 | */ 125 | protected function getLogger() { 126 | if (!isset(static::$_logger)) { 127 | static::$_logger = \Yii::$app->log->getLogger(); 128 | } 129 | 130 | return static::$_logger; 131 | } 132 | } -------------------------------------------------------------------------------- /src/Mixins/StaticSelfAccess.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | trait StaticSelfAccess { 12 | /** 13 | * @return \PHPKitchen\Domain\Base\Component[] 14 | */ 15 | protected static $_instances = []; 16 | 17 | /** 18 | * @return $this 19 | */ 20 | public static function getInstance() { 21 | if (!isset(static::$_instances[static::class])) { 22 | static::initializeInstance(); 23 | } 24 | 25 | return static::$_instances[static::class]; 26 | } 27 | 28 | protected static function initializeInstance() { 29 | static::$_instances[static::class] = \Yii::$container->create(static::class); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Mixins/StrategiesComposingAlgorithm.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait StrategiesComposingAlgorithm { 13 | /** 14 | * @var \PHPKitchen\Domain\Base\Strategy[]|array 15 | */ 16 | private $_chainedStrategies; 17 | 18 | public function executeCallAction() { 19 | $chainedStrategies = $this->getChainedStrategies(); 20 | $container = $this->container; 21 | foreach ($chainedStrategies as $key => $chainedStrategy) { 22 | if (!is_object($chainedStrategy)) { 23 | $chainedStrategy = $container->create($chainedStrategy, $this->getStrategyConstructorArguments()); 24 | } 25 | $chainedStrategy->call(); 26 | } 27 | } 28 | 29 | protected function getStrategyConstructorArguments() { 30 | return []; 31 | } 32 | 33 | public function getChainedStrategies() { 34 | return $this->_chainedStrategies; 35 | } 36 | 37 | public function setChainedStrategies($chainedStrategies) { 38 | $this->_chainedStrategies = $chainedStrategies; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Mixins/TransactionAccess.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | trait TransactionAccess { 17 | /** 18 | * @var \yii\db\Transaction 19 | */ 20 | protected $_transaction; 21 | 22 | protected function beginTransaction() { 23 | if (null === $this->_transaction) { 24 | $this->_transaction = $this->serviceLocator->db->beginTransaction(); 25 | } else { 26 | throw new InvalidCallException('Transaction already started, unable to start another transaction in class ' . static::class); 27 | } 28 | } 29 | 30 | protected function commitTransaction() { 31 | if (null === $this->_transaction) { 32 | throw new InvalidCallException('Transaction should be started before committing in class ' . static::class); 33 | } 34 | $this->_transaction->commit(); 35 | $this->clearTransaction(); 36 | } 37 | 38 | protected function rollbackTransaction() { 39 | if (null === $this->_transaction) { 40 | throw new InvalidCallException('Transaction should be started before rolling back in class ' . static::class); 41 | } 42 | $this->_transaction->rollBack(); 43 | $this->clearTransaction(); 44 | } 45 | 46 | protected function clearTransaction() { 47 | $this->_transaction = null; 48 | } 49 | 50 | /** 51 | * Allows to wrap method of a class by transaction. 52 | * 53 | * @param string $methodName class method name that should be wrapped by transaction. 54 | * @param array ...$methodArguments [optional] method arguments. 55 | * 56 | * @return bool|mixed returns method result or false if transaction failed. 57 | */ 58 | protected function callTransactionalMethod($methodName, ...$methodArguments) { 59 | $this->beginTransaction(); 60 | try { 61 | $result = call_user_func_array([$this, $methodName], $methodArguments); 62 | $this->commitTransaction(); 63 | } catch (\Exception $e) { 64 | $result = false; 65 | $this->rollbackTransaction(); 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | * Wraps passed callback in transaction. 73 | * 74 | * @param callable $callback valid callback to be wrapped by transaction. 75 | * 76 | * @return bool|mixed returns callback result or false if transaction failed. 77 | */ 78 | protected function callInTransaction(callable $callback) { 79 | $this->beginTransaction(); 80 | try { 81 | $result = $callback(); 82 | $this->commitTransaction(); 83 | } catch (\Exception $e) { 84 | $result = false; 85 | $this->rollbackTransaction(); 86 | } 87 | 88 | return $result; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Web/Actions/AddEntity.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AddEntity extends EntityModificationAction { 14 | public function init() { 15 | $this->setViewFileIfNotSetTo('add'); 16 | } 17 | 18 | public function run() { 19 | return $this->loadModelAndSaveOrPrintView(); 20 | } 21 | 22 | protected function initModel() { 23 | $this->_model = $this->createNewModel(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Web/Actions/CallableAction.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class CallableAction extends Action { 20 | private $_callable; 21 | 22 | protected function runCallable() { 23 | return call_user_func($this->callable, $this); 24 | } 25 | 26 | public function getCallable(): callable { 27 | return $this->_callable; 28 | } 29 | 30 | public function setCallable(callable $callable): void { 31 | $this->_callable = $callable; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Web/Actions/DeleteEntity.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DeleteEntity extends Action { 16 | use ModelSearching; 17 | public $failToDeleteErrorFlashMessage = 'Unable to delete entity'; 18 | public $successfulDeleteFlashMessage = 'Entity successfully deleted'; 19 | public $redirectUrl; 20 | 21 | public function init() { 22 | $this->setViewFileIfNotSetTo('list'); 23 | } 24 | 25 | /** 26 | * @param int $id 27 | * 28 | * @return \yii\web\Response 29 | * @throws \yii\web\NotFoundHttpException 30 | */ 31 | public function run($id) { 32 | $entity = $this->findEntityByIdentifierOrFail($id); 33 | $this->tryToDeleteEntity($entity); 34 | 35 | return $this->redirectToNextPage(); 36 | } 37 | 38 | protected function tryToDeleteEntity($entity) { 39 | try { 40 | $savedSuccessfully = $this->getRepository()->delete($entity); 41 | } catch (UnableToSaveEntityException $e) { 42 | $savedSuccessfully = false; 43 | } 44 | if ($savedSuccessfully) { 45 | $this->addSuccessFlash($this->successfulDeleteFlashMessage); 46 | } else { 47 | $this->addErrorFlash($this->failToDeleteErrorFlashMessage); 48 | } 49 | } 50 | 51 | protected function redirectToNextPage() { 52 | if (null === $this->redirectUrl) { 53 | $redirectUrl = ['list']; 54 | } elseif (is_callable($this->redirectUrl)) { 55 | $redirectUrl = call_user_func($this->redirectUrl, $this); 56 | } else { 57 | $redirectUrl = $this->redirectUrl; 58 | } 59 | 60 | return $this->controller->redirect($redirectUrl); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Web/Actions/EditEntity.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class EditEntity extends EntityModificationAction { 14 | protected $entityId; 15 | 16 | public function init() { 17 | $this->setViewFileIfNotSetTo('edit'); 18 | } 19 | 20 | public function run($id) { 21 | $this->entityId = $id; 22 | 23 | return $this->loadModelAndSaveOrPrintView(); 24 | } 25 | 26 | protected function initModel() { 27 | $entity = $this->findEntityByIdentifierOrFail($this->entityId); 28 | $this->_model = $this->createViewModel($entity); 29 | $this->_model->loadAttributesFromEntity(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Web/Actions/ListRecords.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ListRecords extends Action { 16 | use ViewModelManagement; 17 | /** 18 | * @var callable a PHP callable that will be called to prepare a data provider that 19 | * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead. 20 | * The signature of the callable should be: 21 | * 22 | * ```php 23 | * function ($dataProvider, $action) { 24 | * // $dataProvider the data provider instance 25 | * // $action is the action object currently running 26 | * } 27 | * ``` 28 | * 29 | * The callable should return an instance of [[\yii\data\DataProviderInterface]]. 30 | */ 31 | public $prepareDataProvider; 32 | 33 | public function init() { 34 | $this->setViewFileIfNotSetTo('list'); 35 | if (!$this->viewModelClassName) { 36 | $this->viewModelClassName = ListingModel::class; 37 | } 38 | } 39 | 40 | public function run() { 41 | $model = $this->createNewModel(); 42 | $request = $this->getRequest(); 43 | $model->load($request->queryParams); 44 | 45 | return $this->renderViewFile(compact('model')); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Web/Actions/RecoverEntity.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class RecoverEntity extends Action { 17 | use ModelSearching; 18 | public $failedToRecoverFlashMessage = 'Unable to recover entity'; 19 | public $successfullyRecoveredFlashMessage = 'Entity successfully recovered'; 20 | public $redirectUrl; 21 | public $recoveredListFieldName = 'restored-ids'; 22 | 23 | public function init() { 24 | $this->setViewFileIfNotSetTo('list'); 25 | } 26 | 27 | /** 28 | * @param int|null $id 29 | * 30 | * @return \yii\web\Response 31 | * @throws \yii\web\NotFoundHttpException 32 | */ 33 | public function run($id = null) { 34 | $ids = ($id) ? [$id] : $this->serviceLocator->request->post($this->recoveredListFieldName, []); 35 | 36 | $savedResults = []; 37 | foreach ($ids as $id) { 38 | $entity = $this->findEntityByIdentifierOrFail($id); 39 | $savedResults[] = $this->tryToRecoverEntity($entity); 40 | } 41 | 42 | $savedNotSuccessfully = array_filter($savedResults, function ($value) { 43 | return !$value; 44 | }); 45 | if ($savedNotSuccessfully) { 46 | $this->addErrorFlash($this->failedToRecoverFlashMessage); 47 | } else { 48 | $this->addSuccessFlash($this->successfullyRecoveredFlashMessage); 49 | } 50 | 51 | return $this->redirectToNextPage(); 52 | } 53 | 54 | protected function tryToRecoverEntity($entity) { 55 | $repository = $this->repository; 56 | try { 57 | if ($repository instanceof RecoverableRepository) { 58 | $savedSuccessfully = $repository->recover($entity); 59 | } else { 60 | $savedSuccessfully = false; 61 | } 62 | } catch (UnableToSaveEntityException $e) { 63 | $savedSuccessfully = false; 64 | } 65 | 66 | return $savedSuccessfully; 67 | } 68 | 69 | // @todo fix duplicate with EntityModificationAction 70 | protected function redirectToNextPage() { 71 | if (null === $this->redirectUrl) { 72 | $redirectUrl = ['list']; 73 | } elseif (is_callable($this->redirectUrl)) { 74 | $redirectUrl = call_user_func($this->redirectUrl, $this); 75 | } else { 76 | $redirectUrl = $this->redirectUrl; 77 | } 78 | 79 | return $this->controller->redirect($redirectUrl, $statusCode = 200); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Web/Actions/ServiceAction.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | abstract class ServiceAction extends Action { 19 | /** 20 | * @var object a service class object 21 | */ 22 | private $_service; 23 | 24 | public function getService() { 25 | if (!is_object($this->_service)) { 26 | $this->initService(); 27 | } 28 | 29 | return $this->_service; 30 | } 31 | 32 | public function setService($service): void { 33 | if (!is_object($service) && (!class_exists($service) || !$this->container->has($service))) { 34 | throw new InvalidConfigException("Service must be an object or container definition"); 35 | } 36 | $this->_service = $service; 37 | } 38 | 39 | protected function initService(): void { 40 | $this->_service = $this->container->get($this->_service); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Web/Base/Action.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class Action extends \yii\base\Action implements ServiceLocatorAware, ContainerAware, RepositoryAware { 33 | use ServiceLocatorAccess; 34 | use ContainerAccess; 35 | use RepositoryAccess; 36 | use SessionMessagesManagement; 37 | use ResponseManagement; 38 | /** 39 | * @var string name of the view, which should be rendered 40 | */ 41 | public $viewFile; 42 | /** 43 | * @var callable callback that prepares params for a view. Use it to extend default view params list. 44 | */ 45 | public $prepareViewParams; 46 | /** 47 | * @var bool defines whether an action can be rendered or it just process request without printing any HTML and should 48 | * redirect to a next page using {@link redirectUrl}. 49 | */ 50 | public $printable = true; 51 | /** 52 | * @var string default action to redirect using {@link redirectToNextPage} 53 | */ 54 | protected $defaultRedirectUrlAction = 'index'; 55 | 56 | /** 57 | * Checks whether action with specified ID exists in owner controller. 58 | * 59 | * @param string $id action ID. 60 | * 61 | * @return boolean whether action exists or not. 62 | */ 63 | protected function isActionExistsInController($id): bool { 64 | $inlineActionMethodName = 'action' . Inflector::camelize($id); 65 | 66 | return $this->controller->hasMethod($inlineActionMethodName) || array_key_exists($id, $this->controller->actions()); 67 | } 68 | 69 | protected function setViewFileIfNotSetTo($file) { 70 | $this->viewFile = isset($this->viewFile) ? $this->viewFile : $file; 71 | } 72 | 73 | /** 74 | * Prints page view file defined at {@link viewFile}. 75 | * Params being passed to view from {@link prepareViewContext} and {@link getDefaultViewParams} 76 | * 77 | * @return string the rendering result. 78 | */ 79 | protected function printView() { 80 | return $this->printable ? $this->renderViewFile([]) : $this->redirectToNextPage(); 81 | } 82 | 83 | /** 84 | * Renders a view defined at {@link viewFile} and applies layout if available. 85 | * 86 | * 87 | * @param array $params the parameters (name-value pairs) that should be made available in the view. 88 | * Params are extended by {@link prepareViewContext} and {@link getDefaultViewParams} 89 | * 90 | * @return string the rendering result. 91 | */ 92 | protected function renderViewFile($params = []) { 93 | return $this->controller->render($this->viewFile, $this->prepareParamsForViewFile($params)); 94 | } 95 | 96 | /** 97 | * Renders a view defined at {@link viewFile} without applying layout. 98 | * It will inject into the rendering result JS/CSS scripts and files which are registered with 99 | * the view. 100 | * 101 | * @param array $params the parameters (name-value pairs) that should be made available in the view. 102 | * Params are extended by {@link prepareViewContext} and {@link getDefaultViewParams} 103 | * 104 | * @return string the rendering result. 105 | */ 106 | protected function renderViewFileForAjax($params = []) { 107 | return $this->controller->renderAjax($this->viewFile, $this->prepareParamsForViewFile($params)); 108 | } 109 | 110 | /** 111 | * Prepares params for {@link viewFile} extending them wit the ones defined by {@link prepareViewContext} 112 | * and {@link getDefaultViewParams}. 113 | * 114 | * @param $params name-value pairs 115 | * 116 | * @return array parameters (name-value pairs) 117 | */ 118 | protected function prepareParamsForViewFile($params): array { 119 | $viewParams = array_merge($this->prepareViewContext(), $this->getDefaultViewParams()); 120 | $viewParams = array_merge($viewParams, $params); 121 | if (is_callable($this->prepareViewParams)) { 122 | $viewParams = call_user_func($this->prepareViewParams, $viewParams, $this); 123 | } 124 | 125 | return $viewParams; 126 | } 127 | 128 | /** 129 | * Renders a view file. 130 | * 131 | * @param array $params the parameters (name-value pairs) that should be made available in the view. 132 | * Params are extended by {@link prepareViewContext}. 133 | * 134 | * @return string the rendering result. 135 | */ 136 | protected function renderFile($params = []) { 137 | $params = array_merge($this->prepareViewContext(), $params); 138 | 139 | return $this->controller->renderFile($this->viewFile, $params); 140 | } 141 | 142 | /** 143 | * Renders a file without applying layout. 144 | * 145 | * @param array $params the parameters (name-value pairs) that should be made available in the view. 146 | * Params are extended by {@link prepareViewContext}. 147 | * 148 | * @return string the rendering result. 149 | */ 150 | protected function renderPartial($params = []) { 151 | $params = array_merge($this->prepareViewContext(), $params); 152 | 153 | return $this->controller->renderPartial($this->viewFile, $params); 154 | } 155 | 156 | /** 157 | * Renders a view in response to an AJAX request. 158 | * 159 | * This method is similar to {@link renderPartial} except that it will inject into 160 | * the rendering result with JS/CSS scripts and files which are registered with the view. 161 | * For this reason, you should use this method instead of {@link renderPartial} to render 162 | * a view to respond to an AJAX request. 163 | * 164 | * @param array $params the parameters (name-value pairs) that should be made available in the view. 165 | * Params are extended by {@link prepareViewContext}. 166 | * 167 | * @return string the rendering result. 168 | */ 169 | protected function renderAjax($params = []) { 170 | $params = array_merge($this->prepareViewContext(), $params); 171 | 172 | return $this->controller->renderAjax($this->viewFile, $params); 173 | } 174 | 175 | /** 176 | * Override this method to set params that should be passed to a view file defined at {@link viewFile}. 177 | * 178 | * @return array of view params (name-value pairs). 179 | */ 180 | protected function getDefaultViewParams(): array { 181 | return []; 182 | } 183 | 184 | /** 185 | * Override this method to set variables that will be passed to any file rendered by 186 | * an action. 187 | * 188 | * @return array of view params (name-value pairs). 189 | */ 190 | protected function prepareViewContext(): array { 191 | return []; 192 | } 193 | 194 | /** 195 | * Defines default redirect URL. 196 | * 197 | * If you need to change redirect action, set {@link defaultRedirectUrlAction} at action init. 198 | * 199 | * Override this method if you need to define custom format of URL. 200 | * 201 | * @return array url definition; 202 | */ 203 | protected function prepareDefaultRedirectUrl() { 204 | return [$this->defaultRedirectUrlAction]; 205 | } 206 | 207 | /** 208 | * @return mixed|\yii\console\Request|\yii\web\Request 209 | */ 210 | protected function getRequest() { 211 | return $this->serviceLocator->request; 212 | } 213 | 214 | protected function getRequestStatusCore(): int { 215 | if ($this->request->isAjax) { 216 | return ResponseHttpStatus::OK; 217 | } 218 | 219 | return ResponseHttpStatus::FOUND; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Web/Base/Actions/EntityModificationAction.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class EntityModificationAction extends Action { 20 | use ViewModelManagement; 21 | use ModelSearching; 22 | use EntityActionHooks; 23 | /** 24 | * @var int indicates whether to throw exception or handle it 25 | */ 26 | public $throwExceptions = false; 27 | /** 28 | * @var \PHPKitchen\Domain\Web\Base\ViewModel; 29 | */ 30 | protected $_model; 31 | 32 | public function __construct($id, $controller, $config = []) { 33 | $this->defaultRedirectUrlAction = 'edit'; 34 | 35 | parent::__construct($id, $controller, $config); 36 | } 37 | 38 | abstract protected function initModel(); 39 | 40 | protected function loadModelAndSaveOrPrintView() { 41 | return $this->modelLoaded() 42 | ? $this->saveModel() 43 | : $this->printView(); 44 | } 45 | 46 | protected function modelLoaded(): bool { 47 | return $this->getModel()->load($this->getRequest()->post()); 48 | } 49 | 50 | protected function saveModel() { 51 | return $this->validateModelAndTryToSaveEntity() 52 | ? $this->handleSuccessfulOperation() 53 | : $this->handleFailedOperation(); 54 | } 55 | 56 | protected function validateModelAndTryToSaveEntity() { 57 | if ($this->getModel()->validate()) { 58 | $result = $this->tryToSaveEntity(); 59 | } else { 60 | $result = false; 61 | } 62 | 63 | return $result; 64 | } 65 | 66 | protected function tryToSaveEntity() { 67 | $model = $this->getModel(); 68 | $entity = $model->convertToEntity(); 69 | try { 70 | $savedSuccessfully = $this->getRepository()->validateAndSave($entity); 71 | $this->getRepository()->refresh($entity); 72 | $model->loadAttributesFromEntity(); 73 | } catch (UnableToSaveEntityException $e) { 74 | $savedSuccessfully = false; 75 | if ($this->throwExceptions) { 76 | throw $e; 77 | } 78 | } 79 | if ($savedSuccessfully) { 80 | // @TODO seems like duplicates handleSuccessfulOperation - need to investigate 81 | $this->addSuccessFlash($this->successFlashMessage); 82 | } else { 83 | $this->addErrorFlash($this->failToSaveErrorFlashMessage); 84 | } 85 | 86 | return $savedSuccessfully; 87 | } 88 | 89 | /** 90 | * Defines default redirect URL. 91 | * 92 | * If you need to change redirect action, set {@link defaultRedirectUrlAction} at action init. 93 | * 94 | * Override this method if you need to define custom format of URL. 95 | * 96 | * @return array url definition; 97 | */ 98 | protected function prepareDefaultRedirectUrl() { 99 | $entity = $this->getModel()->convertToEntity(); 100 | 101 | return [$this->defaultRedirectUrlAction, 'id' => $entity->id]; 102 | } 103 | 104 | /** 105 | * @override base implementation for BC compatibility. 106 | * @TODO remove it in the next major release 107 | */ 108 | protected function callRedirectUrlCallback(): array { 109 | $entity = $this->getModel()->convertToEntity(); 110 | 111 | return call_user_func($this->redirectUrl, $entity, $this); 112 | } 113 | 114 | public function getModel() { 115 | if (null === $this->_model) { 116 | $this->initModel(); 117 | } 118 | 119 | return $this->_model; 120 | } 121 | 122 | /** 123 | * @override 124 | */ 125 | protected function prepareViewContext(): array { 126 | $context = parent::prepareViewContext(); 127 | $context['model'] = $this->getModel(); 128 | 129 | return $context; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Web/Base/EntityModificationAction.php: -------------------------------------------------------------------------------- 1 | addSuccessFlash($this->successFlashMessage); 32 | if ($this->redirectUrl !== false) { 33 | return $this->redirectToNextPage(); 34 | } 35 | 36 | return $this->printView(); 37 | } 38 | 39 | protected function handleFailedOperation() { 40 | $this->addErrorFlash($this->validationFailedFlashMessage); 41 | 42 | return $this->printView(); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Web/Base/Mixins/RepositoryAccess.php: -------------------------------------------------------------------------------- 1 | _repository) { 32 | // fallback to support old approach with defining repositories in controllers 33 | $this->_repository = $this->controller->repository ?? null; 34 | } 35 | 36 | return $this->_repository; 37 | } 38 | 39 | public function setRepository($repository): void { 40 | if ($this->isObjectValidRepository($repository)) { 41 | $this->_repository = $repository; 42 | } else { 43 | $this->createAndSetRepositoryFromDefinition($repository); 44 | } 45 | } 46 | 47 | protected function createAndSetRepositoryFromDefinition($definition): void { 48 | $repository = $this->container->create($definition); 49 | if (!$this->isObjectValidRepository($repository)) { 50 | throw new InvalidArgumentException('Repository should be an instance of ' . EntitiesRepository::class); 51 | } 52 | $this->_repository = $repository; 53 | } 54 | 55 | protected function isObjectValidRepository($object): bool { 56 | return is_object($object) && $object instanceof EntitiesRepository; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Web/Base/Mixins/ResponseManagement.php: -------------------------------------------------------------------------------- 1 | redirectUrl) { 34 | $redirectUrl = $this->prepareDefaultRedirectUrl(); 35 | } elseif (is_callable($this->redirectUrl)) { 36 | $redirectUrl = $this->callRedirectUrlCallback(); 37 | } else { 38 | $redirectUrl = $this->redirectUrl; 39 | } 40 | 41 | return $this->controller->redirect($redirectUrl, $this->getRequestStatusCore()); 42 | } 43 | 44 | protected function callRedirectUrlCallback() { 45 | return call_user_func($this->redirectUrl, $this); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Web/Base/Mixins/SessionMessagesManagement.php: -------------------------------------------------------------------------------- 1 | setFlash([$this->errorFlashMessageKey => $message]); 28 | } 29 | 30 | public function addSuccessFlash($message): void { 31 | $this->setFlash([$this->successFlashMessageKey => $message]); 32 | } 33 | /** 34 | * Sets a flash message. 35 | * 36 | * @param string|array|null $message flash message(s) to be set. 37 | * If plain string is passed, it will be used as a message with the key 'success'. 38 | * You may specify multiple messages as an array, if element name is not integer, it will be used as a key, 39 | * otherwise 'success' will be used as key. 40 | * If empty value passed, no flash will be set. 41 | * Particular message value can be a PHP callback, which should return actual message. Such callback, should 42 | * have following signature: 43 | * 44 | * ```php 45 | * function (array $params) { 46 | * // return string 47 | * } 48 | * ``` 49 | * 50 | * @param array $params extra params for the message parsing in format: key => value. 51 | */ 52 | public function setFlash($message, $params = []): void { 53 | if (!$this->useFlashMessages || empty($message)) { 54 | return; 55 | } 56 | $session = $this->session; 57 | foreach ((array)$message as $key => $value) { 58 | if (is_scalar($value)) { 59 | $value = preg_replace_callback("/{(\\w+)}/", function ($matches) use ($params) { 60 | $paramName = $matches[1]; 61 | 62 | return isset($params[$paramName]) ? $params[$paramName] : $paramName; 63 | }, $value); 64 | } else { 65 | $value = call_user_func($value, $params); 66 | } 67 | if (is_int($key)) { 68 | $session->setFlash($this->successFlashMessageKey, $value); 69 | } else { 70 | $session->setFlash($key, $value); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * @return \yii\web\Session 77 | */ 78 | protected function getSession() { 79 | return $this->serviceLocator->session; 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/Web/Base/Models/ListingModel.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ListingModel extends ViewModel { 14 | public $fetchDataAsArray = true; 15 | 16 | /** 17 | * Override this method 18 | * 19 | * @return \PHPKitchen\Domain\Data\EntitiesProvider 20 | */ 21 | public function getDataProvider() { 22 | $provider = $this->repository->getEntitiesProvider(); 23 | if ($this->fetchDataAsArray) { 24 | $provider->query->asArray(); 25 | } 26 | if ($provider->query instanceof Specification) { 27 | $provider->query->bySearchModel($this); 28 | } 29 | 30 | return $provider; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Web/Base/Models/RecoverableEntitiesListModel.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class RecoverableEntitiesListModel extends ListingModel { 14 | /** 15 | * Override this method 16 | * 17 | * @return \PHPKitchen\Domain\Data\EntitiesProvider 18 | */ 19 | public function getDeletedDataProvider(): EntitiesProvider { 20 | $provider = $this->getDataProvider(); 21 | $provider->query->deleted(); 22 | 23 | return $provider; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Web/Base/Models/ViewModel.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class ViewModel extends Model implements ContainerAware, ServiceLocatorAware { 28 | use ContainerAccess; 29 | use ServiceLocatorAccess; 30 | /** 31 | * @var Entity 32 | */ 33 | private $_entity; 34 | /** 35 | * @var array Defines map of entity attributes required in {@link convertAttributesToEntityAttributes()} 36 | * Format of map: 37 | *
 38 |      * [
 39 |      *      'entityAttribute' => 'formAttributeName',
 40 |      *      'entityAttribute' => any value (for example digits, objects, array) except string equals to name of the form attributes,
 41 |      *      'entityAttribute' => callable // callable will be executed and result will be set as entity attribute
 42 |      * ]
 43 |      * 
44 | */ 45 | private $_entityAttributesMap; 46 | /** 47 | * @var \PHPKitchen\Domain\Contracts\EntityController|\yii\web\Controller 48 | */ 49 | private $_controller; 50 | 51 | public function convertToEntity(): Entity { 52 | $defaultAttributes = $this->prepareDefaultEntityAttributes(); 53 | $newAttributes = $this->convertToEntityAttributes(); 54 | $entity = $this->getEntity(); 55 | $entity->load(ArrayHelper::merge($defaultAttributes, $newAttributes)); 56 | 57 | return $entity; 58 | } 59 | 60 | /** 61 | * Override to set default entity attributes. 62 | * 63 | * @return array default entity attributes 64 | */ 65 | protected function prepareDefaultEntityAttributes(): array { 66 | return []; 67 | } 68 | 69 | /** 70 | * Converts form to entity attributes. 71 | * 72 | * @return array entity attributes. 73 | */ 74 | public function convertToEntityAttributes(): array { 75 | $entityAttributesMap = $this->getEntityAttributesMap(); 76 | if (empty($entityAttributesMap)) { 77 | return $this->getAttributes(); 78 | } 79 | $attributes = []; 80 | foreach ($entityAttributesMap as $entityAttribute => $formValue) { 81 | if (is_string($formValue) && $this->canGetProperty($formValue)) { 82 | $attributeValue = $this->$formValue; 83 | } elseif (is_callable($formValue)) { 84 | $attributeValue = call_user_func($formValue); 85 | } else { 86 | $attributeValue = $formValue; 87 | } 88 | $attributes[$entityAttribute] = $attributeValue; 89 | } 90 | 91 | return $attributes; 92 | } 93 | 94 | /** 95 | * Populates the form by entity data. 96 | * 97 | * @return bool 98 | */ 99 | public function loadAttributesFromEntity(): bool { 100 | $attributes = $this->convertEntityToSelfAttributes(); 101 | 102 | return $this->load($attributes, ''); 103 | } 104 | 105 | /** 106 | * Converts AR attributes to form attributes. 107 | * 108 | * @return array 109 | */ 110 | protected function convertEntityToSelfAttributes(): array { 111 | $entity = $this->getEntity(); 112 | $attributes = []; 113 | foreach ($this->getEntityAttributesMap() as $modelAttribute => $formValue) { 114 | if (is_string($formValue) && $this->canGetProperty($formValue) && $entity->canGetProperty($modelAttribute)) { 115 | $attributes[$formValue] = $entity->$modelAttribute; 116 | } 117 | } 118 | 119 | return $attributes; 120 | } 121 | 122 | /** 123 | * Returns the name of entity attribute mapped to specified form field 124 | * 125 | * @param string $formAttributeName 126 | * 127 | * @return string 128 | */ 129 | public function getEntityAttributeMappedToFieldName(string $formAttributeName): string { 130 | return array_flip($this->getEntityAttributesMap())[$formAttributeName]; 131 | } 132 | 133 | protected function getEntityAttributesMap(): array { 134 | if (null === $this->_entityAttributesMap) { 135 | $selfAttributeNames = $this->attributes(); 136 | $this->_entityAttributesMap = array_combine($selfAttributeNames, $selfAttributeNames); 137 | } 138 | 139 | return $this->_entityAttributesMap; 140 | } 141 | 142 | public function setEntityAttributesMap(array $entityAttributesMap): void { 143 | $this->_entityAttributesMap = $entityAttributesMap; 144 | } 145 | 146 | //region -------------------- GETTERS/SETTERS -------------------- 147 | public function getEntity() { 148 | return $this->_entity; 149 | } 150 | 151 | public function setEntity(DomainEntity $entity) { 152 | $this->_entity = $entity; 153 | } 154 | 155 | public function getId() { 156 | return $this->getEntity()->id; 157 | } 158 | 159 | public function getController() { 160 | return $this->_controller; 161 | } 162 | 163 | public function setController($controller) { 164 | $this->_controller = $controller; 165 | } 166 | 167 | public function getRepository() { 168 | if ($this->controller->action instanceof RepositoryAware) { 169 | return $this->controller->action->repository; 170 | } 171 | 172 | return $this->controller->repository; 173 | } 174 | //endregion 175 | } -------------------------------------------------------------------------------- /src/Web/Base/RecoverableEntitiesListModel.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface RepositoryAware { 18 | /** 19 | * @return Repository|EntitiesRepository 20 | */ 21 | public function getRepository(); 22 | 23 | /** 24 | * @param Repository $repository 25 | */ 26 | public function setRepository($repository); 27 | } -------------------------------------------------------------------------------- /src/Web/Mixins/ControllerActionsManagement.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | trait ControllerActionsManagement { 14 | private $_actions = []; 15 | 16 | public function actions() { 17 | return $this->_actions; 18 | } 19 | 20 | protected function addAction($name, $definition) { 21 | $this->_actions[$name] = $definition; 22 | } 23 | 24 | protected function updateActionDefinition($name, $definition) { 25 | if (is_string($definition) || is_object($definition)) { 26 | $this->_actions[$name] = $definition; 27 | } elseif (is_array($definition)) { 28 | if ($this->isDynamicActionDefined($name) && is_array($this->_actions[$name])) { 29 | $this->_actions[$name] = ArrayHelper::merge($this->_actions[$name], $definition); 30 | } else { 31 | $this->_actions[$name] = $definition; 32 | } 33 | } 34 | } 35 | 36 | protected function removeAction($name) { 37 | unset($this->_actions[$name]); 38 | } 39 | 40 | protected function isDynamicActionDefined($name) { 41 | return isset($this->_actions[$name]); 42 | } 43 | 44 | protected function setActions(array $actions) { 45 | $this->_actions = $actions; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Web/Mixins/EntityManagement.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | trait EntityManagement { 21 | public $notFoundModelExceptionMessage = 'Requested page does not exist!'; 22 | public $notFoundModelExceptionClassName = NotFoundHttpException::class; 23 | /** 24 | * @var \PHPKitchen\Domain\DB\EntitiesRepository 25 | */ 26 | private $_repository; 27 | 28 | public function findEntityByPk($pk) { 29 | $entity = $this->getRepository()->find()->oneWithPk($pk); 30 | if (null === $entity) { 31 | /** 32 | * @var NotFoundHttpException $exception 33 | */ 34 | $exception = $this->getContainer() 35 | ->create($this->notFoundModelExceptionClassName, [$this->notFoundModelExceptionMessage]); 36 | throw $exception; 37 | } 38 | 39 | return $entity; 40 | } 41 | 42 | public function getRepository() { 43 | if ($this->_repository === null) { 44 | throw new InvalidConfigException('Repository should be set in ' . static::class); 45 | } 46 | 47 | return $this->_repository; 48 | } 49 | 50 | public function setRepository($repository) { 51 | if (is_string($repository) || is_array($repository)) { 52 | $this->_repository = $this->container->create($repository); 53 | } elseif (is_object($repository) && $repository instanceof Repository) { 54 | $this->_repository = $repository; 55 | } else { 56 | throw new InvalidConfigException('Repository should be a valid container config or an instance of ' . Repository::class); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Web/Mixins/ModelSearching.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | trait ModelSearching { 24 | /** 25 | * @var callable a PHP callable that will be called to return the model corresponding 26 | * to the specified primary key value. If not set, [[findModel()]] will be used instead. 27 | * The signature of the callable should be: 28 | * 29 | * ```php 30 | * function ($id, $action) { 31 | * // $id is the primary key value. If composite primary key, the key values 32 | * // will be separated by comma. 33 | * // $action is the action object currently running 34 | * } 35 | * ``` 36 | * 37 | * The callable should return the model found, or throw an exception if not found. 38 | */ 39 | protected $_searchBy; 40 | 41 | /** 42 | * Returns the data model based on the primary key given. 43 | * If the data model is not found, a 404 HTTP exception will be raised. 44 | * 45 | * @param string $entityPrimaryKey the ID of the model to be loaded. If the model has a composite primary key, 46 | * the ID must be a string of the primary key values separated by commas. 47 | * The order of the primary key values should follow that returned by the `primaryKey()` method 48 | * of the model. 49 | * 50 | * @return mixed 51 | * @throws InvalidConfigException on invalid configuration 52 | * 53 | * @throws NotFoundHttpException if the model cannot be found 54 | * @deprecated use {@link findEntityByIdentifierOrFail} instead 55 | */ 56 | protected function findModelByPk($entityPrimaryKey) { 57 | if ($this->searchBy !== null) { 58 | $model = call_user_func($this->searchBy, $entityPrimaryKey, $this); 59 | } elseif ($this->controller->hasMethod('findEntityByPk')) { 60 | //@deprecated use repositories or callback 61 | $entity = $this->controller->findEntityByPk($entityPrimaryKey); 62 | $model = $this->createViewModel($entity); 63 | } elseif ($this->repository) { 64 | $model = $this->findEntityByPK($entityPrimaryKey); 65 | } else { 66 | throw new InvalidConfigException('Either "' . static::class . '::modelSearchCallback" must be set or controller must declare method "findEntityByPk()".'); 67 | } 68 | 69 | return $model; 70 | } 71 | 72 | /** 73 | * Returns the data model based on the primary key given. 74 | * If the data model is not found, a 404 HTTP exception will be raised. 75 | * 76 | * @param string $identifier the ID of the model to be loaded. If the model has a composite primary key, 77 | * the ID must be a string of the primary key values separated by commas. 78 | * The order of the primary key values should follow that returned by the `primaryKey()` method 79 | * of the model. 80 | * 81 | * @return mixed 82 | * @throws InvalidConfigException on invalid configuration 83 | * @throws NotFoundHttpException on invalid configuration 84 | */ 85 | protected function findEntityByIdentifierOrFail($identifier) { 86 | if ($this->searchBy !== null) { 87 | $entity = call_user_func($this->searchBy, $identifier, $this); 88 | } elseif ($this->repository) { 89 | $entity = $this->findEntityByPK($identifier); 90 | } else { 91 | throw new InvalidConfigException('Either "' . static::class . '::searchBy" or "' . static::class . '::repository" must be set.'); 92 | } 93 | 94 | if (!$entity) { 95 | throw new NotFoundHttpException('Entity doest not exist'); 96 | } 97 | 98 | return $entity; 99 | } 100 | 101 | protected function findEntityByPK($primaryKey) { 102 | return $this->repository->find()->oneWithPk($primaryKey); 103 | } 104 | 105 | public function getSearchBy() { 106 | return $this->_searchBy; 107 | } 108 | 109 | public function setSearchBy(callable $filter) { 110 | $this->_searchBy = $filter; 111 | } 112 | 113 | /** 114 | * @deprecated use {@link getSearchBy} 115 | */ 116 | public function getModelSearchCallback() { 117 | return $this->getSearchBy(); 118 | } 119 | 120 | /** 121 | * @deprecated use {@link setSearchBy} 122 | */ 123 | public function setModelSearchCallback(callable $findModelByPk) { 124 | $this->setSearchBy($findModelByPk); 125 | } 126 | } -------------------------------------------------------------------------------- /src/Web/Mixins/ViewModelManagement.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | trait ViewModelManagement { 21 | private $_viewModelClassName; 22 | /** 23 | * @var string the scenario to be assigned to the new model before it is validated and saved. 24 | */ 25 | public $scenario = ViewModel::SCENARIO_DEFAULT; 26 | 27 | protected function createNewModel() { 28 | $entity = $this->getRepository()->createNewEntity(); 29 | 30 | return $this->createViewModel($entity); 31 | } 32 | 33 | /** 34 | * @param \PHPKitchen\Domain\Base\Entity $entity 35 | * 36 | * @return \PHPKitchen\Domain\Web\Base\ViewModel 37 | */ 38 | protected function createViewModel($entity) { 39 | $model = $this->container->create([ 40 | 'class' => $this->getViewModelClassName(), 41 | 'entity' => $entity, 42 | 'controller' => $this->controller, 43 | ]); 44 | $model->scenario = $this->scenario; 45 | 46 | return $model; 47 | } 48 | 49 | public function getViewModelClassName() { 50 | return $this->_viewModelClassName; 51 | } 52 | 53 | public function setViewModelClassName($viewModelClassName) { 54 | $this->_viewModelClassName = $viewModelClassName; 55 | } 56 | } --------------------------------------------------------------------------------