├── .bowerrc ├── .gitignore ├── LICENSE.md ├── README.md ├── assets └── AppAsset.php ├── bootstrap └── ContainerBootstrap.php ├── codeception.yml ├── commands └── HelloController.php ├── composer.json ├── composer.lock ├── config ├── .gitignore ├── console.php ├── params.php ├── test.php └── web.php ├── controllers └── SiteController.php ├── dispatchers ├── DummyEventDispatcher.php └── EventDispatcher.php ├── entities ├── AggregateRoot.php ├── Employee │ ├── Address.php │ ├── Employee.php │ ├── Events │ │ ├── EmployeeAddressChanged.php │ │ ├── EmployeeArchived.php │ │ ├── EmployeeCreated.php │ │ ├── EmployeePhoneAdded.php │ │ ├── EmployeePhoneRemoved.php │ │ ├── EmployeeReinstated.php │ │ ├── EmployeeRemoved.php │ │ └── EmployeeRenamed.php │ ├── Id.php │ ├── Name.php │ ├── Phone.php │ ├── Phones.php │ └── Status.php └── EventTrait.php ├── mail └── layouts │ └── html.php ├── models ├── ContactForm.php ├── LoginForm.php └── User.php ├── repositories ├── EmployeeRepository.php ├── MemoryEmployeeRepository.php └── NotFoundException.php ├── requirements.php ├── runtime └── .gitignore ├── services ├── EmployeeService.php └── dto │ ├── AddressDto.php │ ├── EmployeeArchiveDto.php │ ├── EmployeeCreateDto.php │ ├── EmployeeReinstateDto.php │ ├── NameDto.php │ └── PhoneDto.php ├── tests ├── _bootstrap.php ├── _data │ └── .gitignore ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── FunctionalTester.php │ └── UnitTester.php ├── acceptance.suite.yml.example ├── acceptance │ ├── AboutCest.php │ ├── ContactCest.php │ ├── HomeCest.php │ ├── LoginCest.php │ └── _bootstrap.php ├── bin │ ├── yii │ └── yii.bat ├── functional.suite.yml ├── functional │ ├── ContactFormCest.php │ ├── LoginFormCest.php │ └── _bootstrap.php ├── unit.suite.yml └── unit │ ├── _bootstrap.php │ ├── entities │ └── Employee │ │ ├── ArchiveTest.php │ │ ├── ChangeAddressTest.php │ │ ├── CreateTest.php │ │ ├── EmployeeBuilder.php │ │ ├── PhoneTest.php │ │ ├── ReinstateTest.php │ │ ├── RemoveTest.php │ │ └── RenameTest.php │ ├── models │ ├── ContactFormTest.php │ ├── LoginFormTest.php │ └── UserTest.php │ └── repositories │ ├── BaseRepositoryTest.php │ └── MemoryEmployeeRepositoryTest.php ├── views ├── layouts │ └── main.php └── site │ ├── about.php │ ├── contact.php │ ├── error.php │ ├── index.php │ └── login.php ├── web ├── assets │ └── .gitignore ├── css │ └── site.css ├── favicon.ico ├── index-test.php ├── index.php └── robots.txt ├── widgets └── Alert.php ├── yii └── yii.bat /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/bower-asset" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | 29 | tests/_output/* 30 | tests/_support/_generated -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The BSD License (BSD) 2 | 3 | Copyright © 2017 by Dmitry Eliseev (ElisDN). All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of Yii Software LLC nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 23 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 24 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 29 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii 2 DDD Aggregates Demo 2 | ============================ 3 | 4 | Demonstration of domain entities and repositories. 5 | 6 | INSTALLATION 7 | ------------ 8 | 9 | You can then clone this project template using the following command: 10 | 11 | ~~~ 12 | git clone git@github.com:ElisDN/yii2-demo-aggregates project 13 | cd project 14 | composer install 15 | ~~~ 16 | 17 | CONFIGURATION 18 | ------------- 19 | 20 | Add the file `config/db.php` with real data, for example: 21 | 22 | ```php 23 | 'yii\db\Connection', 26 | 'dsn' => 'mysql:host=localhost;dbname=aggregates', 27 | 'username' => 'root', 28 | 'password' => '1234', 29 | 'charset' => 'utf8', 30 | ]; 31 | ``` 32 | 33 | Add the file `config/test_db.php` for test data, for example: 34 | 35 | ```php 36 | 16 | * @since 2.0 17 | */ 18 | class AppAsset extends AssetBundle 19 | { 20 | public $basePath = '@webroot'; 21 | public $baseUrl = '@web'; 22 | public $css = [ 23 | 'css/site.css', 24 | ]; 25 | public $js = [ 26 | ]; 27 | public $depends = [ 28 | 'yii\web\YiiAsset', 29 | 'yii\bootstrap\BootstrapAsset', 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /bootstrap/ContainerBootstrap.php: -------------------------------------------------------------------------------- 1 | setSingleton(EventDispatcher::class, DummyEventDispatcher::class); 16 | } 17 | } -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | helpers: tests/_support 7 | settings: 8 | bootstrap: _bootstrap.php 9 | memory_limit: 1024M 10 | colors: true 11 | modules: 12 | config: 13 | Yii2: 14 | configFile: 'config/test.php' 15 | cleanup: false 16 | 17 | # To enable code coverage: 18 | #coverage: 19 | # #c3_url: http://localhost:8080/index-test.php/ 20 | # enabled: true 21 | # #remote: true 22 | # #remote_config: '../codeception.yml' 23 | # whitelist: 24 | # include: 25 | # - models/* 26 | # - controllers/* 27 | # - commands/* 28 | # - mail/* 29 | # blacklist: 30 | # include: 31 | # - assets/* 32 | # - config/* 33 | # - runtime/* 34 | # - vendor/* 35 | # - views/* 36 | # - web/* 37 | # - tests/* 38 | -------------------------------------------------------------------------------- /commands/HelloController.php: -------------------------------------------------------------------------------- 1 | 19 | * @since 2.0 20 | */ 21 | class HelloController extends Controller 22 | { 23 | /** 24 | * This command echoes what you have entered as the message. 25 | * @param string $message the message to be echoed. 26 | */ 27 | public function actionIndex($message = 'hello world') 28 | { 29 | echo $message . "\n"; 30 | 31 | return ExitCode::OK; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii2-app-basic", 3 | "description": "Yii 2 Basic Project Template", 4 | "keywords": ["yii2", "framework", "basic", "project template"], 5 | "homepage": "http://www.yiiframework.com/", 6 | "type": "project", 7 | "license": "BSD-3-Clause", 8 | "support": { 9 | "issues": "https://github.com/yiisoft/yii2/issues?state=open", 10 | "forum": "http://www.yiiframework.com/forum/", 11 | "wiki": "http://www.yiiframework.com/wiki/", 12 | "irc": "irc://irc.freenode.net/yii", 13 | "source": "https://github.com/yiisoft/yii2" 14 | }, 15 | "minimum-stability": "stable", 16 | "require": { 17 | "php": ">=7.1.0", 18 | "yiisoft/yii2": "~2.0.13", 19 | "yiisoft/yii2-bootstrap": "~2.0.0", 20 | "yiisoft/yii2-swiftmailer": "~2.0.0", 21 | "beberlei/assert": "^2.7", 22 | "ramsey/uuid": "^3.6" 23 | }, 24 | "require-dev": { 25 | "yiisoft/yii2-debug": "~2.0.0", 26 | "yiisoft/yii2-gii": "~2.0.0", 27 | "yiisoft/yii2-faker": "~2.0.0", 28 | 29 | "codeception/base": "^2.2.3", 30 | "codeception/verify": "~0.3.1", 31 | "codeception/specify": "~0.4.3" 32 | }, 33 | "config": { 34 | "process-timeout": 1800, 35 | "fxp-asset": { 36 | "enabled": false 37 | } 38 | }, 39 | "scripts": { 40 | "post-install-cmd": [ 41 | "yii\\composer\\Installer::postInstall" 42 | ], 43 | "post-create-project-cmd": [ 44 | "yii\\composer\\Installer::postCreateProject", 45 | "yii\\composer\\Installer::postInstall" 46 | ] 47 | }, 48 | "extra": { 49 | "yii\\composer\\Installer::postCreateProject": { 50 | "setPermission": [ 51 | { 52 | "runtime": "0777", 53 | "web/assets": "0777", 54 | "yii": "0755" 55 | } 56 | ] 57 | }, 58 | "yii\\composer\\Installer::postInstall": { 59 | "generateCookieValidationKey": [ 60 | "config/web.php" 61 | ] 62 | } 63 | }, 64 | "repositories": [ 65 | { 66 | "type": "composer", 67 | "url": "https://asset-packagist.org" 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | /db.php 2 | /test_db.php -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | 'basic-console', 8 | 'basePath' => dirname(__DIR__), 9 | 'bootstrap' => [ 10 | 'log', 11 | 'app\bootstrap\ContainerBootstrap', 12 | ], 13 | 'controllerNamespace' => 'app\commands', 14 | 'aliases' => [ 15 | '@bower' => '@vendor/bower-asset', 16 | '@npm' => '@vendor/npm-asset', 17 | ], 18 | 'components' => [ 19 | 'cache' => [ 20 | 'class' => 'yii\caching\FileCache', 21 | ], 22 | 'log' => [ 23 | 'targets' => [ 24 | [ 25 | 'class' => 'yii\log\FileTarget', 26 | 'levels' => ['error', 'warning'], 27 | ], 28 | ], 29 | ], 30 | 'db' => $db, 31 | ], 32 | 'params' => $params, 33 | /* 34 | 'controllerMap' => [ 35 | 'fixture' => [ // Fixture generation command line. 36 | 'class' => 'yii\faker\FixtureController', 37 | ], 38 | ], 39 | */ 40 | ]; 41 | 42 | if (YII_ENV_DEV) { 43 | // configuration adjustments for 'dev' environment 44 | $config['bootstrap'][] = 'gii'; 45 | $config['modules']['gii'] = [ 46 | 'class' => 'yii\gii\Module', 47 | ]; 48 | } 49 | 50 | return $config; 51 | -------------------------------------------------------------------------------- /config/params.php: -------------------------------------------------------------------------------- 1 | 'admin@example.com', 5 | ]; 6 | -------------------------------------------------------------------------------- /config/test.php: -------------------------------------------------------------------------------- 1 | 'basic-tests', 10 | 'basePath' => dirname(__DIR__), 11 | 'aliases' => [ 12 | '@bower' => '@vendor/bower-asset', 13 | '@npm' => '@vendor/npm-asset', 14 | ], 15 | 'language' => 'en-US', 16 | 'bootstrap' => [ 17 | 'app\bootstrap\ContainerBootstrap', 18 | ], 19 | 'components' => [ 20 | 'db' => $db, 21 | 'mailer' => [ 22 | 'useFileTransport' => true, 23 | ], 24 | 'assetManager' => [ 25 | 'basePath' => __DIR__ . '/../web/assets', 26 | ], 27 | 'urlManager' => [ 28 | 'showScriptName' => true, 29 | ], 30 | 'user' => [ 31 | 'identityClass' => 'app\models\User', 32 | ], 33 | 'request' => [ 34 | 'cookieValidationKey' => 'test', 35 | 'enableCsrfValidation' => false, 36 | // but if you absolutely need it set cookie domain to localhost 37 | /* 38 | 'csrfCookie' => [ 39 | 'domain' => 'localhost', 40 | ], 41 | */ 42 | ], 43 | ], 44 | 'params' => $params, 45 | ]; 46 | -------------------------------------------------------------------------------- /config/web.php: -------------------------------------------------------------------------------- 1 | 'basic', 8 | 'basePath' => dirname(__DIR__), 9 | 'bootstrap' => [ 10 | 'log', 11 | 'app\bootstrap\ContainerBootstrap', 12 | ], 13 | 'aliases' => [ 14 | '@bower' => '@vendor/bower-asset', 15 | '@npm' => '@vendor/npm-asset', 16 | ], 17 | 'components' => [ 18 | 'request' => [ 19 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation 20 | 'cookieValidationKey' => '', 21 | ], 22 | 'cache' => [ 23 | 'class' => 'yii\caching\FileCache', 24 | ], 25 | 'user' => [ 26 | 'identityClass' => 'app\models\User', 27 | 'enableAutoLogin' => true, 28 | ], 29 | 'errorHandler' => [ 30 | 'errorAction' => 'site/error', 31 | ], 32 | 'mailer' => [ 33 | 'class' => 'yii\swiftmailer\Mailer', 34 | // send all mails to a file by default. You have to set 35 | // 'useFileTransport' to false and configure a transport 36 | // for the mailer to send real emails. 37 | 'useFileTransport' => true, 38 | ], 39 | 'log' => [ 40 | 'traceLevel' => YII_DEBUG ? 3 : 0, 41 | 'targets' => [ 42 | [ 43 | 'class' => 'yii\log\FileTarget', 44 | 'levels' => ['error', 'warning'], 45 | ], 46 | ], 47 | ], 48 | 'db' => $db, 49 | /* 50 | 'urlManager' => [ 51 | 'enablePrettyUrl' => true, 52 | 'showScriptName' => false, 53 | 'rules' => [ 54 | ], 55 | ], 56 | */ 57 | ], 58 | 'params' => $params, 59 | ]; 60 | 61 | if (YII_ENV_DEV) { 62 | // configuration adjustments for 'dev' environment 63 | $config['bootstrap'][] = 'debug'; 64 | $config['modules']['debug'] = [ 65 | 'class' => 'yii\debug\Module', 66 | // uncomment the following to add your IP if you are not connecting from localhost. 67 | //'allowedIPs' => ['127.0.0.1', '::1'], 68 | ]; 69 | 70 | $config['bootstrap'][] = 'gii'; 71 | $config['modules']['gii'] = [ 72 | 'class' => 'yii\gii\Module', 73 | // uncomment the following to add your IP if you are not connecting from localhost. 74 | //'allowedIPs' => ['127.0.0.1', '::1'], 75 | ]; 76 | } 77 | 78 | return $config; 79 | -------------------------------------------------------------------------------- /controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'class' => AccessControl::className(), 23 | 'only' => ['logout'], 24 | 'rules' => [ 25 | [ 26 | 'actions' => ['logout'], 27 | 'allow' => true, 28 | 'roles' => ['@'], 29 | ], 30 | ], 31 | ], 32 | 'verbs' => [ 33 | 'class' => VerbFilter::className(), 34 | 'actions' => [ 35 | 'logout' => ['post'], 36 | ], 37 | ], 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function actions() 45 | { 46 | return [ 47 | 'error' => [ 48 | 'class' => 'yii\web\ErrorAction', 49 | ], 50 | 'captcha' => [ 51 | 'class' => 'yii\captcha\CaptchaAction', 52 | 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, 53 | ], 54 | ]; 55 | } 56 | 57 | /** 58 | * Displays homepage. 59 | * 60 | * @return string 61 | */ 62 | public function actionIndex() 63 | { 64 | return $this->render('index'); 65 | } 66 | 67 | /** 68 | * Login action. 69 | * 70 | * @return Response|string 71 | */ 72 | public function actionLogin() 73 | { 74 | if (!Yii::$app->user->isGuest) { 75 | return $this->goHome(); 76 | } 77 | 78 | $model = new LoginForm(); 79 | if ($model->load(Yii::$app->request->post()) && $model->login()) { 80 | return $this->goBack(); 81 | } 82 | 83 | $model->password = ''; 84 | return $this->render('login', [ 85 | 'model' => $model, 86 | ]); 87 | } 88 | 89 | /** 90 | * Logout action. 91 | * 92 | * @return Response 93 | */ 94 | public function actionLogout() 95 | { 96 | Yii::$app->user->logout(); 97 | 98 | return $this->goHome(); 99 | } 100 | 101 | /** 102 | * Displays contact page. 103 | * 104 | * @return Response|string 105 | */ 106 | public function actionContact() 107 | { 108 | $model = new ContactForm(); 109 | if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) { 110 | Yii::$app->session->setFlash('contactFormSubmitted'); 111 | 112 | return $this->refresh(); 113 | } 114 | return $this->render('contact', [ 115 | 'model' => $model, 116 | ]); 117 | } 118 | 119 | /** 120 | * Displays about page. 121 | * 122 | * @return string 123 | */ 124 | public function actionAbout() 125 | { 126 | return $this->render('about'); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /dispatchers/DummyEventDispatcher.php: -------------------------------------------------------------------------------- 1 | country = $country; 21 | $this->region = $region; 22 | $this->city = $city; 23 | $this->street = $street; 24 | $this->house = $house; 25 | } 26 | 27 | public function getCountry(): string { return $this->country; } 28 | public function getRegion(): string { return $this->region; } 29 | public function getCity(): string { return $this->city; } 30 | public function getStreet(): string { return $this->street; } 31 | public function getHouse(): string { return $this->house; } 32 | } 33 | -------------------------------------------------------------------------------- /entities/Employee/Employee.php: -------------------------------------------------------------------------------- 1 | id = $id; 41 | $this->name = $name; 42 | $this->address = $address; 43 | $this->phones = new Phones($phones); 44 | $this->createDate = $date; 45 | $this->addStatus(Status::ACTIVE, $date); 46 | $this->recordEvent(new Events\EmployeeCreated($this->id)); 47 | } 48 | 49 | public function rename(Name $name): void 50 | { 51 | $this->name = $name; 52 | $this->recordEvent(new Events\EmployeeRenamed($this->id, $name)); 53 | } 54 | 55 | public function changeAddress(Address $address): void 56 | { 57 | $this->address = $address; 58 | $this->recordEvent(new Events\EmployeeAddressChanged($this->id, $address)); 59 | } 60 | 61 | public function addPhone(Phone $phone): void 62 | { 63 | $this->phones->add($phone); 64 | $this->recordEvent(new Events\EmployeePhoneAdded($this->id, $phone)); 65 | } 66 | 67 | public function removePhone($index): void 68 | { 69 | $phone = $this->phones->remove($index); 70 | $this->recordEvent(new Events\EmployeePhoneRemoved($this->id, $phone)); 71 | } 72 | 73 | public function archive(\DateTimeImmutable $date): void 74 | { 75 | if ($this->isArchived()) { 76 | throw new \DomainException('Employee is already archived.'); 77 | } 78 | $this->addStatus(Status::ARCHIVED, $date); 79 | $this->recordEvent(new Events\EmployeeArchived($this->id, $date)); 80 | } 81 | 82 | public function reinstate(\DateTimeImmutable $date): void 83 | { 84 | if (!$this->isArchived()) { 85 | throw new \DomainException('Employee is not archived.'); 86 | } 87 | $this->addStatus(Status::ACTIVE, $date); 88 | $this->recordEvent(new Events\EmployeeReinstated($this->id, $date)); 89 | } 90 | 91 | public function remove(): void 92 | { 93 | if (!$this->isArchived()) { 94 | throw new \DomainException('Cannot remove active employee.'); 95 | } 96 | $this->recordEvent(new Events\EmployeeRemoved($this->id)); 97 | } 98 | 99 | public function isActive(): bool 100 | { 101 | return $this->getCurrentStatus()->isActive(); 102 | } 103 | 104 | public function isArchived(): bool 105 | { 106 | return $this->getCurrentStatus()->isArchived(); 107 | } 108 | 109 | private function getCurrentStatus(): Status 110 | { 111 | return end($this->statuses); 112 | } 113 | 114 | private function addStatus($value, \DateTimeImmutable $date): void 115 | { 116 | $this->statuses[] = new Status($value, $date); 117 | } 118 | 119 | public function getId(): Id { return $this->id; } 120 | public function getName(): Name { return $this->name; } 121 | public function getPhones(): array { return $this->phones->getAll(); } 122 | public function getAddress(): Address { return $this->address; } 123 | public function getCreateDate(): \DateTimeImmutable { return $this->createDate; } 124 | public function getStatuses(): array { return $this->statuses; } 125 | } 126 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeAddressChanged.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 16 | $this->address = $address; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeArchived.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 15 | $this->date = $date; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeCreated.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeePhoneAdded.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 16 | $this->phone = $phone; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeePhoneRemoved.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 16 | $this->phone = $phone; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeReinstated.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 15 | $this->date = $date; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeRemoved.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /entities/Employee/Events/EmployeeRenamed.php: -------------------------------------------------------------------------------- 1 | employee = $employee; 16 | $this->name = $name; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /entities/Employee/Id.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | } 18 | 19 | public static function next(): self 20 | { 21 | return new self(Uuid::uuid4()->toString()); 22 | } 23 | 24 | public function getId(): string 25 | { 26 | return $this->id; 27 | } 28 | 29 | public function isEqualTo(self $other): bool 30 | { 31 | return $this->getId() === $other->getId(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /entities/Employee/Name.php: -------------------------------------------------------------------------------- 1 | last = $last; 19 | $this->first = $first; 20 | $this->middle = $middle; 21 | } 22 | 23 | public function getFull(): string 24 | { 25 | return trim($this->last . ' ' . $this->first . ' ' . $this->middle); 26 | } 27 | 28 | public function getFirst(): string { return $this->first; } 29 | public function getMiddle(): ?string { return $this->middle; } 30 | public function getLast(): string { return $this->last; } 31 | } 32 | -------------------------------------------------------------------------------- /entities/Employee/Phone.php: -------------------------------------------------------------------------------- 1 | country = $country; 20 | $this->code = $code; 21 | $this->number = $number; 22 | } 23 | 24 | public function isEqualTo(self $phone): bool 25 | { 26 | return $this->getFull() === $phone->getFull(); 27 | } 28 | 29 | public function getFull(): string 30 | { 31 | return '+' . $this->country . ' (' . $this->code . ') ' . $this->number; 32 | } 33 | 34 | public function getCountry(): int { return $this->country; } 35 | public function getCode(): string { return $this->code; } 36 | public function getNumber(): string { return $this->number; } 37 | } 38 | -------------------------------------------------------------------------------- /entities/Employee/Phones.php: -------------------------------------------------------------------------------- 1 | add($phone); 19 | } 20 | } 21 | 22 | public function add(Phone $phone): void 23 | { 24 | foreach ($this->phones as $item) { 25 | if ($item->isEqualTo($phone)) { 26 | throw new \DomainException('Phone already exists.'); 27 | } 28 | } 29 | $this->phones[] = $phone; 30 | } 31 | 32 | public function remove($index): Phone 33 | { 34 | if (!isset($this->phones[$index])) { 35 | throw new \DomainException('Phone is not found.'); 36 | } 37 | if (\count($this->phones) === 1) { 38 | throw new \DomainException('Cannot remove the last phone.'); 39 | } 40 | $phone = $this->phones[$index]; 41 | unset($this->phones[$index]); 42 | return $phone; 43 | } 44 | 45 | public function getAll(): array 46 | { 47 | return $this->phones; 48 | } 49 | } -------------------------------------------------------------------------------- /entities/Employee/Status.php: -------------------------------------------------------------------------------- 1 | value = $value; 23 | $this->date = $date; 24 | } 25 | 26 | public function isActive(): bool 27 | { 28 | return $this->value === self::ACTIVE; 29 | } 30 | 31 | public function isArchived(): bool 32 | { 33 | return $this->value === self::ARCHIVED; 34 | } 35 | 36 | public function getValue(): string { return $this->value; } 37 | public function getDate(): \DateTimeImmutable { return $this->date; } 38 | } -------------------------------------------------------------------------------- /entities/EventTrait.php: -------------------------------------------------------------------------------- 1 | events[] = $event; 12 | } 13 | 14 | public function releaseEvents(): array 15 | { 16 | $events = $this->events; 17 | $this->events = []; 18 | return $events; 19 | } 20 | } -------------------------------------------------------------------------------- /mail/layouts/html.php: -------------------------------------------------------------------------------- 1 | 8 | beginPage() ?> 9 | 10 | 11 |
12 | 13 |The path to yii framework seems to be incorrect.
\n" 34 | . 'You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . ".
\n" 35 | . 'Please refer to the README on how to install Yii.
\n"; 36 | 37 | if (!empty($_SERVER['argv'])) { 38 | // do not print HTML when used in console mode 39 | echo strip_tags($message); 40 | } else { 41 | echo $message; 42 | } 43 | exit(1); 44 | } 45 | 46 | require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); 47 | $requirementsChecker = new YiiRequirementChecker(); 48 | 49 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; 50 | $gdOK = $imagickOK = false; 51 | 52 | if (extension_loaded('imagick')) { 53 | $imagick = new Imagick(); 54 | $imagickFormats = $imagick->queryFormats('PNG'); 55 | if (in_array('PNG', $imagickFormats)) { 56 | $imagickOK = true; 57 | } else { 58 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; 59 | } 60 | } 61 | 62 | if (extension_loaded('gd')) { 63 | $gdInfo = gd_info(); 64 | if (!empty($gdInfo['FreeType Support'])) { 65 | $gdOK = true; 66 | } else { 67 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; 68 | } 69 | } 70 | 71 | /** 72 | * Adjust requirements according to your application specifics. 73 | */ 74 | $requirements = array( 75 | // Database : 76 | array( 77 | 'name' => 'PDO extension', 78 | 'mandatory' => true, 79 | 'condition' => extension_loaded('pdo'), 80 | 'by' => 'All DB-related classes', 81 | ), 82 | array( 83 | 'name' => 'PDO SQLite extension', 84 | 'mandatory' => false, 85 | 'condition' => extension_loaded('pdo_sqlite'), 86 | 'by' => 'All DB-related classes', 87 | 'memo' => 'Required for SQLite database.', 88 | ), 89 | array( 90 | 'name' => 'PDO MySQL extension', 91 | 'mandatory' => false, 92 | 'condition' => extension_loaded('pdo_mysql'), 93 | 'by' => 'All DB-related classes', 94 | 'memo' => 'Required for MySQL database.', 95 | ), 96 | array( 97 | 'name' => 'PDO PostgreSQL extension', 98 | 'mandatory' => false, 99 | 'condition' => extension_loaded('pdo_pgsql'), 100 | 'by' => 'All DB-related classes', 101 | 'memo' => 'Required for PostgreSQL database.', 102 | ), 103 | // Cache : 104 | array( 105 | 'name' => 'Memcache extension', 106 | 'mandatory' => false, 107 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), 108 | 'by' => 'MemCache', 109 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached totrue
.' : ''
110 | ),
111 | // CAPTCHA:
112 | array(
113 | 'name' => 'GD PHP extension with FreeType support',
114 | 'mandatory' => false,
115 | 'condition' => $gdOK,
116 | 'by' => 'Captcha',
117 | 'memo' => $gdMemo,
118 | ),
119 | array(
120 | 'name' => 'ImageMagick PHP extension with PNG support',
121 | 'mandatory' => false,
122 | 'condition' => $imagickOK,
123 | 'by' => 'Captcha',
124 | 'memo' => $imagickMemo,
125 | ),
126 | // PHP ini :
127 | 'phpExposePhp' => array(
128 | 'name' => 'Expose PHP',
129 | 'mandatory' => false,
130 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
131 | 'by' => 'Security reasons',
132 | 'memo' => '"expose_php" should be disabled at php.ini',
133 | ),
134 | 'phpAllowUrlInclude' => array(
135 | 'name' => 'PHP allow url include',
136 | 'mandatory' => false,
137 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
138 | 'by' => 'Security reasons',
139 | 'memo' => '"allow_url_include" should be disabled at php.ini',
140 | ),
141 | 'phpSmtp' => array(
142 | 'name' => 'PHP mail SMTP',
143 | 'mandatory' => false,
144 | 'condition' => strlen(ini_get('SMTP')) > 0,
145 | 'by' => 'Email sending',
146 | 'memo' => 'PHP mail SMTP server required',
147 | ),
148 | );
149 |
150 | // OPcache check
151 | if (!version_compare(phpversion(), '5.5', '>=')) {
152 | $requirements[] = array(
153 | 'name' => 'APC extension',
154 | 'mandatory' => false,
155 | 'condition' => extension_loaded('apc'),
156 | 'by' => 'ApcCache',
157 | );
158 | }
159 |
160 | $requirementsChecker->checkYii()->check($requirements)->render();
161 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/services/EmployeeService.php:
--------------------------------------------------------------------------------
1 | employees = $employees;
27 | $this->dispatcher = $dispatcher;
28 | }
29 |
30 | public function create(EmployeeCreateDto $dto): void
31 | {
32 | $employee = new Employee(
33 | Id::next(),
34 | new \DateTimeImmutable(),
35 | new Name(
36 | $dto->name->last,
37 | $dto->name->first,
38 | $dto->name->middle
39 | ),
40 | new Address(
41 | $dto->address->country,
42 | $dto->address->region,
43 | $dto->address->city,
44 | $dto->address->street,
45 | $dto->address->house
46 | ),
47 | array_map(static function (PhoneDto $phone) {
48 | return new Phone(
49 | $phone->country,
50 | $phone->code,
51 | $phone->number
52 | );
53 | }, $dto->phones)
54 | );
55 | $this->employees->add($employee);
56 | $this->dispatcher->dispatch($employee->releaseEvents());
57 | }
58 |
59 | public function rename(Id $id, NameDto $dto): void
60 | {
61 | $employee = $this->employees->get($id);
62 | $employee->rename(new Name(
63 | $dto->last,
64 | $dto->first,
65 | $dto->middle
66 | ));
67 | $this->employees->save($employee);
68 | $this->dispatcher->dispatch($employee->releaseEvents());
69 | }
70 |
71 | public function changeAddress(Id $id, AddressDto $dto): void
72 | {
73 | $employee = $this->employees->get($id);
74 | $employee->changeAddress(new Address(
75 | $dto->country,
76 | $dto->region,
77 | $dto->city,
78 | $dto->street,
79 | $dto->house
80 | ));
81 | $this->employees->save($employee);
82 | $this->dispatcher->dispatch($employee->releaseEvents());
83 | }
84 |
85 | public function addPhone(Id $id, PhoneDto $dto): void
86 | {
87 | $employee = $this->employees->get($id);
88 | $employee->addPhone(new Phone(
89 | $dto->country,
90 | $dto->code,
91 | $dto->number
92 | ));
93 | $this->employees->save($employee);
94 | $this->dispatcher->dispatch($employee->releaseEvents());
95 | }
96 |
97 | public function removePhone(Id $id, $index): void
98 | {
99 | $employee = $this->employees->get($id);
100 | $employee->removePhone($index);
101 | $this->employees->save($employee);
102 | $this->dispatcher->dispatch($employee->releaseEvents());
103 | }
104 |
105 | public function archive(Id $id, EmployeeArchiveDto $dto): void
106 | {
107 | $employee = $this->employees->get($id);
108 | $employee->archive($dto->date);
109 | $this->employees->save($employee);
110 | $this->dispatcher->dispatch($employee->releaseEvents());
111 | }
112 |
113 | public function reinstate(Id $id, EmployeeReinstateDto $dto): void
114 | {
115 | $employee = $this->employees->get($id);
116 | $employee->reinstate($dto->date);
117 | $this->employees->save($employee);
118 | $this->dispatcher->dispatch($employee->releaseEvents());
119 | }
120 |
121 | public function remove(Id $id): void
122 | {
123 | $employee = $this->employees->get($id);
124 | $employee->remove();
125 | $this->employees->remove($employee);
126 | $this->dispatcher->dispatch($employee->releaseEvents());
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/services/dto/AddressDto.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/about'));
10 | $I->see('About', 'h1');
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/acceptance/ContactCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/contact'));
10 | }
11 |
12 | public function contactPageWorks(AcceptanceTester $I)
13 | {
14 | $I->wantTo('ensure that contact page works');
15 | $I->see('Contact', 'h1');
16 | }
17 |
18 | public function contactFormCanBeSubmitted(AcceptanceTester $I)
19 | {
20 | $I->amGoingTo('submit contact form with correct data');
21 | $I->fillField('#contactform-name', 'tester');
22 | $I->fillField('#contactform-email', 'tester@example.com');
23 | $I->fillField('#contactform-subject', 'test subject');
24 | $I->fillField('#contactform-body', 'test content');
25 | $I->fillField('#contactform-verifycode', 'testme');
26 |
27 | $I->click('contact-button');
28 |
29 | $I->wait(2); // wait for button to be clicked
30 |
31 | $I->dontSeeElement('#contact-form');
32 | $I->see('Thank you for contacting us. We will respond to you as soon as possible.');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/acceptance/HomeCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/index'));
10 | $I->see('My Company');
11 |
12 | $I->seeLink('About');
13 | $I->click('About');
14 | $I->wait(2); // wait for page to be opened
15 |
16 | $I->see('This is the About page.');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/acceptance/LoginCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/login'));
10 | $I->see('Login', 'h1');
11 |
12 | $I->amGoingTo('try to login with correct credentials');
13 | $I->fillField('input[name="LoginForm[username]"]', 'admin');
14 | $I->fillField('input[name="LoginForm[password]"]', 'admin');
15 | $I->click('login-button');
16 | $I->wait(2); // wait for button to be clicked
17 |
18 | $I->expectTo('see user info');
19 | $I->see('Logout');
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/acceptance/_bootstrap.php:
--------------------------------------------------------------------------------
1 | [
21 | 'db' => require __DIR__ . '/../../config/test_db.php'
22 | ]
23 | ]
24 | );
25 |
26 |
27 | $application = new yii\console\Application($config);
28 | $exitCode = $application->run();
29 | exit($exitCode);
30 |
--------------------------------------------------------------------------------
/tests/bin/yii.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem
6 | rem @author Qiang Xue 14 | This is the About page. You may modify the following file to customize its content: 15 |
16 | 17 |= __FILE__ ?>
18 |
24 | Note that if you turn on the Yii debugger, you should be able
25 | to view the mail message on the mail panel of the debugger.
26 | mailer->useFileTransport): ?>
27 | Because the application is in development mode, the email is not sent but saved as
28 | a file under = Yii::getAlias(Yii::$app->mailer->fileTransportPath) ?>
.
29 | Please configure the useFileTransport
property of the mail
30 | application component to be false to enable email sending.
31 |
32 |
37 | If you have business inquiries or other questions, please fill out the following form to contact us. 38 | Thank you. 39 |
40 | 41 |21 | The above error occurred while the Web server was processing your request. 22 |
23 |24 | Please contact us if you think this is a server error. Thank you. 25 |
26 | 27 |You have successfully created your Yii-powered application.
13 | 14 | 15 |Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 24 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 25 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 26 | fugiat nulla pariatur.
27 | 28 | 29 |Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 34 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 35 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 36 | fugiat nulla pariatur.
37 | 38 | 39 |Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 44 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 45 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 46 | fugiat nulla pariatur.
47 | 48 | 49 |Please fill out the following fields to login:
17 | 18 | 'login-form', 20 | 'layout' => 'horizontal', 21 | 'fieldConfig' => [ 22 | 'template' => "{label}\napp\models\User::$users
.
46 |