Skip to content

Commit

Permalink
Merge 44206ec into c9e2611
Browse files Browse the repository at this point in the history
  • Loading branch information
albertborsos committed Oct 3, 2019
2 parents c9e2611 + 44206ec commit b182c0b
Show file tree
Hide file tree
Showing 170 changed files with 8,352 additions and 4,726 deletions.
2 changes: 1 addition & 1 deletion .coveralls.yml
@@ -1,4 +1,4 @@
service_name: travis-ci

coverage_clover: tests/_output/coverage.xml
json_path: tests/_output/unit.json
json_path: tests/_output/unit.json
10 changes: 9 additions & 1 deletion .gitignore
@@ -1,3 +1,11 @@
/vendor
composer.lock

/tests/config/*.local.php

/tests/runtime/cache/
!/tests/runtime/cache/.gitkeep

/tests/runtime/web/assets/
!/tests/runtime/web/assets/.gitkeep

/vendor
17 changes: 14 additions & 3 deletions .travis.yml
Expand Up @@ -11,20 +11,31 @@ dist: trusty
# faster builds on new travis setup not using sudo
sudo: false

services:
- mysql

# cache vendor dirs
cache:
directories:
- $HOME/.composer/cache

env:
matrix:
- COMPOSER_OPTIONS="--prefer-lowest --prefer-stable"
- COMPOSER_OPTIONS=""
- COMPOSER_OPTIONS="--prefer-lowest --prefer-stable"
- COMPOSER_OPTIONS=""
global:
- DB_TEST_DSN="mysql:host=localhost;dbname=database"
- DB_USERNAME=travis
- DB_PASSWORD=

before_install:
- mysql -e 'CREATE DATABASE IF NOT EXISTS `database`;'

install:
- travis_retry composer self-update && composer --version
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- travis_retry composer install --prefer-dist --no-interaction
- travis_retry composer update --prefer-dist --no-interaction $COMPOSER_OPTIONS
- php tests/bin/yii migrate --interactive=0

script:
- sh ./phpcs.sh
Expand Down
222 changes: 180 additions & 42 deletions README.md
Expand Up @@ -5,6 +5,13 @@ DDD Classes for Yii 2.0
=======================
Classes for a Domain-Driven Design inspired workflow with Yii 2.0 Framework

Summary
--------
- develop with DDD metodology, but you can use ActiveRecord classes
- decouple business logic from ActiveRecord models into an Entity class
- decouple database queries into a repository class
- encapsulate business logic for different scenarios into a dedicated form and a service class

Installation
------------

Expand All @@ -21,72 +28,158 @@ to the require section of your `composer.json` file.
Usage
-----

`TL;DR; check the tests folder for live examples`

Lets see an example with a standard `App` model which is an implementation of `\yii\db\ActiveRecord` and generated via `gii`.
I recommend to do not make any modification with this class, but make it `abstract` to prevent direct usages.
I recommend to do not make any modification with this class.

Then create a business model which extends our abstract active record class and implements `BusinessObject` interface.
Then create an entity which extends `\albertborsos\ddd\models\AbstractEntity` class and implements `\albertborsos\ddd\interfaces\EntityInterface` interface.
Every business logic will be implemented in this class.

```php
<?php

namespace application\domains\app\business;
namespace application\domains\customer\entities;

use application\domains\app\activerecords\AbstractApp;
use albertborsos\ddd\interfaces\BusinessObject;
use application\domains\customer\entities\CustomerLanguage;

class App extends AbstractApp implements BusinessObject
class Customer extends AbstractEntity
{
// business logic
public $id;
public $name;
public $createdAt;
public $createdBy;
public $updatedAt;
public $updatedBy;

/** @var Language[] */
public $languages;

public function fields()
{
return [
'id',
'name',
'createdAt',
'createdBy',
'updatedAt',
'updatedBy',
];
}

public function extraFields()
{
return [
'languages',
];
}
}
```

Now this class is fully decoupled from the underlying storage.

For every entity you need to define a repository which handles the communication between the application and the storage.
For `ActiveRecord` usage you can use the `AbstractActiveRecordRepository` class, which has fully functional methods and you only need to implement the following way:

```php
<?php

namespace applcation\domains\customer\mysql;

use albertborsos\ddd\repositories\AbstractActiveRepository;
use albertborsos\ddd\data\CycleDataProvider;
use application\domains\customer\interfaces\CustomerActiveRepositoryInterface;

class CustomerActiveRepository extends AbstractActiveRepository implements CustomerActiveRepositoryInterface
{
protected $dataModelClass = \application\domains\customer\mysql\Customer::class;

protected $entityClass = \application\domains\customer\entities\Customer::class;

/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @param string $formName
* @return CycleDataProvider
* @throws \yii\base\InvalidConfigException
*/
public function search($params, $formName = null): BaseDataProvider
{
// same as it would be a `CustomerQuery` instance
// check `tests/_support/base/domains/customer/mysql/CustomerActiveRepository.php` for a live example
}
}

```

Then you have to configure the DI container in the application/module configuration:

```php
return [

...

'container' => [
'definitions' => [
\application\domains\customer\interfaces\CustomerActiveRepositoryInterface::class => \application\domains\customer\mysql\CustomerActiveRepository::class,
\application\domains\customer\interfaces\CustomerLanguageActiveRepositoryInterface::class => \application\domains\customer\mysql\CustomerLanguageActiveRepository::class,
],
],

...

];
```

#### Lets create a new record!

We will need a new `FormObject` which will be responsible for the data validation.
And we will need a `service` model, which handles the business logic with the related models too.


A simple example for a `FormObject`:

```php
<?php

namespace application\services\app\forms;
namespace application\services\customer\forms;

use yii\base\Model;
use albertborsos\ddd\interfaces\FormObject;
use application\domains\customer\entities\Customer;

class CreateAppForm extends Model implements FormObject
class CreateCustomerForm extends Customer implements FormObject
{
public $name;
public $languages;
public $contactLanguages;

public function rules()
{
return [
[['name', 'languages'], 'required'],
[['languages'], 'each', 'rule' => ['in', 'range' => ['en', 'de', 'hu']]],
[['name'], 'trim'],
[['name'], 'default'],
[['name'], 'required'],
[['contactLanguages'], 'each', 'rule' => ['in', 'range' => ['en', 'de', 'hu']]],
];
}
}

```

And a simple example for a `service`. Services are expecting that the values in the `FormObject` are valid values.
Services are expecting that the values in the `FormObject` are valid values.
That is why it is just store the values. The validation will be handled in the controller.

```php
<?php

namespace application\services\app;
namespace application\services\customer;

use albertborsos\ddd\models\AbstractService;
use application\services\app\forms\CreateAppLanguageForm;
use application\domains\app\business\App;
use application\domains\customer\entities\Customer;
use application\services\customer\forms\CreateCustomerForm;
use yii\base\Exception;

class CreateAppService extends AbstractService
class CreateCustomerService extends AbstractService
{
/**
* Business logic to store data for multiple resources.
Expand All @@ -95,65 +188,66 @@ class CreateAppService extends AbstractService
*/
public function execute()
{
try {
$model = new App();
$model->load($this->getForm()->attributes, '');
/** @var CreateCustomerForm $form */
$form = $this->getForm();

if ($model->save()) {
$this->assignLanguages($model->id, $this->getForm()->languages);
$this->setId($model->id);
/** @var Customer $entity */
$entity = $this->getRepository()->hydrate([]);
$entity->setAttributes($form->attributes, false);

return true;
}
} catch(\yii\db\Exception $e) {
$this->getForm()->addErrors(['exception' => $e->getMessage()]);
if ($this->getRepository()->insert($entity)) {
$this->setId($entity->id);
return true;
}

$form->addErrors($entity->getErrors());

return false;
}

private function assignLanguages($appId, $languageIds)
private function assignLanguages($customerId, $languageIds)
{
foreach ($languageIds as $languageId) {
$form = new CreateAppLanguageForm([
'app_id' => $appId,
$form = new CreateCustomerLanguageForm([
'customerId' => $customerId,
'language_id' => $languageId,
]);

if ($form->validate() === false) {
throw new Exception('Unable to validate language for this app');
throw new Exception('Unable to validate language for this customer');
}

$service = new CreateAppLanguageService($form);
$service = new CreateCustomerLanguageService($form);
if ($service->execute() === false) {
throw new Exception('Unable to save language for this app');
throw new Exception('Unable to save language for this customer');
}
}
}
}

```

And this is how you can use it in the controller
And this is how you can use it in a web controller:

```php
<?php

namespace application\controllers;

use Yii;
use application\services\app\forms\CreateAppForm;
use application\services\app\CreateAppService;
use application\services\customer\forms\CreateCustomerForm;
use application\services\customer\CreateCustomerService;

class AppController extends \yii\web\Controller
class CustomerController extends \yii\web\Controller
{
public function actionCreate()
{
$form = new CreateAppForm();
$form = new CreateCustomerForm();

if ($form->load(Yii::$app->request->post()) && $form->validate()) {
$service = new CreateAppService($form);
$service = new CreateCustomerService($form);
if ($service->execute()) {
AlertWidget::addSuccess('App created successfully!');
AlertWidget::addSuccess('Customer created successfully!');
return $this->redirect(['view', 'id' => $service->getId()]);
}
}
Expand All @@ -163,5 +257,49 @@ class AppController extends \yii\web\Controller
]);
}
}
```

And this is how you can use it in a REST controller:

```php
<?php

namespace application\modules\api\v1\controllers;

use Yii;
use albertborsos\rest\active\CreateAction;
use albertborsos\rest\active\DeleteAction;
use albertborsos\rest\active\IndexAction;
use albertborsos\rest\active\UpdateAction;
use albertborsos\rest\active\ViewAction;

class CustomerController extends \yii\rest\Controller
{
public $repositoryInterface = CustomerActiveRepositoryInterface::class;

public function actions()
{
return [
'index' => IndexAction::class,
'view' => ViewAction::class,
'create' => [
'class' => CreateAction::class,
'formClass' => CreateCustomerForm::class,
'serviceClass' => CreateCustomerService::class,
],
'update' => [
'class' => UpdateAction::class,
'formClass' => UpdateCustomerForm::class,
'serviceClass' => UpdateCustomerService::class,
],
'delete' => [
'class' => DeleteAction::class,
'formClass' => DeleteCustomerForm::class,
'serviceClass' => DeleteCustomerService::class,
],
'options' => OptionsAction::class,
];
}
}

```
5 changes: 5 additions & 0 deletions UPGRADE.md
@@ -0,0 +1,5 @@
Upgrading from 1.0.0
====================

- use `\albertborsos\ddd\interfaces\EntityInterface` instead of `\albertborsos\ddd\interfaces\BusinessObject`

0 comments on commit b182c0b

Please sign in to comment.