DMS Developer Documentation¶
DMS is PHP7 application framework that helps to develop maintainable and robust apps of any scale.
- Integrates with Laravel
- Promotes separation of concerns and defines clear architectural boundaries
- Build strongly typed, rich or anemic domain models to suit your application
- Fully-featured data mapper ORM
- Provides an integrated and powerful CMS/Backend framework
- Extensive scaffolding to speed up application development
- Reusuable functionality can be installed via composer packages
Dashboard¶
Overview¶
Edit¶
Code¶
<?php declare(strict_types = 1);
namespace Dms\Package\Shop\Domain\Entities\Order;
use Dms\Common\Structure\DateTime\DateTime;
use Dms\Common\Structure\Money\Money;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
use Dms\Core\Model\ValueObjectCollection;
use Dms\Core\Util\IClock;
use Dms\Package\PaymentGateway\Gateway\Response\TransactionReference;
use Dms\Package\Shop\Coupon\Core\Coupon;
use Dms\Package\Shop\Domain\Entities\Pricing\PricingType;
use Dms\Package\Shop\Domain\Exception\ShopException;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class Order extends Entity
{
const ORDER_NUMBER = 'orderNumber';
const STATUS = 'status';
const CUSTOMER_DETAILS = 'customerDetails';
const PRICING_TYPE = 'pricingType';
const ITEMS = 'items';
const DISCOUNT = 'discount';
const SHIPPING_DETAILS = 'shippingDetails';
const TAX_FEE = 'taxFee';
const COUPON = 'coupon';
const NOTES = 'notes';
const TRANSACTION_REFERENCE = 'transactionReference';
const AMOUNT_REFUNDED = 'amountRefunded';
const CREATED_AT = 'createdAt';
const METADATA = 'metadata';
/**
* @var string
*/
public $orderNumber;
/**
* @var OrderStatus
*/
public $status;
/**
* @var CustomerDetails
*/
public $customerDetails;
/**
* @var PricingType
*/
public $pricingType;
/**
* @var ValueObjectCollection|OrderItem[]
*/
public $items;
/**
* @var Money|null
*/
public $discount;
/**
* @var ShippingDetails|null
*/
public $shippingDetails;
/**
* @var Money|null
*/
public $taxFee;
/**
* @var Coupon|null
*/
public $coupon;
/**
* @var DateTime
*/
public $createdAt;
/**
* @var TransactionReference|null
*/
public $transactionReference;
/**
* @var Money
*/
public $amountRefunded;
/**
* @var ValueObjectCollection|OrderNote[]
*/
public $notes;
/**
* @var \ArrayObject
*/
public $metadata;
/**
* Order constructor.
*
* @param string $orderNumber
* @param OrderStatus $status
* @param CustomerDetails $customerDetails
* @param IClock $clock
* @param PricingType $pricingType
* @param OrderItem[] $items
* @param Money|null $discount
* @param ShippingDetails $shippingDetails
* @param Money|null $taxFee
* @param Coupon|null $coupon
* @param TransactionReference|null $transactionReference
* @param array $metadata
*/
public function __construct(
string $orderNumber,
OrderStatus $status,
CustomerDetails $customerDetails,
IClock $clock,
PricingType $pricingType,
array $items,
Money $discount = null,
ShippingDetails $shippingDetails = null,
Money $taxFee = null,
Coupon $coupon = null,
TransactionReference $transactionReference = null,
array $metadata = []
) {
parent::__construct();
$this->orderNumber = $orderNumber;
$this->status = $status;
$this->customerDetails = $customerDetails;
$this->pricingType = $pricingType;
$this->items = OrderItem::collection($items);
$this->discount = $discount;
$this->shippingDetails = $shippingDetails;
$this->taxFee = $taxFee;
$this->coupon = $coupon;
$this->transactionReference = $transactionReference;
$this->amountRefunded = new Money(0, $pricingType->currency);
$this->notes = OrderNote::collection();
$this->createdAt = new DateTime($clock->utcNow());
$this->metadata = new \ArrayObject($metadata);
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->orderNumber)->asString();
$class->property($this->status)->asObject(OrderStatus::class);
$class->property($this->customerDetails)->asObject(CustomerDetails::class);
$class->property($this->pricingType)->asObject(PricingType::class);
$class->property($this->items)->asType(OrderItem::collectionType());
$class->property($this->shippingDetails)->nullable()->asObject(ShippingDetails::class);
$class->property($this->discount)->nullable()->asObject(Money::class);
$class->property($this->taxFee)->nullable()->asObject(Money::class);
$class->property($this->coupon)->nullable()->asObject(Coupon::class);
$class->property($this->transactionReference)->nullable()->asObject(TransactionReference::class);
$class->property($this->amountRefunded)->asObject(Money::class);
$class->property($this->notes)->asType(OrderNote::collectionType());
$class->property($this->createdAt)->asObject(DateTime::class);
$class->property($this->metadata)->asObject(\ArrayObject::class);
}
/**
* @return Money
*/
public function getSubtotal() : Money
{
$amountOfMoney = new Money(0, $this->pricingType->currency);
foreach ($this->items as $item) {
/** @var OrderItem $item */
$amountOfMoney = $amountOfMoney->add($item->getTotalPrice());
}
return $amountOfMoney;
}
/**
* @return Money
*/
public function getTotal() : Money
{
$amountOfMoney = $this->getSubtotal();
if ($this->discount) {
$amountOfMoney = $amountOfMoney->subtract($this->discount);
}
if ($this->shippingDetails && $this->shippingDetails->shippingFee) {
$amountOfMoney = $amountOfMoney->add($this->shippingDetails->shippingFee);
}
if ($this->taxFee) {
$amountOfMoney = $amountOfMoney->add($this->taxFee);
}
return $amountOfMoney;
}
/**
* @return bool
*/
public function requiresShipping() : bool
{
return $this->items->any(function (OrderItem $item) {
return $item->requiresShipping;
});
}
/**
* @return Money
* @throws ShopException
*/
public function getMaxRefundAmount() : Money
{
if (!$this->transactionReference) {
throw new ShopException('The order has no transaction');
}
return $this->transactionReference->amount->subtract($this->amountRefunded);
}
}
User Guide¶
Prologue¶
The DMS framework was borne out of frustration from multiple attempts at creating maintainable applications using the Laravel framework. Although possible, the Laravel structure does not encourage clear separation of concerns which could cause problems when building complex applications.
A common requirement of projects is for content and functionality to be manageable through an admin panel. These can be tedious and time consuming to set up on a per-project basis.
The DMS aims to alleviate these issues by encouraging a clear application structure, inspired by concepts from DDD and Hexagonal Architecture, to allow complexity to be expressed through robust and maintainable code. While also providing an integrated CMS framework to build powerful backends quickly and easily.
Terminology¶
Building on concepts of DDD, this documentation uses similar language and jargon. See glossary.
Architecture¶
All applications must define their business logic somewhere in the application. Every developer has their own opinion on how to structure an application, the DMS takes an approach where the application is separated into layers. The functionality of an application should be contained within service classes which expose an API to be used by each application entry point, whether it be the web UI, CLI or CMS.
Getting Started¶
Requirements¶
- PHP 7.0+
- Laravel 5.4+
Installation¶
It is recommended to install the DMS via Composer in a fresh Laravel project.
Create a new Laravel project More details
composer create-project --prefer-dist laravel/laravel your-project-name cd your-project-name
Edit the
.env
file and enter the correct database credentials.# Enter the correct database credentails DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
Install the latest version of DMS via Composer
composer require dms-org/web.laravel
Add the service provider your
config/app.php
/* * Package Service Providers... */ Dms\Web\Laravel\DmsServiceProvider::class,
Run the DMS installation command
php artisan dms:install
Visit
http://your-app-domain/dms
to view the backend of your new projectYou can login with the default user account. U: admin P: admin.
Your First App¶
To illustrate how to build apps using the DMS, let’s go through building a simple TODO list app. See installation guide if necessary
Defining your domain model¶
Firstly lets build our entities. Lets create a file app/Domain/Entities/TodoItem.php
.
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
/**
* Your first entity.
*
* An item on the TODO list.
*/
class TodoItem extends Entity
{
// Define constants for property names
const DESCRIPTION = 'description';
const COMPLETED = 'completed';
/**
* @var string
*/
public $description;
/**
* @var bool
*/
public $completed;
/**
* Initialises a new TODO item.
*
* @param string $description
*/
public function __construct(string $description)
{
parent::__construct();
$this->description = $description;
$this->completed = false;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
// Enables strong typing for this entity
$class->property($this->description)->asString();
$class->property($this->completed)->asBool();
}
}
Building your persistence layer¶
So now we have our entities, we need a way to persist them to a database.
Defining the mapper¶
Create the entity mapper under app/Infrastructure/Persistence/TodoItemMapper.php
.
Configure how to map the entity to the database table.
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\TodoItem;
/**
* Your first entity mapper.
*
* The App\Domain\Entities\TodoItem entity mapper.
*/
class TodoItemMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(TodoItem::class);
$map->toTable('todo_items');
$map->idToPrimaryKey('id');
$map->property(TodoItem::DESCRIPTION)->to('description')->asVarchar(255);
$map->property(TodoItem::COMPLETED)->to('completed')->asBool();
}
}
Register the mapper in app/AppOrm.php
:
<?php declare(strict_types = 1);
namespace App;
use App\Domain\Entities\TodoItem;
use App\Infrastructure\Persistence\TodoItemMapper;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Web\Laravel\Persistence\Db\DmsOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
$orm->enableLazyLoading();
$orm->encompass(DmsOrm::inDefaultNamespace());
// You can register your mappers here
$orm->entities([
TodoItem::class => TodoItemMapper::class
]);
}
}
Defining the repository interface¶
Create the repository interface app/Domain/Services/ITodoItemRepository.php
.
<?php declare(strict_types = 1);
namespace App\Domain\Services\Persistence;
use Dms\Core\Model\ICriteria;
use Dms\Core\Model\ISpecification;
use Dms\Core\Persistence\IRepository;
use App\Domain\Entities\TodoItem;
/**
* The repository for the App\Domain\Entities\TodoItem entity.
*/
interface ITodoItemRepository extends IRepository
{
/**
* {@inheritDoc}
*
* @return TodoItem[]
*/
public function getAll() : array;
/**
* {@inheritDoc}
*
* @return TodoItem
*/
public function get($id);
/**
* {@inheritDoc}
*
* @return TodoItem[]
*/
public function getAllById(array $ids) : array;
/**
* {@inheritDoc}
*
* @return TodoItem|null
*/
public function tryGet($id);
/**
* {@inheritDoc}
*
* @return TodoItem[]
*/
public function tryGetAll(array $ids) : array;
/**
* {@inheritDoc}
*
* @return TodoItem[]
*/
public function matching(ICriteria $criteria) : array;
/**
* {@inheritDoc}
*
* @return TodoItem[]
*/
public function satisfying(ISpecification $specification) : array;
}
Defining the repository implementation¶
Create the repository implementation app/Infrastructure/Persistence/DbTodoItemRepository.php
.
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Connection\IConnection;
use Dms\Core\Persistence\Db\Mapping\IOrm;
use Dms\Core\Persistence\DbRepository;
use App\Domain\Services\Persistence\ITodoItemRepository;
use App\Domain\Entities\TodoItem;
/**
* The database repository implementation for the App\Domain\Entities\TodoItem entity.
*/
class DbTodoItemRepository extends DbRepository implements ITodoItemRepository
{
public function __construct(IConnection $connection, IOrm $orm)
{
parent::__construct($connection, $orm->getEntityMapper(TodoItem::class));
}
// Custom queries can go here...
}
Bind your repository implementation to the interface in app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use App\AppCms;
use App\AppOrm;
use App\Domain\Services\Persistence\ITodoItemRepository;
use App\Infrastructure\Persistence\DbTodoItemRepository;
use Dms\Core\ICms;
use Dms\Core\Persistence\Db\Mapping\IOrm;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(IOrm::class, AppOrm::class);
$this->app->singleton(ICms::class, AppCms::class);
// Bind your repositories here
$this->app->singleton(ITodoItemRepository::class, DbTodoItemRepository::class);
}
}
Migrating the database¶
To sync the database we must generate a migration and run it:
# Auto-generate a migration to sync the database
php artisan dms:make:migration add_todo_items_table
# Run the migration
php artisan migrate
You can review the generated migration under the database/migrations/
directory
Building the CMS¶
Defining your modules¶
To build the backend we must first configure the module app/Cms/Modules/TodoItemModule.php
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\ITodoItemRepository;
use App\Domain\Entities\TodoItem;
use Dms\Common\Structure\Field;
/**
* The todo-item module.
*/
class TodoItemModule extends CrudModule
{
public function __construct(ITodoItemRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('todo-item');
$module->labelObjects()->fromProperty(TodoItem::DESCRIPTION);
$module->metadata([
'icon' => 'list', // Choose icon from http://fontawesome.io/icons/
]);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('description', 'Description')->string()->required()
)->bindToProperty(TodoItem::DESCRIPTION),
//
$form->field(
Field::create('completed', 'Completed')->bool()
)->bindToProperty(TodoItem::COMPLETED),
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(TodoItem::DESCRIPTION)->to(Field::create('description', 'Description')->string()->required());
$table->mapProperty(TodoItem::COMPLETED)->to(Field::create('completed', 'Completed')->bool());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Defining your package¶
Create the package (a group of modules) app/Cms/TodoAppPackage.php
<?php declare(strict_types = 1);
namespace App\Cms;
use Dms\Core\Package\Definition\PackageDefinition;
use Dms\Core\Package\Package;
use App\Cms\Modules\TodoItemModule;
/**
* The todo-app package.
*/
class TodoAppPackage extends Package
{
/**
* Defines the structure of this cms package.
*
* @param PackageDefinition $package
*
* @return void
*/
protected function define(PackageDefinition $package)
{
$package->name('todo-app');
$package->metadata([
'icon' => 'check-square', // Choose icon from http://fontawesome.io/icons/
]);
$package->modules([
'todo-item' => TodoItemModule::class,
]);
}
}
Register the package in app/AppCms.php
<?php declare(strict_types=1);
namespace App;
use App\Cms\TodoAppPackage;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Package\Analytics\AnalyticsPackage;
use Dms\Web\Laravel\Auth\AdminPackage;
use Dms\Web\Laravel\Document\PublicFilePackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Default packages installed out of the box
'admin' => AdminPackage::class,
'documents' => PublicFilePackage::class,
'analytics' => AnalyticsPackage::class,
// Register your packages here...
'todo-app' => TodoAppPackage::class,
]);
}
}
Visit http://your-app-domain/dms
and login to see your first backend:
Scaffolding¶
For each and every project, setting up your entities, mappers, modules can feel repetitive and painful. The DMS provides powerful scaffolding commands that allow you to generate mappers for the ORM and modules for the CMS out of the box.
Scaffolding the ORM¶
php artisan dms:scaffold:persistence
Running this command will look for entities in the App\Domain\Entities
namespace
and auto-generate your persistence layer.
- Mapper and repository classes will be put under
app/Infrastructure/Pesistence
- Repository interfaces will be put under
app/Domain/Services/Pesistence
All that is left to do is register your mappers in app/AppOrm.php
<?php declare(strict_types = 1);
namespace App;
use App\Domain\Entities\TodoItem;
use App\Infrastructure\Persistence\TodoItemMapper;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Web\Laravel\Persistence\Db\DmsOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
$orm->enableLazyLoading();
$orm->encompass(DmsOrm::inDefaultNamespace());
// You can register your mappers here
$orm->entities([
TodoItem::class => TodoItemMapper::class
]);
}
}
And to bind your repository interfaces to the implementations
<?php
namespace App\Providers;
use App\AppCms;
use App\AppOrm;
use App\Domain\Services\Persistence\ITodoItemRepository;
use App\Infrastructure\Persistence\DbTodoItemRepository;
use Dms\Core\ICms;
use Dms\Core\Persistence\Db\Mapping\IOrm;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(IOrm::class, AppOrm::class);
$this->app->singleton(ICms::class, AppCms::class);
// Bind your repositories here
$this->app->singleton(ITodoItemRepository::class, DbTodoItemRepository::class);
}
}
Generating Migrations¶
php artisan dms:make:migration your_migration_name
Running this command will look at the current structure of your mappers and the current database structure and generate a migration class to sync your database with the current entity structure.
- A migration will be generated under
database/migrations
An example auto-generated migration:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTodoItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('todo_items', function (Blueprint $table) {
$table->integer('id')->autoIncrement()->unsigned();
$table->string('description', 255);
$table->boolean('completed');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('todo_items');
}
}
This command only generates the migration, you will have to run
php artisan migrate
to actually run the migration
It is recommended to review the migrations to ensure the database is synced expected
Scaffolding the CMS¶
php artisan dms:scaffold:cms your-app-name
Running this command will look for entities in the App\Domain\Entities
namespace
and auto-generate your CMS layer.
- Module classes will be put under
app/Cms/Modules
- A package class will be put under
app/Cms/YouAppNamePackage.php
All that is left to do is register your package in app/AppCms.php
<?php declare(strict_types=1);
namespace App;
use App\Cms\TodoAppPackage;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Package\Analytics\AnalyticsPackage;
use Dms\Web\Laravel\Auth\AdminPackage;
use Dms\Web\Laravel\Document\PublicFilePackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Default packages installed out of the box
'admin' => AdminPackage::class,
'documents' => PublicFilePackage::class,
'analytics' => AnalyticsPackage::class,
// Register your packages here...
'todo-app' => TodoAppPackage::class,
]);
}
}
Repositories¶
Persisting entities¶
You can save (create or update) entities to the database using the repository
<?php
function saveItem(ITodoItemRepository $repo)
{
$repo->save(new TodoItem('some item'));
$repo->saveAll([
new TodoItem('an item'),
new TodoItem('another item'),
]);
}
Loading entities¶
You can load entities from the database
<?php
function getItem(ITodoItemRepository $repo)
{
// Get the entity with the id = 1, throw if not found
$entity = $repo->get(1);
// Get the entity with the id = 1, null if not found
$entity = $repo->tryGet(1);
// Get the entities with the ids 1, 2 or 3, throw if any not found
$entities = $repo->getAll([1, 2, 3]);
// Get the entity with the ids 1, 2 or 3, ignore if any not found
$entities = $repo->tryGetAll([1, 2, 3]);
}
Deleting entities¶
You can load entities from the database
<?php
function deleteItems(ITodoItemRepository $repo)
{
// Delete the entity
$repo->remove($someEntity);
// Delete the entity with the ids 1, 2 or 3
$repo->removeById(1);
// Delete the entity
$repo->removeAll([$someEntity, $anotherEntity]);
// Delete the entity with the ids 1, 2 or 3
$repo->removeAllById([1, 2, 3]);
// Delete all entities
$repo->clear();
}
Criteria¶
Simple queries can be expressed through criteria, so you don’t need to resort to SQL for most of your database operations.
<?php
function getEntitiesWithCriteria(ITodoItemRepository $repo)
{
$items = $repo->matching(
$repo->criteria()
->where(TodoItem::COMPLETED, '=', true)
);
$items = $repo->matching(
$repo->criteria()
->whereStringContainsCaseInsensitive(TodoItem::DESCRIPTION, 'some text')
->orderByDesc(TodoItem::ID)
->skip(5)
->limit(10)
);
}
function removeEntitiesWithCriteria(ITodoItemRepository $repo)
{
$repo->removeMatching(
$repo->criteria()
->whereIn(TodoItem::ID, [1, 2, 3])
);
}
Custom Queries¶
Add methods to the repository for custom database queries.
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Connection\IConnection;
use Dms\Core\Persistence\Db\Mapping\IOrm;
use Dms\Core\Persistence\DbRepository;
use App\Domain\Services\Persistence\ITodoItemRepository;
use App\Domain\Entities\TodoItem;
/**
* The database repository implementation for the App\Domain\Entities\TodoItem entity.
*/
class DbTodoItemRepository extends DbRepository implements ITodoItemRepository
{
public function __construct(IConnection $connection, IOrm $orm)
{
parent::__construct($connection, $orm->getEntityMapper(TodoItem::class));
}
/**
* Gets five random todo items
*
* @return TodoItem[]
*/
public function loadRandomItems() : array
{
return $this->loadQuery('
SELECT * FROM todo_items
ORDER BY RAND()
LIMIT :limit
', ['limit' => 5]);
}
/**
* Marks all todo items as complete
*
* @return void
*/
public function markAllItemsAsComplete()
{
$this->connection->prepare('
UPDATE todo_items
SET completed = TRUE
')->execute();
}
}
Field Types¶
There are many types of fields which you can use throughout your application, see below for examples.
Scalars¶
Native data types (strings, integers, floats, booleans) can be expressed as follows
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Person extends Entity
{
const NAME = 'name';
const AGE = 'age';
const WEIGHT = 'weight';
const HAPPY = 'happy';
/**
* @var string
*/
public $name;
/**
* @var int
*/
public $age;
/**
* @var float
*/
public $weight;
/**
* @var bool
*/
public $happy;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->age)->asInt();
$class->property($this->weight)->asFloat();
$class->property($this->happy)->asBool();
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Person;
/**
* The App\Domain\Entities\Person entity mapper.
*/
class PersonMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Person::class);
$map->toTable('people');
$map->idToPrimaryKey('id');
$map->property(Person::NAME)->to('name')->asVarchar(255);
$map->property(Person::AGE)->to('age')->asInt();
$map->property(Person::WEIGHT)->to('weight')->asDecimal(16, 8);
$map->property(Person::HAPPY)->to('happy')->asBool();
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IPersonRepository;
use App\Domain\Entities\Person;
use Dms\Common\Structure\Field;
/**
* The person module.
*/
class PersonModule extends CrudModule
{
public function __construct(IPersonRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('person');
$module->labelObjects()->fromProperty(Person::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Person::NAME),
//
$form->field(
Field::create('age', 'Age')->int()->required()
)->bindToProperty(Person::AGE),
//
$form->field(
Field::create('weight', 'Weight')->decimal()->required()
)->bindToProperty(Person::WEIGHT),
//
$form->field(
Field::create('happy', 'Happy')->bool()
)->bindToProperty(Person::HAPPY),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Person::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->mapProperty(Person::AGE)->to(Field::create('age', 'Age')->int()->required());
$table->mapProperty(Person::WEIGHT)->to(Field::create('weight', 'Weight')->decimal()->required());
$table->mapProperty(Person::HAPPY)->to(Field::create('happy', 'Happy')->bool());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Nullable¶
Fields can be defined as nullable which means they can possible contain a null
value.
This is useful for optional fields.
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class User extends Entity
{
const NAME = 'name';
/**
* @var string|null
*/
public $name;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->nullable()->asString();
}
/**
* @return bool
*/
public function isAnonymous() : bool
{
return $this->name === null;
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\User;
/**
* The App\Domain\Entities\User entity mapper.
*/
class UserMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(User::class);
$map->toTable('users');
$map->idToPrimaryKey('id');
$map->property(User::NAME)->to('name')->nullable()->asVarchar(255);
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IUserRepository;
use App\Domain\Entities\User;
use Dms\Common\Structure\Field;
/**
* The user module.
*/
class UserModule extends CrudModule
{
public function __construct(IUserRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('user');
$module->labelObjects()->fromProperty(User::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
// Omit ->required() to make the field optional
Field::create('name', 'Name')->string()
)->bindToProperty(User::NAME),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(User::NAME)->to(Field::create('name', 'Name')->string());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Enums¶
Custom enums can be defined using class constants:
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\Enum;
use Dms\Core\Model\Object\PropertyTypeDefiner;
class Colour extends Enum
{
const RED = 'red';
const GREEN = 'green';
const BLUE = 'blue';
/**
* Defines the type of the options contained within the enum.
*
* @param PropertyTypeDefiner $values
*
* @return void
*/
protected function defineEnumValues(PropertyTypeDefiner $values)
{
$values->asString();
}
// Static factory methods...
public static function red() : self
{
return new self(self::RED);
}
public static function green() : self
{
return new self(self::GREEN);
}
public static function blue() : self
{
return new self(self::BLUE);
}
}
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Car extends Entity
{
const COLOUR = 'colour';
/**
* @var Colour
*/
public $colour;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->colour)->asObject(Colour::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Car;
/**
* The App\Domain\Entities\Car entity mapper.
*/
class CarMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Car::class);
$map->toTable('cars');
$map->idToPrimaryKey('id');
$map->enum(Car::COLOUR)->to('colour')->usingValuesFromConstants();
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\ICarRepository;
use App\Domain\Entities\Car;
use Dms\Common\Structure\Field;
use App\Domain\Entities\Colour;
/**
* The car module.
*/
class CarModule extends CrudModule
{
public function __construct(ICarRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('car');
$module->labelObjects()->fromProperty(Car::ID);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('colour', 'Colour')->enum(Colour::class, [
Colour::RED => 'Red',
Colour::GREEN => 'Green',
Colour::BLUE => 'Blue',
])->required()
)->bindToProperty(Car::COLOUR),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Car::COLOUR)->to(Field::create('colour', 'Colour')->enum(Colour::class, [
Colour::RED => 'Red',
Colour::GREEN => 'Green',
Colour::BLUE => 'Blue',
])->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Files¶
Files and images can be modelled using value objects.
Dms\Common\Structure\FileSystem\File
Dms\Common\Structure\FileSystem\Image
These classes wrap an absolute file path which can reference a file stored on disk or externally.
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\FileSystem\File;
use Dms\Common\Structure\FileSystem\Image;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class House extends Entity
{
const FLOORPLAN = 'floorplan';
const PHOTO = 'photo';
/**
* @var File
*/
public $floorplan;
/**
* @var Image
*/
public $photo;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->floorplan)->asObject(File::class);
$class->property($this->photo)->asObject(Image::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\House;
use Dms\Common\Structure\FileSystem\Persistence\FileMapper;
use Dms\Common\Structure\FileSystem\Persistence\ImageMapper;
/**
* The App\Domain\Entities\House entity mapper.
*/
class HouseMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(House::class);
$map->toTable('houses');
$map->idToPrimaryKey('id');
$map->embedded(House::FLOORPLAN)
->using(new FileMapper('floorplan', 'floorplan_file_name', public_path('app/house-files')));
$map->embedded(House::PHOTO)
->using(new ImageMapper('photo', 'photo_file_name', public_path('app/house-files')));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IHouseRepository;
use App\Domain\Entities\House;
use Dms\Common\Structure\Field;
/**
* The house module.
*/
class HouseModule extends CrudModule
{
public function __construct(IHouseRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('house');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('floorplan', 'Floorplan')
->file()
->required()
->moveToPathWithRandomFileName(public_path('app/house-files'))
)->bindToProperty(House::FLOORPLAN),
//
$form->field(
Field::create('photo', 'Photo')
->image()
->required()
->moveToPathWithRandomFileName(public_path('app/house-files'))
)->bindToProperty(House::PHOTO),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Date and time¶
Date and time fields can be modelled using value objects.
Dms\Common\Structure\DateTime\Date
- A date valueeg 2000-10-12
Dms\Common\Structure\DateTime\TimeOfDay
- A time valueeg 06:12:56
Dms\Common\Structure\DateTime\DateTime
- A date and time valueeg 2000-10-12 06:12:56
Dms\Common\Structure\DateTime\TimezonedDateTime
- A date and time in a particular timezone valueeg 2000-10-12 06:12:56 Australia/Melbourne
These classes wrap a native DateTime
instance and provide more descriptive API
for each type of date/time class.
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\DateTime\Date;
use Dms\Common\Structure\DateTime\DateTime;
use Dms\Common\Structure\DateTime\TimeOfDay;
use Dms\Common\Structure\DateTime\TimezonedDateTime;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Event extends Entity
{
const DATE = 'date';
const TIME = 'time';
const DATETIME = 'dateTime';
const TIMEZONED_DATETIME = 'timezoneDateTime';
/**
* @var Date
*/
public $date;
/**
* @var TimeOfDay
*/
public $time;
/**
* @var DateTime
*/
public $dateTime;
/**
* @var TimezonedDateTime
*/
public $timezoneDateTime;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->date)->asObject(Date::class);
$class->property($this->time)->asObject(TimeOfDay::class);
$class->property($this->dateTime)->asObject(DateTime::class);
$class->property($this->timezoneDateTime)->asObject(TimezonedDateTime::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Event;
use Dms\Common\Structure\DateTime\Persistence\DateMapper;
use Dms\Common\Structure\DateTime\Persistence\TimeOfDayMapper;
use Dms\Common\Structure\DateTime\Persistence\DateTimeMapper;
use Dms\Common\Structure\DateTime\Persistence\TimezonedDateTimeMapper;
/**
* The App\Domain\Entities\Event entity mapper.
*/
class EventMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Event::class);
$map->toTable('events');
$map->idToPrimaryKey('id');
$map->embedded(Event::DATE)
->using(new DateMapper('date'));
$map->embedded(Event::TIME)
->using(new TimeOfDayMapper('time'));
$map->embedded(Event::DATETIME)
->using(new DateTimeMapper('date_time'));
$map->embedded(Event::TIMEZONED_DATETIME)
->using(new TimezonedDateTimeMapper('timezone_date_time_date_time', 'timezone_date_time_timezone'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IEventRepository;
use App\Domain\Entities\Event;
use Dms\Common\Structure\Field;
/**
* The event module.
*/
class EventModule extends CrudModule
{
public function __construct(IEventRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('event');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('date', 'Date')->date()->required()
)->bindToProperty(Event::DATE),
//
$form->field(
Field::create('time', 'Time')->time()->required()
)->bindToProperty(Event::TIME),
//
$form->field(
Field::create('date_time', 'Date Time')->dateTime()->required()
)->bindToProperty(Event::DATETIME),
//
$form->field(
Field::create('timezone_date_time', 'Timezone Date Time')->dateTimeWithTimezone()->required()
)->bindToProperty(Event::TIMEZONED_DATETIME),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Event::DATE)->to(Field::create('date', 'Date')->date()->required());
$table->mapProperty(Event::TIME)->to(Field::create('time', 'Time')->time()->required());
$table->mapProperty(Event::DATETIME)->to(Field::create('date_time', 'Date Time')->dateTime()->required());
$table->mapProperty(Event::TIMEZONED_DATETIME)->to(Field::create('timezone_date_time', 'Timezone Date Time')->dateTimeWithTimezone()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Date and time ranges¶
Date and time range fields can be modelled using value objects.
Dms\Common\Structure\DateTime\DateRange
- A date rangeeg 2000-10-12 until 2002-03-05
Dms\Common\Structure\DateTime\TimeRage
- A time range valueeg 06:12:56 until 14:40:23
Dms\Common\Structure\DateTime\DateTimeRange
- A date and time range valueeg 2000-10-12 06:12:56 until 2002-03-05 14:40:23
Dms\Common\Structure\DateTime\TimezonedDateTimeRange
- A date and time in a particular timezone range valueeg 2000-10-12 06:12:56 Australia/Melbourne until 2002-03-05 14:40:23 UTC
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\DateTime\DateRange;
use Dms\Common\Structure\DateTime\DateTimeRange;
use Dms\Common\Structure\DateTime\TimeRange;
use Dms\Common\Structure\DateTime\TimezonedDateTimeRange;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Holiday extends Entity
{
const DATE_RANGE = 'dateRange';
const TIME_RANGE = 'timeRange';
const DATETIME_RANGE = 'dateTimeRange';
const TIMEZONED_DATETIME_RANGE = 'timezoneDateTimeRange';
/**
* @var DateRange
*/
public $dateRange;
/**
* @var TimeRange
*/
public $timeRange;
/**
* @var DateTimeRange
*/
public $dateTimeRange;
/**
* @var TimezonedDateTimeRange
*/
public $timezoneDateTimeRange;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->dateRange)->asObject(DateRange::class);
$class->property($this->timeRange)->asObject(TimeRange::class);
$class->property($this->dateTimeRange)->asObject(DateTimeRange::class);
$class->property($this->timezoneDateTimeRange)->asObject(TimezonedDateTimeRange::class);
}
}
Mapper Configuration¶
<?php declare(strict_types=1);
namespace App\Infrastructure\Persistence;
use App\Domain\Entities\Holiday;
use Dms\Common\Structure\DateTime\Persistence\DateRangeMapper;
use Dms\Common\Structure\DateTime\Persistence\DateTimeRangeMapper;
use Dms\Common\Structure\DateTime\Persistence\TimeRangeMapper;
use Dms\Common\Structure\DateTime\Persistence\TimezonedDateTimeRangeMapper;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
/**
* The App\Domain\Entities\Holiday entity mapper.
*/
class HolidayMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Holiday::class);
$map->toTable('holidays');
$map->idToPrimaryKey('id');
$map->embedded(Holiday::DATE_RANGE)
->using(new DateRangeMapper('start_date', 'end_date'));
$map->embedded(Holiday::TIME_RANGE)
->using(new TimeRangeMapper('time_time', 'end_time'));
$map->embedded(Holiday::DATETIME_RANGE)
->using(new DateTimeRangeMapper('start_date_time', 'end_date_time'));
$map->embedded(Holiday::TIMEZONED_DATETIME_RANGE)
->using(new TimezonedDateTimeRangeMapper('start_date_time', 'start_timezone', 'end_date_time', 'end_timezone'));
}
}
Module Configuration¶
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use App\Domain\Entities\Holiday;
use App\Domain\Services\Persistence\IHolidayRepository;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
/**
* The holiday module.
*/
class HolidayModule extends CrudModule
{
public function __construct(IHolidayRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('holidays');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('date_range', 'Date Range')->dateRange()->required()
)->bindToProperty(Holiday::DATE_RANGE),
//
$form->field(
Field::create('time_range', 'Time Range')->timeRange()->required()
)->bindToProperty(Holiday::TIME_RANGE),
//
$form->field(
Field::create('date_time_range', 'Date Time Range')->dateTimeRange()->required()
)->bindToProperty(Holiday::DATETIME_RANGE),
//
$form->field(
Field::create('timezoned_date_time_range', 'Timezoned Date Time Range')->dateTimeWithTimezoneRange()->required()
)->bindToProperty(Holiday::TIMEZONED_DATETIME_RANGE),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Holiday::DATE_RANGE)->to(Field::create('date_range', 'Date Range')->dateRange()->required());
$table->mapProperty(Holiday::TIME_RANGE)->to(Field::create('time_range', 'Time Range')->timeRange()->required());
$table->mapProperty(Holiday::DATETIME_RANGE)->to(Field::create('date_time_range', 'Date Time Range')->dateTimeRange()->required());
$table->mapProperty(Holiday::TIMEZONED_DATETIME_RANGE)->to(Field::create('timezoned_date_time_range', 'Timezone Date Time Range')->dateTimeWithTimezoneRange()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Location¶
Geographical locations can be modelled using value objects:
Dms\Common\Structure\Geo\LatLng
- A latitude/longitude coordinateDms\Common\Structure\Geo\StreetAddress
- A street address stringDms\Common\Structure\Geo\StreetAddressWithLatLng
- A street address string and its associated lat/lng coordinates
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\Geo\LatLng;
use Dms\Common\Structure\Geo\StreetAddress;
use Dms\Common\Structure\Geo\StreetAddressWithLatLng;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Building extends Entity
{
const ADDRESS = 'address';
const LAT_LNG = 'latLng';
const ADDRESS_WITH_LAT_LNG = 'addressWithLatLng';
/**
* @var StreetAddress
*/
public $address;
/**
* @var LatLng
*/
public $latLng;
/**
* @var StreetAddressWithLatLng
*/
public $addressWithLatLng;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->address)->asObject(StreetAddress::class);
$class->property($this->latLng)->asObject(LatLng::class);
$class->property($this->addressWithLatLng)->asObject(StreetAddressWithLatLng::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Building;
use Dms\Common\Structure\Geo\Persistence\StreetAddressMapper;
use Dms\Common\Structure\Geo\Persistence\StreetAddressWithLatLngMapper;
use Dms\Common\Structure\Geo\Persistence\LatLngMapper;
/**
* The App\Domain\Entities\Building entity mapper.
*/
class BuildingMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Building::class);
$map->toTable('buildings');
$map->idToPrimaryKey('id');
$map->embedded(Building::ADDRESS)
->using(new StreetAddressMapper('address'));
$map->embedded(Building::LAT_LNG)
->using(new LatLngMapper('lat', 'lng'));
$map->embedded(Building::ADDRESS_WITH_LAT_LNG)
->using(new StreetAddressWithLatLngMapper('address1', 'lat1', 'lng1'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IBuildingRepository;
use App\Domain\Entities\Building;
use Dms\Common\Structure\Field;
/**
* The building module.
*/
class BuildingModule extends CrudModule
{
public function __construct(IBuildingRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('building');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('address', 'Address')->streetAddress()->required()
)->bindToProperty(Building::ADDRESS),
//
$form->field(
Field::create('lat_lng', 'Lat Lng')->latLng()->required()
)->bindToProperty(Building::LAT_LNG),
//
$form->field(
Field::create('address_with_lat_lng', 'Address With Lat Lng')->streetAddressWithLatLng()->required()
)->bindToProperty(Building::ADDRESS_WITH_LAT_LNG),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Building::ADDRESS)->to(Field::create('address', 'Address')->streetAddress()->required());
$table->mapProperty(Building::LAT_LNG)->to(Field::create('lat_lng', 'Lat Lng')->latLng()->required());
$table->mapProperty(Building::ADDRESS_WITH_LAT_LNG)->to(Field::create('address_with_lat_lng', 'Address With Lat Lng')->streetAddressWithLatLng()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Web¶
Miscellaneous web-related fields can be modelled using the value objects:
Dms\Common\Structure\Web\EmailAddress
- An email addressjohn@gmail.com
Dms\Common\Structure\Web\IpAddress
- An IP address123.123.123.123
Dms\Common\Structure\Web\Url
- An absolute urlhttps://www.google.com
Dms\Common\Structure\Web\Html
- A raw HTML string<p>abc</p>
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\Web\EmailAddress;
use Dms\Common\Structure\Web\Html;
use Dms\Common\Structure\Web\IpAddress;
use Dms\Common\Structure\Web\Url;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Business extends Entity
{
const EMAIL_ADDRESS = 'emailAddress';
const IP_ADDRESS = 'ipAddress';
const WEBSITE = 'website';
const DESCRIPTION = 'description';
/**
* @var EmailAddress
*/
public $emailAddress;
/**
* @var IpAddress
*/
public $ipAddress;
/**
* @var Url
*/
public $website;
/**
* @var Html
*/
public $description;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->emailAddress)->asObject(EmailAddress::class);
$class->property($this->ipAddress)->asObject(IpAddress::class);
$class->property($this->website)->asObject(Url::class);
$class->property($this->description)->asObject(Html::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Business;
use Dms\Common\Structure\Web\Persistence\EmailAddressMapper;
use Dms\Common\Structure\Web\Persistence\IpAddressMapper;
use Dms\Common\Structure\Web\Persistence\UrlMapper;
use Dms\Common\Structure\Web\Persistence\HtmlMapper;
/**
* The App\Domain\Entities\Business entity mapper.
*/
class BusinessMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Business::class);
$map->toTable('businesses');
$map->idToPrimaryKey('id');
$map->embedded(Business::EMAIL_ADDRESS)
->using(new EmailAddressMapper('email_address'));
$map->embedded(Business::IP_ADDRESS)
->using(new IpAddressMapper('ip_address'));
$map->embedded(Business::WEBSITE)
->using(new UrlMapper('website'));
$map->embedded(Business::DESCRIPTION)
->using(new HtmlMapper('description'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IBusinessRepository;
use App\Domain\Entities\Business;
use Dms\Common\Structure\Field;
/**
* The business module.
*/
class BusinessModule extends CrudModule
{
public function __construct(IBusinessRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('business');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('email_address', 'Email Address')->email()->required()
)->bindToProperty(Business::EMAIL_ADDRESS),
//
$form->field(
Field::create('ip_address', 'Ip Address')->ipAddress()->required()
)->bindToProperty(Business::IP_ADDRESS),
//
$form->field(
Field::create('website', 'Website')->url()->required()
)->bindToProperty(Business::WEBSITE),
//
$form->field(
Field::create('description', 'Description')->html()->required()
)->bindToProperty(Business::DESCRIPTION),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Business::EMAIL_ADDRESS)->to(Field::create('email_address', 'Email Address')->email()->required());
$table->mapProperty(Business::IP_ADDRESS)->to(Field::create('ip_address', 'Ip Address')->ipAddress()->required());
$table->mapProperty(Business::WEBSITE)->to(Field::create('website', 'Website')->url()->required());
$table->mapProperty(Business::DESCRIPTION)->to(Field::create('description', 'Description')->html()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Colours¶
Colours can be modelled using value objects:
Dms\Common\Structure\Colour\Colour
- A RGB colour valuergb(123, 123, 123)
Dms\Common\Structure\Colour\TransparentColour
- A RGBA colour valuergba(123, 123, 123, 0.5)
Entity Structure¶
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Common\Structure\Colour\Colour;
use Dms\Common\Structure\Colour\TransparentColour;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Photograph extends Entity
{
const BACKGROUND = 'background';
const FOREGROUND = 'foreground';
/**
* @var Colour
*/
public $background;
/**
* @var TransparentColour
*/
public $foreground;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->background)->asObject(Colour::class);
$class->property($this->foreground)->asObject(TransparentColour::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Photograph;
use Dms\Common\Structure\Colour\Mapper\ColourMapper;
use Dms\Common\Structure\Colour\Mapper\TransparentColourMapper;
/**
* The App\Domain\Entities\Photograph entity mapper.
*/
class PhotographMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Photograph::class);
$map->toTable('photographs');
$map->idToPrimaryKey('id');
$map->embedded(Photograph::BACKGROUND)
->using(ColourMapper::asHexString('background'));
$map->embedded(Photograph::FOREGROUND)
->using(TransparentColourMapper::asRgbaString('foreground'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IPhotographRepository;
use App\Domain\Entities\Photograph;
use Dms\Common\Structure\Field;
/**
* The photograph module.
*/
class PhotographModule extends CrudModule
{
public function __construct(IPhotographRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('photograph');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('background', 'Background')->colour()->required()
)->bindToProperty(Photograph::BACKGROUND),
//
$form->field(
Field::create('foreground', 'Foreground')->colourWithTransparency()->required()
)->bindToProperty(Photograph::FOREGROUND),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Photograph::BACKGROUND)->to(Field::create('background', 'Background')->colour()->required());
$table->mapProperty(Photograph::FOREGROUND)->to(Field::create('foreground', 'Foreground')->colourWithTransparency()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Money¶
Monetary values can be modelled using a value object:
Dms\Common\Structure\Money\Money
- Represents an amount of currency$123.45 AUD
Entity Structure¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Common\Structure\Money\Money;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Product extends Entity
{
const PRICE = 'price';
/**
* @var Money
*/
public $price;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->price)->asObject(Money::class);
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Product;
use Dms\Common\Structure\Money\Persistence\MoneyMapper;
/**
* The App\Domain\Entities\Product entity mapper.
*/
class ProductMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Product::class);
$map->toTable('products');
$map->idToPrimaryKey('id');
$map->embedded(Product::PRICE)
->using(new MoneyMapper('price_amount', 'price_currency'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IProductRepository;
use App\Domain\Entities\Product;
use Dms\Common\Structure\Field;
/**
* The product module.
*/
class ProductModule extends CrudModule
{
public function __construct(IProductRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('product');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('price', 'Price')->money()->required()
)->bindToProperty(Product::PRICE),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Product::PRICE)->to(Field::create('price', 'Price')->money()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Arrays/Collections¶
It is often useful to represent an array of values.
For scalar values you can use a native array
.
For a collection of value objects it is recommended you use Dms\Core\Model\ValueObjectCollection
.
For a collection of entities see relations.
Entity Structure¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Common\Structure\Web\EmailAddress;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
use Dms\Core\Model\Type\Builder\Type;
use Dms\Core\Model\ValueObjectCollection;
class Contact extends Entity
{
const NAMES = 'NAMES';
const EMAIL_ADDRESSES = 'emailAddresses';
/**
* @var string[]
*/
public $names;
/**
* @var ValueObjectCollection|EmailAddress[]
*/
public $emailAddresses;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->names)->asArrayOf(Type::string());
$class->property($this->emailAddresses)->asType(EmailAddress::collectionType());
}
}
Mapper Configuration¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Contact;
use Dms\Common\Structure\Web\Persistence\EmailAddressMapper;
/**
* The App\Domain\Entities\Contact entity mapper.
*/
class ContactMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Contact::class);
$map->toTable('contacts');
$map->idToPrimaryKey('id');
// Map names array to JSON string in 'names' column
$map->property(Contact::NAMES)
->mappedVia(
function (array $names) {
return json_encode($names);
},
function (string $json) {
return (array)json_decode($json);
}
)
->to('names')
->asVarchar(8000);
// Map email addresses to separate table 'contact_email_addresses'
$map->embeddedCollection(Contact::EMAIL_ADDRESSES)
->toTable('contact_email_addresses')
->withPrimaryKey('id')
->withForeignKeyToParentAs('contact_id')
->using(new EmailAddressMapper('email_addresses'));
}
}
Module Configuration¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IContactRepository;
use App\Domain\Entities\Contact;
use Dms\Common\Structure\Field;
/**
* The contact module.
*/
class ContactModule extends CrudModule
{
public function __construct(IContactRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('contact');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('names', 'Names')->arrayOf(
Field::element()->string()->required()
)
)->bindToProperty('names'),
//
$form->field(
Field::create('email_addresses', 'Email Addresses')->arrayOf(
Field::element()->email()->required()
)
)->bindToProperty(Contact::EMAIL_ADDRESSES),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Custom Value Objects¶
You may find yourself repeating a bunch of fields throughout your application, in this case you can extract these fields into your own custom value objects which can then be reused throughout your app.
Entity Structure¶
app/Domain/Entities/Engine.php¶
Your custom value object class
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\ValueObject;
class Engine extends ValueObject
{
const NAME = 'name';
const HORSE_POWER = 'horsePower';
/**
* @var string
*/
public $name;
/**
* @var int
*/
public $horsePower;
/**
* Defines the structure of this class.
*
* @param ClassDefinition $class
*/
protected function define(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->horsePower)->asInt();
}
}
app/Domain/Entities/Vehicle.php¶
An entity which contains your value object as a field.
<?php declare(strict_types = 1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Vehicle extends Entity
{
const ENGINE = 'engine';
/**
* @var Engine
*/
public $engine;
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->engine)->asObject(Engine::class);
}
}
Mapper Configuration¶
app/Infrastructure/Persistence/EngineMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\IndependentValueObjectMapper;
use App\Domain\Entities\Engine;
/**
* The App\Domain\Entities\Engine value object mapper.
*/
class EngineMapper extends IndependentValueObjectMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Engine::class);
$map->property(Engine::NAME)->to('name')->asVarchar(255);
$map->property(Engine::HORSE_POWER)->to('horse_power')->asInt();
}
}
app/Infrastructure/Persistence/VehicleMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Vehicle;
use App\Infrastructure\Persistence\EngineMapper;
/**
* The App\Domain\Entities\Vehicle entity mapper.
*/
class VehicleMapper extends EntityMapper
{
/**
* Defines the value object mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Vehicle::class);
$map->toTable('vehicles');
$map->idToPrimaryKey('id');
// Embeds the columns of the value object in the 'vehicles' table
// Each column will be prefixed so they will become 'engine_name' and 'engine_horse_power'
$map->embedded(Vehicle::ENGINE)
->withColumnsPrefixedBy('engine_')
->using(new EngineMapper());
}
}
Module Configuration¶
app/Cms/Modules/Fields/EngineField.php¶
You can define custom fields for value objects
<?php declare(strict_types = 1);
namespace App\Cms\Modules\Fields;
use Dms\Core\Common\Crud\Definition\Form\ValueObjectFieldDefinition;
use Dms\Core\Common\Crud\Form\ValueObjectField;
use App\Domain\Entities\Engine;
use Dms\Common\Structure\Field;
/**
* The App\Domain\Entities\Engine value object field.
*/
class EngineField extends ValueObjectField
{
public function __construct(string $name, string $label)
{
parent::__construct($name, $label);
}
/**
* Defines the structure of this value object field.
*
* @param ValueObjectFieldDefinition $form
*
* @return void
*/
protected function define(ValueObjectFieldDefinition $form)
{
$form->bindTo(Engine::class);
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Engine::NAME),
//
$form->field(
Field::create('horse_power', 'Horse Power')->int()->required()
)->bindToProperty(Engine::HORSE_POWER),
//
]);
}
}
app/Cms/Modules/VehicleModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IVehicleRepository;
use App\Domain\Entities\Vehicle;
use App\Cms\Modules\Fields\EngineField;
/**
* The vehicle module.
*/
class VehicleModule extends CrudModule
{
public function __construct(IVehicleRepository $dataSource, IAuthSystem $authSystem)
{
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('vehicle');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
// Reference your custom value object field
(new EngineField('engine', 'Engine'))->required()
)->bindToProperty(Vehicle::ENGINE),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Vehicle::ENGINE)->to((new EngineField('engine', 'Engine'))->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Relations¶
You can define different types of relationships between entities, see examples below
One to One¶
An one to one bi-directional relationship between entities can be defined as follows.
Entity Structure¶
app/Domain/Entities/Country.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Country extends Entity
{
const NAME = 'name';
const CAPITAL_CITY = 'capitalCity';
/**
* @var string
*/
public $name;
/**
* @var CapitalCity
*/
public $capitalCity;
/**
* Country constructor.
*
* @param string $name
* @param CapitalCity $capitalCity
*/
public function __construct(string $name, CapitalCity $capitalCity)
{
parent::__construct();
$this->name = $name;
$this->capitalCity = $capitalCity;
$capitalCity->country = $this;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->capitalCity)->asObject(CapitalCity::class);
}
}
app/Domain/Entities/CapitalCity.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class CapitalCity extends Entity
{
const NAME = 'name';
const COUNTRY = 'country';
/**
* @var string
*/
public $name;
/**
* @var Country
*/
public $country;
/**
* CapitalCity constructor.
*
* @param string $name
* @param Country $country
*/
public function __construct(string $name, Country $country)
{
parent::__construct();
$this->name = $name;
$this->country = $country;
$country->capitalCity = $this;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->country)->asObject(Country::class);
}
}
Mapper Configuration¶
app/Infrastructure/Persistence/CountryMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Country;
use App\Domain\Entities\CapitalCity;
/**
* The App\Domain\Entities\Country entity mapper.
*/
class CountryMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Country::class);
$map->toTable('countries');
$map->idToPrimaryKey('id');
$map->property(Country::NAME)->to('name')->asVarchar(255);
$map->column('capital_city_id')->asUnsignedInt();
$map->relation(Country::CAPITAL_CITY)
->to(CapitalCity::class)
->manyToOne()
->withBidirectionalRelation(CapitalCity::COUNTRY)
->withRelatedIdAs('capital_city_id');
}
}
app/Infrastructure/Persistence/CapitalCityMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\CapitalCity;
use App\Domain\Entities\Country;
/**
* The App\Domain\Entities\CapitalCity entity mapper.
*/
class CapitalCityMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(CapitalCity::class);
$map->toTable('capital_cities');
$map->idToPrimaryKey('id');
$map->property(CapitalCity::NAME)->to('name')->asVarchar(255);
$map->relation(CapitalCity::COUNTRY)
->to(Country::class)
->toOne()
->identifying()
->withBidirectionalRelation(Country::CAPITAL_CITY)
->withParentIdAs('capital_city_id');
}
}
Module Configuration¶
app/Cms/Modules/CountryModule.php¶
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use App\Domain\Entities\CapitalCity;
use App\Domain\Entities\Country;
use App\Domain\Services\Persistence\ICapitalCityRepository;
use App\Domain\Services\Persistence\ICountryRepository;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
/**
* The country module.
*/
class CountryModule extends CrudModule
{
/**
* @var ICapitalCityRepository
*/
protected $capitalCityRepository;
public function __construct(ICountryRepository $dataSource, IAuthSystem $authSystem, ICapitalCityRepository $capitalCityRepository)
{
$this->capitalCityRepository = $capitalCityRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('country');
$module->labelObjects()->fromProperty(Country::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Country::NAME),
//
//
]);
if ($form->isCreateForm()) {
$form->continueSection([
$form->field(
Field::create('capital_city', 'Capital City')->string()->required()
)->bindToCallbacks(
function () {
// Unused
},
function (Country $country, string $cityName) {
$country->capitalCity = new CapitalCity($cityName, $country);
}
),
]);
} else {
$form->continueSection([
$form->field(
Field::create('capital_city', 'Capital City')
->entityFrom($this->capitalCityRepository)
->required()
->labelledBy(CapitalCity::NAME)
// Add this line if you want to load the
// options asynchronously via autocomplete
->searchableBy(Country::NAME)
)->bindToProperty(Country::CAPITAL_CITY),
]);
}
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Country::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
app/Cms/Modules/CapitalCityModule.php¶
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use App\Domain\Entities\CapitalCity;
use App\Domain\Entities\Country;
use App\Domain\Services\Persistence\ICapitalCityRepository;
use App\Domain\Services\Persistence\ICountryRepository;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
/**
* The capital-city module.
*/
class CapitalCityModule extends CrudModule
{
/**
* @var ICountryRepository
*/
protected $countryRepository;
public function __construct(ICapitalCityRepository $dataSource, IAuthSystem $authSystem, ICountryRepository $countryRepository)
{
$this->countryRepository = $countryRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('capital-city');
$module->labelObjects()->fromProperty(CapitalCity::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(CapitalCity::NAME),
]);
if ($form->isCreateForm()) {
$form->continueSection([
$form->field(
Field::create('country', 'Country')->string()->required()
)->bindToCallbacks(
function () {
// Unused
},
function (CapitalCity $capitalCity, string $countryName) {
$capitalCity->country = new Country($countryName, $capitalCity);
}
)
]);
} else {
$form->continueSection([
$form->field(
Field::create('country', 'Country')
->entityFrom($this->countryRepository)
->required()
->labelledBy(Country::NAME)
// Add this line if you want to load the
// options asynchronously via autocomplete
->searchableBy(Country::NAME)
)->bindToProperty(CapitalCity::COUNTRY),
]);
}
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(CapitalCity::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
One to Many¶
An one to many bi-directional relationship between entities can be defined as follows.
Entity Structure¶
app/Domain/Entities/Mother.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\EntityCollection;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Mother extends Entity
{
const NAME = 'name';
const CHILDREN = 'children';
/**
* @var string
*/
public $name;
/**
* @var EntityCollection|Child[]
*/
public $children;
/**
* Parent constructor.
*/
public function __construct()
{
parent::__construct();
$this->children = Child::collection();
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->children)->asType(Child::collectionType());
}
}
app/Domain/Entities/Child.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Child extends Entity
{
const NAME = 'name';
const MOTHER = 'mother';
/**
* @var string
*/
public $name;
/**
* @var Mother
*/
public $mother;
/**
* Child constructor.
*
* @param string $name
* @param Mother $mother
*/
public function __construct($name, Mother $mother)
{
parent::__construct();
$this->name = $name;
$this->mother = $mother;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->mother)->asObject(Mother::class);
}
}
Mapper Configuration¶
app/Infrastructure/Persistence/MotherMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Mother;
use App\Domain\Entities\Child;
/**
* The App\Domain\Entities\Mother entity mapper.
*/
class MotherMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Mother::class);
$map->toTable('mothers');
$map->idToPrimaryKey('id');
$map->property(Mother::NAME)->to('name')->asVarchar(255);
$map->relation(Mother::CHILDREN)
->to(Child::class)
->toMany()
->identifying()
->withBidirectionalRelation(Child::MOTHER)
->withParentIdAs('mother_id');
}
}
app/Infrastructure/Persistence/ChildMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Child;
use App\Domain\Entities\Mother;
/**
* The App\Domain\Entities\Child entity mapper.
*/
class ChildMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Child::class);
$map->toTable('children');
$map->idToPrimaryKey('id');
$map->property(Child::NAME)->to('name')->asVarchar(255);
$map->column('mother_id')->asUnsignedInt();
$map->relation(Child::MOTHER)
->to(Mother::class)
->manyToOne()
->withBidirectionalRelation(Mother::CHILDREN)
->withRelatedIdAs('mother_id');
}
}
Module Configuration¶
app/Cms/Modules/MotherModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IMotherRepository;
use App\Domain\Entities\Mother;
use Dms\Common\Structure\Field;
use App\Domain\Services\Persistence\IChildRepository;
use App\Domain\Entities\Child;
/**
* The mother module.
*/
class MotherModule extends CrudModule
{
/**
* @var IChildRepository
*/
protected $childRepository;
public function __construct(IMotherRepository $dataSource, IAuthSystem $authSystem, IChildRepository $childRepository)
{
$this->childRepository = $childRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('mother');
$module->labelObjects()->fromProperty(Mother::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Mother::NAME),
//
$form->field(
Field::create('children', 'Children')
->entitiesFrom($this->childRepository)
->labelledBy(Child::NAME)
->mapToCollection(Child::collectionType())
// Add this line if you want to load the
// options asynchronously via autocomplete
->searchableBy(Child::NAME)
)->bindToProperty(Mother::CHILDREN),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Mother::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
app/Cms/Modules/ChildModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IChildRepository;
use App\Domain\Entities\Child;
use Dms\Common\Structure\Field;
use App\Domain\Services\Persistence\IMotherRepository;
use App\Domain\Entities\Mother;
/**
* The child module.
*/
class ChildModule extends CrudModule
{
/**
* @var IMotherRepository
*/
protected $motherRepository;
public function __construct(IChildRepository $dataSource, IAuthSystem $authSystem, IMotherRepository $motherRepository)
{
$this->motherRepository = $motherRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('child');
$module->labelObjects()->fromProperty(Child::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Child::NAME),
//
$form->field(
Field::create('mother', 'Mother')
->entityFrom($this->motherRepository)
->required()
->labelledBy(Mother::NAME)
)->bindToProperty(Child::MOTHER),
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Child::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Many to Many¶
A many to many bi-directional relationship between entities can be defined as follows.
Entity Structure¶
app/Domain/Entities/Article.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\EntityCollection;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Article extends Entity
{
const TITLE = 'title';
const TAGS = 'tags';
/**
* @var string
*/
public $title;
/**
* @var EntityCollection|Tag[]
*/
public $tags;
/**
* Article constructor.
*/
public function __construct()
{
parent::__construct();
$this->tags = Tag::collection();
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->title)->asString();
$class->property($this->tags)->asType(Tag::collectionType());
}
}
app/Domain/Entities/Tag.php¶
<?php declare(strict_types=1);
namespace App\Domain\Entities;
use Dms\Core\Model\EntityCollection;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
class Tag extends Entity
{
const NAME = 'name';
const ARTICLES = 'articles';
/**
* @var string
*/
public $name;
/**
* @var EntityCollection|Article[]
*/
public $articles;
/**
* Tag constructor.
*/
public function __construct()
{
parent::__construct();
$this->articles = Article::collection();
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
$class->property($this->articles)->asType(Article::collectionType());
}
}
Mapper Configuration¶
app/Infrastructure/Persistence/ArticleMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Article;
use App\Domain\Entities\Tag;
/**
* The App\Domain\Entities\Article entity mapper.
*/
class ArticleMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Article::class);
$map->toTable('articles');
$map->idToPrimaryKey('id');
$map->property(Article::TITLE)->to('title')->asVarchar(255);
$map->relation(Article::TAGS)
->to(Tag::class)
->toMany()
->withBidirectionalRelation(Tag::ARTICLES)
->throughJoinTable('article_tags')
->withParentIdAs('article_id')
->withRelatedIdAs('tag_id');
}
}
app/Infrastructure/Persistence/TagMapper.php¶
<?php declare(strict_types = 1);
namespace App\Infrastructure\Persistence;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
use App\Domain\Entities\Tag;
use App\Domain\Entities\Article;
/**
* The App\Domain\Entities\Tag entity mapper.
*/
class TagMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Tag::class);
$map->toTable('tags');
$map->idToPrimaryKey('id');
$map->property(Tag::NAME)->to('name')->asVarchar(255);
$map->relation(Tag::ARTICLES)
->to(Article::class)
->toMany()
->withBidirectionalRelation(Article::TAGS)
->throughJoinTable('article_tags')
->withParentIdAs('tag_id')
->withRelatedIdAs('article_id');
}
}
Module Configuration¶
app/Cms/Modules/ArticleModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IArticleRepository;
use App\Domain\Entities\Article;
use Dms\Common\Structure\Field;
use App\Domain\Services\Persistence\ITagRepository;
use App\Domain\Entities\Tag;
/**
* The article module.
*/
class ArticleModule extends CrudModule
{
/**
* @var ITagRepository
*/
protected $tagRepository;
public function __construct(IArticleRepository $dataSource, IAuthSystem $authSystem, ITagRepository $tagRepository)
{
$this->tagRepository = $tagRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('article');
$module->labelObjects()->fromProperty(Article::TITLE);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('title', 'Title')->string()->required()
)->bindToProperty(Article::TITLE),
//
$form->field(
Field::create('tags', 'Tags')
->entitiesFrom($this->tagRepository)
->labelledBy(Tag::NAME)
->mapToCollection(Tag::collectionType())
// Add this line if you want to load the
// options asynchronously via autocomplete
->searchableBy(Tag::NAME)
)->bindToProperty(Article::TAGS),
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Article::TITLE)->to(Field::create('title', 'Title')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
app/Cms/Modules/TagModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\ITagRepository;
use App\Domain\Entities\Tag;
use Dms\Common\Structure\Field;
use App\Domain\Services\Persistence\IArticleRepository;
use App\Domain\Entities\Article;
/**
* The tag module.
*/
class TagModule extends CrudModule
{
/**
* @var IArticleRepository
*/
protected $articleRepository;
public function __construct(ITagRepository $dataSource, IAuthSystem $authSystem, IArticleRepository $articleRepository)
{
$this->articleRepository = $articleRepository;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('tag');
$module->labelObjects()->fromProperty(Tag::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Tag::NAME),
//
$form->field(
Field::create('articles', 'Articles')
->entitiesFrom($this->articleRepository)
->labelledBy(Article::TITLE)
->mapToCollection(Article::collectionType())
// Add this line if you want to load the
// options asynchronously via autocomplete
->searchableBy(Article::TITLE)
)->bindToProperty(Tag::ARTICLES),
//
]);
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Tag::NAME)->to(Field::create('name', 'Name')->string()->required());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Advanced Topics¶
Advanced Topics¶
Subclasses¶
A powerful of feature of OOP to help remove duplication is subclassing. Subclassing allows you to create inheritance trees to better model your domain.
Entity Structure¶
We’ll model this example from the following diagram:
app/Domain/Entities/Player.php¶
<?php
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
use Dms\Core\Model\Object\Entity;
abstract class Player extends Entity
{
const NAME = 'name';
/**
* @var string
*/
public $name;
public function __construct(string $name)
{
parent::__construct();
$this->name = $name;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
$class->property($this->name)->asString();
}
}
app/Domain/Entities/Footballer.php¶
<?php
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
class Footballer extends Player
{
const CLUB = 'club';
/**
* @var string
*/
public $club;
public function __construct(string $name, string $club)
{
parent::__construct($name);
$this->club = $club;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
parent::defineEntity($class);
$class->property($this->club)->asString();
}
}
app/Domain/Entities/Cricketer.php¶
<?php
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
class Cricketer extends Player
{
const BATTING_AVERAGE = 'battingAverage';
/**
* @var int
*/
public $battingAverage;
public function __construct(string $name, int $battingAverage)
{
parent::__construct($name);
$this->battingAverage = $battingAverage;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
parent::defineEntity($class);
$class->property($this->battingAverage)->asInt();
}
}
app/Domain/Entities/Bowler.php¶
<?php
namespace App\Domain\Entities;
use Dms\Core\Model\Object\ClassDefinition;
class Bowler extends Cricketer
{
const BOWLING_AVERAIGE = 'bowlingAverage';
/**
* @var int
*/
public $bowlingAverage;
public function __construct(string $name, int $battingAverage, int $bowlingAverage)
{
parent::__construct($name, $battingAverage);
$this->bowlingAverage = $bowlingAverage;
}
/**
* Defines the structure of this entity.
*
* @param ClassDefinition $class
*/
protected function defineEntity(ClassDefinition $class)
{
parent::defineEntity($class);
$class->property($this->bowlingAverage)->asInt();
}
}
Mapper Configuration (Single Table Inheritance)¶
You can map all the subclasses to one database table using single table inheritance pattern with a type column.
<?php declare(strict_types=1);
namespace App\Infrastructure\Persistence;
use App\Domain\Entities\Bowler;
use App\Domain\Entities\Cricketer;
use App\Domain\Entities\Footballer;
use App\Domain\Entities\Player;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class PlayerMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Player::class);
$map->toTable('players');
$map->idToPrimaryKey('id');
$map->column('type')->asEnum(['footballer', 'cricketer', 'bowler']);
$map->property(Player::NAME)->to('name')->asVarchar(255);
$map->subclass()->withTypeInColumn('type', 'footballer')->define(function (MapperDefinition $map) {
$map->type(Footballer::class);
$map->property(Footballer::CLUB)->to('club')->asVarchar(255);
});
$map->subclass()->withTypeInColumn('type', 'cricketer')->define(function (MapperDefinition $map) {
$map->type(Cricketer::class);
$map->property(Cricketer::BATTING_AVERAGE)->to('batting_average')->asInt();
$map->subclass()->withTypeInColumn('type', 'bowler')->define(function (MapperDefinition $map) {
$map->type(Bowler::class);
$map->property(Bowler::BOWLING_AVERAGE)->to('bowling_average')->asInt();
});
});
}
}
Mapper Configuration (Class Table Inheritance)¶
Alternatively, you can map each the subclasses to a separate table.
<?php declare(strict_types=1);
namespace App\Infrastructure\Persistence;
use App\Domain\Entities\Bowler;
use App\Domain\Entities\Cricketer;
use App\Domain\Entities\Footballer;
use App\Domain\Entities\Player;
use Dms\Core\Persistence\Db\Mapping\Definition\MapperDefinition;
use Dms\Core\Persistence\Db\Mapping\EntityMapper;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class PlayerMapper extends EntityMapper
{
/**
* Defines the entity mapper
*
* @param MapperDefinition $map
*
* @return void
*/
protected function define(MapperDefinition $map)
{
$map->type(Player::class);
$map->toTable('players');
$map->idToPrimaryKey('id');
$map->property(Player::NAME)->to('name')->asVarchar(255);
$map->subclass()->asSeparateTable('footballers')->define(function (MapperDefinition $map) {
$map->type(Footballer::class);
$map->property(Footballer::CLUB)->to('club')->asVarchar(255);
});
$map->subclass()->asSeparateTable('cricketers')->define(function (MapperDefinition $map) {
$map->type(Cricketer::class);
$map->property(Cricketer::BATTING_AVERAGE)->to('batting_average')->asInt();
$map->subclass()->asSeparateTable('bowlers')->define(function (MapperDefinition $map) {
$map->type(Bowler::class);
$map->property(Bowler::BOWLING_AVERAGE)->to('bowling_average')->asInt();
});
});
}
}
Module Configuration¶
Modules support mapping to entity subclasses as shown in the following example
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use App\Domain\Entities\Bowler;
use App\Domain\Entities\Cricketer;
use App\Domain\Entities\Footballer;
use App\Domain\Entities\Player;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
/**
* The player module.
*/
class PlayerModule extends CrudModule
{
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('player');
$module->labelObjects()->fromProperty(Player::NAME);
$module->crudForm(function (CrudFormDefinition $form) {
$typeField = Field::create('type', 'Type')->string()->oneOf([
'footballer' => 'Footballer',
'cricketer' => 'Cricketer',
'bowler' => 'Bowler',
]);
if (!$form->isCreateForm()) {
// If the entity is already exist the type of the entity cannot be changed
$typeField->readonly();
}
$form->section('Details', [
$form->field(
Field::create('name', 'Name')->string()->required()
)->bindToProperty(Player::NAME),
//
$form->field(
$typeField
)->bindToCallbacks(function (Player $player) {
return [
Footballer::class => 'footballer',
Cricketer::class => 'cricketer',
Bowler::class => 'bowler',
][get_class($player)];
}, function () {
// Unused
}),
]);
$form->dependentOn(['type'], function (CrudFormDefinition $form, array $input) {
if ($input['type'] === 'footballer') {
$form->mapToSubClass(Footballer::class);
$form->continueSection([
$form->field(
Field::create('club', 'Club')->string()->required()
)->bindToProperty(Footballer::CLUB)
]);
}
if ($input['type'] === 'cricketer' || $input['type'] === 'bowler') {
if ($input['type'] === 'cricketer') {
$form->mapToSubClass(Cricketer::class);
}
$form->continueSection([
$form->field(
Field::create('batting_average', 'Batting Average')->int()->required()
)->bindToProperty(Cricketer::BATTING_AVERAGE)
]);
if ($input['type'] === 'bowler') {
$form->mapToSubClass(Bowler::class);
$form->continueSection([
$form->field(
Field::create('bowling_average', 'Bowling Average')->int()->required()
)->bindToProperty(Bowler::BOWLING_AVERAGE)
]);
}
}
});
});
$module->removeAction()->deleteFromDataSource();
$module->summaryTable(function (SummaryTableDefinition $table) {
$table->mapProperty(Player::NAME)->to(Field::create('name', 'Name')->string());
$table->view('all', 'All')
->loadAll()
->asDefault();
});
}
}
Advanced CMS Topics¶
Custom Actions¶
It is often necessary to support add functionality to your CMS that is not part of the standard CRUD operations. You can define custom actions in your modules which can be executed in the backend.
General Actions¶
For actions that operate on multiple entities you can define a general action as follows
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Common\Structure\FileSystem\File;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IArticleRepository;
use App\Domain\Entities\Article;
use App\Domain\Services\ArticleExportService;
use Dms\Common\Structure\Field;
use Dms\Core\Form\Builder\Form;
/**
* The article module.
*/
class ArticleModule extends CrudModule
{
/**
* @var ArticleExportService
*/
public $articleExportService;
public function __construct(IArticleRepository $dataSource, IAuthSystem $authSystem, ArticleExportService $articleExportService)
{
$this->articleExportService = $articleExportService;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('article');
$module->labelObjects()->fromProperty(Article::TITLE);
$module->action('export')
->form(Form::create()->section('Settings', [
Field::create('date_range', 'Date Range')->dateRange()->required()
]))
->returns(File::class)
->handler(function (array $input) {
// Generate your export file...
return $this->articleExportService->generateExportForDateRange($input['date_range']);
});
// Omitted the rest of the module configuration
}
}
Object Actions¶
If the action operates on one entity, you can define an object action
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Common\Structure\FileSystem\File;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Services\Persistence\IJobRepository;
use App\Domain\Entities\Job;
use Dms\Common\Structure\Field;
use Dms\Core\Form\Builder\Form;
/**
* The job module.
*/
class JobModule extends CrudModule
{
/**
* @var JobCompletionService
*/
protected $jobCompletionService;
public function __construct(IJobRepository $dataSource, IAuthSystem $authSystem, JobCompletionService $jobCompletionService)
{
$this->jobCompletionService = $jobCompletionService;
parent::__construct($dataSource, $authSystem);
}
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('job');
$module->labelObjects()->fromProperty(Job::NAME);
$module->objectAction('mark-as-complete')
->form(Form::create()->section('Details', [
Field::create('comments', 'Comments')->string()
]))
->handler(function (Job $job, array $input) {
$this->jobCompletionService->completeJob($job, $input['comments']);
});
// Omitted module configuration
}
}
Custom Fields¶
If you need a field in your backend that is not provided out of the box you can implement a custom field renderer in your app which will override how a field is displayed.
app/Cms/Modules/YourModule.php¶
<?php declare(strict_types = 1);
namespace App\Cms\Modules;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
use App\Domain\Entities\Entity;
use Dms\Common\Structure\Field;
/**
* The photograph module.
*/
class YourModule extends CrudModule
{
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
$module->name('your-module');
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('custom_field', 'Custom Field')->string()->required()
)->bindToProperty(Entity::PROPERTY),
//
]);
});
}
}
app/Cms/Custom/CustomFieldRenderer.php¶
<?php declare(strict_types=1);
namespace App\Cms\Custom;
use Dms\Core\Form\Field\Type\ArrayOfType;
use Dms\Core\Form\Field\Type\StringType;
use Dms\Core\Form\IField;
use Dms\Core\Form\IFieldType;
use Dms\Web\Laravel\Renderer\Form\Field\BladeFieldRenderer;
use Dms\Web\Laravel\Renderer\Form\FormRenderingContext;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class CustomFieldRenderer extends BladeFieldRenderer
{
/**
* Gets the expected class of the field type for the field.
*
* @return array
*/
public function getFieldTypeClasses(): array
{
return [StringType::class];
}
/**
* @param FormRenderingContext $renderingContext
* @param IField $field
* @param IFieldType $fieldType
*
* @return bool
*/
protected function canRender(FormRenderingContext $renderingContext, IField $field, IFieldType $fieldType): bool
{
return $renderingContext->getAction()->getPackageName() === 'your-package'
&& $renderingContext->getAction()->getModuleName() === 'your-module'
&& $field->getName() === 'custom_field';
}
/**
* @param FormRenderingContext $renderingContext
* @param IField $field
* @param IFieldType $fieldType
*
* @return string
*/
protected function renderField(FormRenderingContext $renderingContext, IField $field, IFieldType $fieldType): string
{
return view('dms.custom-fields.text', [
'field' => $field,
])->render();
}
/**
* @param FormRenderingContext $renderingContext
* @param IField $field
* @param mixed $value
* @param IFieldType $fieldType
*
* @return string
*/
protected function renderFieldValue(FormRenderingContext $renderingContext, IField $field, $value, IFieldType $fieldType): string
{
return view('dms.custom-fields.readonly-text', [
'field' => $field,
'value' => $value,
])->render();
}
}
config/dms.php¶
Register your custom form field renderer in config/dms.php
<?php
[
// ...
'services' => [
// ...
'renderers' => [
// ...
'form-fields' => [
// Add your class here
App\Cms\Custom\CustomFieldRenderer::class,
// ...
],
// ...
],
],
]
resources/views/dms/custom-fields/text.blade.php¶
Your custom field:
<input type="text" name="{{ $field->getName() }}" value="{{ $field->getInitialValue() }}" />
resources/views/dms/custom-fields/readonly-text.blade.php¶
Your custom value: {{ $value }}
Dependent Fields¶
Often static form builders are quite limiting and dont provide much of the functionality required for even a semi-complex CMS.
Dependent fields offer a powerful way to build forms that contain complex conditionals, multiple steps or can even be (ab)used to provide live calculations or previews.
Multiple steps example¶
Here is an example of a form which will show different options for the second field based on the value of the first field.
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
class YourModule extends CrudModule
{
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
// ...
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('type', 'Type')->string()->required()->oneOf([
'fruit' => 'Fruit',
'vegetable' => 'Vegetable',
])
)->withoutBinding(),
]);
$form->dependentOn(['type'], function (CrudFormDefinition $form, array $input) {
if ($input['type'] === 'fruit') {
$form->continueSection([
$form->field(
Field::create('type_of_fruit', 'Type Of Fruit')->string()->required()->oneOf([
'apple' => 'Apple',
'orange' => 'Orange',
])
)->withoutBinding(),
]);
}
if ($input['type'] === 'vegetable') {
$form->continueSection([
$form->field(
Field::create('type_of_vegetable', 'Type Of Vegetable')->string()->required()->oneOf([
'carrot' => 'Carrot',
'potato' => 'Potato',
])
)->withoutBinding(),
]);
}
});
});
// ...
}
}
Live calculation example¶
Here is an example of a form which will sum two integers and display the result.
<?php declare(strict_types=1);
namespace App\Cms\Modules;
use Dms\Common\Structure\Field;
use Dms\Core\Auth\IAuthSystem;
use Dms\Core\Common\Crud\CrudModule;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Core\Common\Crud\Definition\Table\SummaryTableDefinition;
class YourModule extends CrudModule
{
/**
* Defines the structure of this module.
*
* @param CrudModuleDefinition $module
*/
protected function defineCrudModule(CrudModuleDefinition $module)
{
// ...
$module->crudForm(function (CrudFormDefinition $form) {
$form->section('Details', [
$form->field(
Field::create('a', 'A')->int()->required()
)->withoutBinding(),
//
$form->field(
Field::create('b', 'B')->int()->required()
)->withoutBinding(),
]);
$form->dependentOn(['a', 'b'], function (CrudFormDefinition $form, array $input) {
$form->continueSection([
$form->field(
Field::create('result', 'Result')->int()->value($input['a'] + $input['b'])->readonly()
)->withoutBinding(),
]);
});
});
// ...
}
}
Extending Modules¶
It may become necessary to extend the functionality of a module which is installed as a standalone package. To achieve this, you can hook into to the events emitted and add the desired functionality.
You can add these event listeners directly in your app/AppCms.php
or extract them into their
own classes if applicable.
Adding form fields¶
Here is an example of adding an additional colour field to the products module from the shop package.
<?php declare(strict_types = 1);
namespace App;
use Dms\Common\Structure\Field;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Core\Common\Crud\Definition\Form\CrudFormDefinition;
use Dms\Package\Analytics\AnalyticsPackage;
use Dms\Package\Shop\Cms\ShopPackage;
use Dms\Package\Shop\Domain\Entities\Product\Product;
use Dms\Web\Laravel\Auth\AdminPackage;
use Dms\Web\Laravel\Document\PublicFilePackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
'admin' => AdminPackage::class,
'documents' => PublicFilePackage::class,
'analytics' => AnalyticsPackage::class,
'shop' => ShopPackage::class,
]);
\Event::listen('dms::shop.products.defined-form', function (CrudFormDefinition $form) {
// Add your form fields here
$form->continueSection([
$form->field(
Field::create('colour', 'Colour')->string()->required()
)->bindToCallbacks(
function (Product $product) {
return $product->metadata['colour'] ?? null;
},
function (Product $product, string $value) {
$product->metadata['colour'] = $value;
}
),
]);
});
}
}
Adding actions¶
Here is an example of adding a custom action to the product module.
<?php declare(strict_types = 1);
namespace App;
use Dms\Common\Structure\Field;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Core\Common\Crud\Definition\CrudModuleDefinition;
use Dms\Package\Analytics\AnalyticsPackage;
use Dms\Package\Shop\Cms\ShopPackage;
use Dms\Web\Laravel\Auth\AdminPackage;
use Dms\Web\Laravel\Document\PublicFilePackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
'admin' => AdminPackage::class,
'documents' => PublicFilePackage::class,
'analytics' => AnalyticsPackage::class,
'shop' => ShopPackage::class,
]);
\Event::listen('dms::shop.products.define', function (CrudModuleDefinition $module) {
// Add your custom action here
$module->action('custom-action')
->handler(function (array $input) {
// ...
});
});
}
}
Advanced ORM Topics¶
Packages¶
Analytics Package¶
The analytics package integrates with Google Analytics to provide basic analytics on the dashboard.
Installation¶
This package comes pre-installed with the DMS.
Usage¶
After configuring the Google Analytics settings in the backend you should ensure the analytics scripts are output to all your pages on the site.
Assuming you have a template blade view you can use this package to output the scripts:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
...
<!-- Output analytics scripts at the end of document head -->
{!! app(\Dms\Package\Analytics\AnalyticsEmbedCodeService::class)->getEmbedCode() !!}
</head>
<body>
...
</body>
</html>
Blog Package¶
The blog package provides the functionality for building a simple blogging platform.
Installation¶
Install the package via composer¶
composer require dms-org/package.blog
Register the package in app/AppCms.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Package\Blog\Cms\BlogPackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Add this line to register the blog package...
'blog' => BlogPackage::class,
]);
}
}
Register the ORM in app/AppOrm.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Package\Blog\Infrastructure\Persistence\BlogOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
// Add this line to register the blog mappers
$orm->encompass((new BlogOrm($this->iocContainer))->inNamespace('blog_'));
}
}
Configure the blog package in app/Providers/AppServiceProvider.php
¶
<?php
namespace App\Providers;
use App\AppCms;
use App\AppOrm;
use Dms\Core\ICms;
use Dms\Core\Persistence\Db\Mapping\IOrm;
use Dms\Package\Blog\Domain\Entities\BlogArticle;
use Dms\Package\Blog\Domain\Services\Config\BlogConfiguration;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// ...
// Register your blog configuration here...
$this->app->bind(BlogConfiguration::class, function () {
return BlogConfiguration::builder()
->setFeaturedImagePath(public_path('app/images/blog'))
->useDashedSlugGenerator()
// Supply a preview callback to provide article previews
// directly from the backend. This can be omitted to disable this feature.
->setArticlePreviewCallback(function (BlogArticle $article) {
return view('blog.article', ['article' => $article])->render();
})
->build();
});
}
}
Usage¶
Here is an example of how to utilise the default functionality of the blog package.
<?php declare(strict_types = 1);
use Dms\Common\Structure\Web\EmailAddress;
use Dms\Package\Blog\Domain\Services\BlogKernel;
function demonstrateBlogFunctionality(BlogKernel $blog)
{
$categories = $blog->categories()->getAll();
$specificCategory = $blog->categories()->loadFromSlug('news');
$articles = $blog->articles()->getAll();
$articles = $blog->articles()->getPage(1, 15);
$specificArticle = $blog->articles()->loadFromSlug('a-new-article');
$authors = $blog->authors()->getAll();
$specificAuthor = $blog->authors()->loadFromSlug('john-smith');
$comment = $blog->comments()->postComment(
$specificArticle->getId(),
'John Smith',
new EmailAddress('john-smith@gmail.com'),
'This is a comment.'
);
}
Content Package¶
Sometimes it is unnecessary to define fully-blown entities and mappers for structures in your app. The content package provides a generic structure that you can use to define the schema of the content of your app.
Installation¶
Install the package via composer¶
composer require dms-org/package.content
Register the ORM in app/AppOrm.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Package\Content\Persistence\ContentOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
// Add this line to register the content mappers
$orm->encompass(new ContentOrm($this->iocContainer));
}
}
Register the view composer¶
Register a view composer in the boot
method of app/Providers/AppServiceProvider.php
to resolve
Dms\Package\Content\Core\ContentLoaderService
.
<?php
namespace App\Providers;
use Dms\Package\Content\Core\ContentLoaderService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Ensure the content can be loaded in all views
// via $contentLoader
\view()->composer('*', function ($view) {
$view->with('contentLoader', app(ContentLoaderService::class));
});
}
// Omitted...
}
Usage¶
Defining your content schema¶
Create a custom content package in app/Cms/MyContentPackage.php
<?php declare(strict_types = 1);
namespace App\Cms;
use App\Http\Controllers\PageController;
use Dms\Core\ICms;
use Dms\Package\Content\Cms\ContentPackage;
use Dms\Package\Content\Cms\Definition\ContentConfigDefinition;
use Dms\Package\Content\Cms\Definition\ContentGroupDefiner;
use Dms\Package\Content\Cms\Definition\ContentModuleDefinition;
use Dms\Package\Content\Cms\Definition\ContentPackageDefinition;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class MyContentPackage extends ContentPackage
{
protected static function defineConfig(ContentConfigDefinition $config)
{
$config
->storeImagesUnder(public_path('app/content/images'))
->mappedToUrl(url('app/content/images'))
->storeFilesUnder(public_path('app/content/files'));
}
/**
* Defines the structure of the content.
*
* @param ContentPackageDefinition $content
*
* @return void
*/
protected function defineContent(ContentPackageDefinition $content)
{
$content->module('pages', 'file-text', function (ContentModuleDefinition $module) {
$module->group('home', 'Home')
->withText('title', 'Title')
->withHtml('content', 'Content')
->withArrayOf('carousel-item', 'Hero Carousel', function (ContentGroupDefiner $item) {
$item
->withText('caption', 'Caption')
->withImage('image', 'Image');
})
->withMetadata('title', 'Meta - Title')
->withMetadata('description', 'Meta - Description')
// Optionally define a preview callback to enable
// live content previews in the CMS
->setPreviewCallback(function () {
return \app()->call(PageController::class . '@showHomePage')->render();
});
// Add more pages here ...
});
// Define additional modules here...
// Optionally, register your own custom modules if you want to include
// them in your content package...
$content->customModules([
'custom' => CustomModule::class
]);
}
}
Register the package in app/AppCms.php
¶
<?php declare(strict_types = 1);
namespace App;
use App\Cms\MyContentPackage;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Add this line to register your content package...
'content' => MyContentPackage::class,
]);
}
}
Load the content in your views¶
You can then use the $contentLoader
in your blade files to retrieve the content from the database.
<?php $content = $contentLoader->load('pages.home') ?>
<html>
<head>
<title>{{ $content->getMetadata('title') }}</title>
<meta name="description" content="{{ $content->getMetadata('title') }}">
</head>
<body>
<header>
<h1>{{ $content->getText('title') }}</h1>
</header>
<article>
{!! $content->getHtml('content') !!}
</article>
<ul class="carousel">
@foreach($content->getArrayOf('carousel-item') as $item)
<li>
<img src="{{ $item->hasImage('image') ? asset_file_url($item->getImage('image')) : 'http://placehold.it/500x500' }}" />
<span>{{ $item->getText('caption') }}</span>
</li>
@endforeach
</ul>
</body>
</html>
FAQ’s Package¶
It is often necessary to include a question and answer section in a website. The FAQ package provides the functionality for this.
Installation¶
Install the package via composer¶
composer require dms-org/package.faqs
Register the package in app/AppCms.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Package\Faq\Cms\FaqPackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Add this line to register the package...
'faqs' => FaqPackage::class,
]);
}
}
Register the ORM in app/AppOrm.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Package\Blog\Infrastructure\Persistence\BlogOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
// Add this line to register the mappers
$orm->encompass(new FaqOrm());
}
}
Usage¶
Load the FAQ’s in your controller¶
<?php declare(strict_types = 1);
namespace App\Http\Controllers;
use Dms\Package\Faq\Core\FaqLoaderService;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class FaqsController extends Controller
{
public function showFaqsPage(FaqLoaderService $faqLoaderService)
{
return view('faqs', [
'faqs' => $faqLoaderService->loadFaqs(),
]);
}
}
Display the FAQ’s in your view¶
<article>
<header>
<h1>FAQ's</h1>
</header>
<section class="content">
@foreach($faqs as $faq)
<div>
<strong>{{ $faq->question }}</strong>
<div>{!! $faq->answer->asString() !!}</div>
</div>
@endforeach
</section>
</article>
Contact Us Package¶
Most website’s include a contact form. The contact us packages allows these enquires to be stored in the backend.
Installation¶
Install the package via composer¶
composer require dms-org/package.contact-us
Register the package in app/AppCms.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Cms;
use Dms\Core\CmsDefinition;
use Dms\Package\ContactUs\Cms\ContactUsPackage;
/**
* The application's cms.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppCms extends Cms
{
/**
* Defines the structure and installed packages of the cms.
*
* @param CmsDefinition $cms
*
* @return void
*/
protected function define(CmsDefinition $cms)
{
$cms->packages([
// Add this line to register the package...
'contact-us' => ContactUsPackage::class,
]);
}
}
Register the ORM in app/AppOrm.php
¶
<?php declare(strict_types = 1);
namespace App;
use Dms\Core\Persistence\Db\Mapping\Definition\Orm\OrmDefinition;
use Dms\Core\Persistence\Db\Mapping\Orm;
use Dms\Package\ContactUs\Persistence\ContactUsOrm;
/**
* The application's orm.
*
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class AppOrm extends Orm
{
/**
* Defines the object mappers registered in the orm.
*
* @param OrmDefinition $orm
*
* @return void
*/
protected function define(OrmDefinition $orm)
{
// Add this line to register the mappers
$orm->encompass(new ContactUsOrm());
}
}
Usage¶
Sample contact form¶
<form action="..." method="post">
{!! csrf_field() !!}
<div class="form-group">
<label>Your Name</label>
<input type="text" class="form-control" required name="name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" required name="email">
</div>
<div class="form-group">
<label>Subject</label>
<input type="text" class="form-control" required name="subject">
</div>
<div class="form-group">
<label>Your Message</label>
<textarea name="message" class="form-control" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit Enquiry</button>
</form>
Record contact enquiry and send notification¶
<?php declare(strict_types = 1);
namespace App\Http\Controllers;
use Dms\Package\ContactUs\Core\ContactEnquiry;
use Dms\Package\ContactUs\Core\ContactEnquiryService;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
/**
* @author Elliot Levin <elliotlevin@hotmail.com>
*/
class ContactController extends Controller
{
public function submitEnquiry(Request $request, ContactEnquiryService $contactEnquiryService)
{
$this->validate($request, [
'name' => 'required',
'email' => 'required|email',
'subject' => 'required',
'message' => 'required',
]);
$contactEnquiryService->recordEnquiry(
$request->input('email'),
$request->input('name'),
$request->input('subject'),
$request->input('message'),
function (ContactEnquiry $enquiry) {
// Send the notification email...
}
);
return redirect('contact/success');
}
}
Building Your Own Package¶
Building your own DMS package is similar to building a normal application. You must define your entities, mappers and modules.
Then you must create a composer package so it can be installed in other projects.
For a complete example of a working package, including tests, see the blog package.