├── .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 |
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 | } --------------------------------------------------------------------------------