Moved all reference code to a subfolder in order to do subtree splitting
This commit is contained in:
2
.docker/mysql/01-testdb.sql
Normal file
2
.docker/mysql/01-testdb.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE DATABASE test_db;
|
||||
GRANT ALL PRIVILEGES ON test_db.* to 'user'@'%' WITH GRANT OPTION;
|
||||
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = LF
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
|
||||
[*docker-compose.yml]
|
||||
indent_size = 2
|
||||
40
.env
Normal file
40
.env
Normal file
@@ -0,0 +1,40 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=24d45e8c51d769273b29ee9236ba01e4
|
||||
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
DATABASE_URL=mysql://example:example@127.0.0.1:3306/example?serverVersion=8
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=doctrine://default
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
###< symfony/messenger ###
|
||||
5
.env.test
Normal file
5
.env.test
Normal file
@@ -0,0 +1,5 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS=App\Kernel
|
||||
APP_SECRET=$ecretf0rt3st
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
DATABASE_URL=mysql://user:pass@127.0.0.1:3306/test_db?serverVersion=8
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> application ###
|
||||
infection.log
|
||||
###< application ###
|
||||
|
||||
/manuscript/snippets/
|
||||
22
.php_cs.dist
Normal file
22
.php_cs.dist
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->exclude(['vendor'])
|
||||
->in(__DIR__);
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
->setRiskyAllowed(true)
|
||||
->setRules(
|
||||
[
|
||||
'@PSR2' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'non_printable_character' => true,
|
||||
'ordered_imports' => ['sortAlgorithm' => 'alpha'],
|
||||
'dir_constant' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'visibility_required' => ['elements' => ['property', 'method', 'const']],
|
||||
'no_unused_imports' => true
|
||||
]
|
||||
)
|
||||
->setFinder($finder);
|
||||
33
.psalm-stubs/api-platform.php
Normal file
33
.psalm-stubs/api-platform.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ApiPlatform\Core\DataPersister
|
||||
{
|
||||
/** @template T */
|
||||
interface DataPersisterInterface
|
||||
{
|
||||
/**
|
||||
* @psalm-var mixed $data
|
||||
*/
|
||||
public function supports($data): bool;
|
||||
|
||||
/**
|
||||
* @psalm-var T $data
|
||||
* @psalm-return T|void
|
||||
*/
|
||||
public function persist($data);
|
||||
|
||||
/**
|
||||
* @psalm-var T $data
|
||||
* @psalm-return void
|
||||
*/
|
||||
public function remove($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends DataPersisterInterface<T>
|
||||
*/
|
||||
interface ContextAwareDataPersisterInterface extends DataPersisterInterface { }
|
||||
}
|
||||
83
.psalm-stubs/doctrine-orm.php
Normal file
83
.psalm-stubs/doctrine-orm.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM
|
||||
{
|
||||
/** @template T */
|
||||
abstract class AbstractQuery
|
||||
{
|
||||
/** @psalm-return EntityManagerInterface<T> */
|
||||
public function getEntityManager() { }
|
||||
|
||||
/**
|
||||
* @psalm-param string|1|2|3|4|5|null $hydrationMode
|
||||
* @psalm-return T[]
|
||||
*/
|
||||
public function getResult($hydrationMode = self::HYDRATE_OBJECT) { }
|
||||
|
||||
/**
|
||||
* @psalm-param string|1|2|3|4|5|null $hydrationMode
|
||||
* @psalm-return ?T
|
||||
* @throws NonUniqueResultException
|
||||
*/
|
||||
public function getOneOrNullResult($hydrationMode = null) { }
|
||||
|
||||
/**
|
||||
* @psalm-param string|1|2|3|4|5|null $hydrationMode
|
||||
* @psalm-return T
|
||||
* @throws NonUniqueResultException
|
||||
* @throws NoResultException
|
||||
*/
|
||||
public function getSingleResult($hydrationMode = null) { }
|
||||
|
||||
/**
|
||||
* @psalm-param ArrayCollection|array|null $parameters
|
||||
* @psalm-param string|1|2|3|4|5|null $hydrationMode
|
||||
* @psalm-return \Doctrine\ORM\Internal\Hydration\IterableResult<T>
|
||||
*/
|
||||
public function iterate($parameters = null, $hydrationMode = null) { }
|
||||
|
||||
/**
|
||||
* @psalm-param ArrayCollection|array|null $parameters
|
||||
* @psalm-param string|1|2|3|4|5|null $hydrationMode
|
||||
* @psalm-return T|T[]|null
|
||||
*/
|
||||
public function execute($parameters = null, $hydrationMode = null) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends AbstractQuery<T>
|
||||
*/
|
||||
final class Query extends AbstractQuery {}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class QueryBuilder
|
||||
{
|
||||
/** @psalm-return \Doctrine\ORM\Query<T> */
|
||||
public function getQuery() { }
|
||||
}
|
||||
|
||||
/** @template T */
|
||||
class EntityRepository
|
||||
{
|
||||
/**
|
||||
* @psalm-param string $alias
|
||||
* @psalm-param string $indexBy
|
||||
* @psalm-return \Doctrine\ORM\QueryBuilder<T>
|
||||
*/
|
||||
public function createQueryBuilder($alias, $indexBy = null) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Doctrine\ORM\Internal\Hydration
|
||||
{
|
||||
/**
|
||||
* @template T
|
||||
* @template-implements \Iterator<T>
|
||||
*/
|
||||
class IterableResult implements \Iterator { }
|
||||
}
|
||||
23
.psalm-stubs/lstrojny-functional-php.php
Normal file
23
.psalm-stubs/lstrojny-functional-php.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Functional;
|
||||
|
||||
/**
|
||||
* @template A
|
||||
*
|
||||
* @psalm-param \Traversable<A>|list<A> $collection
|
||||
* @psalm-param callable(A, mixed=, A[]): bool $callback
|
||||
* @psalm-return list<A>
|
||||
*/
|
||||
function select($collection, callable $callback) {}
|
||||
|
||||
/**
|
||||
* @template B
|
||||
*
|
||||
* @psalm-param \Traversable<B>|list<B> $collection
|
||||
* @psalm-param callable(B, mixed=, B[]): bool $callback
|
||||
* @psalm-return B
|
||||
*/
|
||||
function head($collection, callable $callback = null) {}
|
||||
26
.psalm-stubs/symfony-messenger.php
Normal file
26
.psalm-stubs/symfony-messenger.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Symfony\Component\Messenger\Stamp
|
||||
{
|
||||
/** @template T */
|
||||
final class HandledStamp
|
||||
{
|
||||
/** @psam-return T */
|
||||
public function getResult() { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Symfony\Component\Messenger
|
||||
{
|
||||
trait HandleTrait
|
||||
{
|
||||
/**
|
||||
* @template T
|
||||
* @psalm-param object|Envelope $message
|
||||
* @psalm-return T|Envelope
|
||||
*/
|
||||
private function handle($message) { }
|
||||
}
|
||||
}
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM php:7.4-alpine AS base
|
||||
|
||||
RUN apk add --no-cache --update git
|
||||
|
||||
RUN docker-php-ext-install bcmath pdo_mysql pcntl posix mysqli
|
||||
|
||||
RUN curl --silent --show-error https://getcomposer.org/installer | php && \
|
||||
mv composer.phar /usr/local/bin/composer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY src src
|
||||
COPY config config
|
||||
COPY public public
|
||||
COPY templates templates
|
||||
COPY composer.lock ./
|
||||
COPY composer.json ./
|
||||
COPY symfony.lock ./
|
||||
COPY .env ./
|
||||
COPY .env.test ./
|
||||
|
||||
FROM base AS test
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY bin bin
|
||||
COPY tests tests
|
||||
COPY .psalm-stubs .psalm-stubs
|
||||
COPY .php_cs.dist ./
|
||||
COPY psalm.xml ./
|
||||
COPY phpunit.xml.dist ./
|
||||
RUN composer install
|
||||
42
bin/console
Executable file
42
bin/console
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\ErrorHandler\Debug;
|
||||
|
||||
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
|
||||
}
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (!class_exists(Application::class)) {
|
||||
throw new LogicException('You need to add "symfony/framework-bundle" as a Composer dependency.');
|
||||
}
|
||||
|
||||
$input = new ArgvInput();
|
||||
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
|
||||
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
|
||||
}
|
||||
|
||||
if ($input->hasParameterOption('--no-debug', true)) {
|
||||
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
|
||||
if (class_exists(Debug::class)) {
|
||||
Debug::enable();
|
||||
}
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$application = new Application($kernel);
|
||||
$application->run($input);
|
||||
BIN
bin/php-cs-fixer
Executable file
BIN
bin/php-cs-fixer
Executable file
Binary file not shown.
109
composer.json
Normal file
109
composer.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
"beberlei/assert": "^3.2",
|
||||
"lstrojny/functional-php": "^1.11",
|
||||
"ramsey/uuid": "^3.9",
|
||||
"ramsey/uuid-doctrine": "^1.6",
|
||||
"symfony/asset": "5.0.*",
|
||||
"symfony/console": "5.0.*",
|
||||
"symfony/dotenv": "5.0.*",
|
||||
"symfony/expression-language": "5.0.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/framework-bundle": "5.0.*",
|
||||
"symfony/http-client": "5.0.*",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/process": "5.0.*",
|
||||
"symfony/yaml": "5.0.*",
|
||||
"webonyx/graphql-php": "^0.13.8",
|
||||
"api-platform/core": "2.5.*",
|
||||
"nelmio/cors-bundle": "2.0.*",
|
||||
"symfony/security-bundle": "5.0.*",
|
||||
"symfony/twig-bundle": "5.0.*",
|
||||
"symfony/messenger": "5.0.*",
|
||||
"symfony/validator": "5.0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"bunny/bunny": "^0.2",
|
||||
"dama/doctrine-test-bundle": "^6.3",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"elasticsearch/elasticsearch": "^6.0",
|
||||
"fzaninotto/faker": "^1.9",
|
||||
"infection/infection": "^0.16.0",
|
||||
"keyvanakbary/mimic": "^1.0",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"predis/predis": "^1.1",
|
||||
"spatie/async": "^1.1",
|
||||
"symfony/browser-kit": "5.0.*",
|
||||
"symfony/debug-pack": "^1.0",
|
||||
"symfony/maker-bundle": "^1.14",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"theofidry/psysh-bundle": "^4.3",
|
||||
"twig/twig": "^2.4",
|
||||
"vimeo/psalm": "^3.9",
|
||||
"weirdan/doctrine-psalm-plugin": "^0.10.0",
|
||||
"zumba/json-serializer": "^2.1"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"paragonie/random_compat": "2.*",
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php71": "*",
|
||||
"symfony/polyfill-php70": "*",
|
||||
"symfony/polyfill-php56": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"fix-cs": "php bin/php-cs-fixer --verbose --config=.php_cs.dist --using-cache=no --path-mode=intersection fix",
|
||||
"unit-tests": "php vendor/bin/phpunit",
|
||||
"psalm": "php vendor/bin/psalm --config=psalm.xml --show-info=true --no-cache",
|
||||
"clear-db": [
|
||||
"php bin/console doctrine:database:drop --force",
|
||||
"php bin/console doctrine:database:create",
|
||||
"php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing",
|
||||
"php bin/console messenger:setup-transports"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": true,
|
||||
"require": "5.0.*"
|
||||
},
|
||||
"src-dir": "src/App"
|
||||
}
|
||||
}
|
||||
23
composer.json.old
Normal file
23
composer.json.old
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "buenosvinos/cqrs-in-php",
|
||||
"require": {
|
||||
"php": ">=7.1.0",
|
||||
|
||||
"twig/twig": "^2.4",
|
||||
"keyvanakbary/mimic": "^1.0",
|
||||
"mockery/mockery": "^1.0",
|
||||
"ramsey/uuid": "^3.7",
|
||||
"elasticsearch/elasticsearch": "^6.0",
|
||||
"bunny/bunny": "^0.2",
|
||||
"zumba/json-serializer": "^2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~6.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "": "src/" }
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin"
|
||||
}
|
||||
}
|
||||
10980
composer.lock
generated
Normal file
10980
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
config/bootstrap.php
Normal file
23
config/bootstrap.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
// Load cached env vars if the .env.local.php file exists
|
||||
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
|
||||
if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
|
||||
foreach ($env as $k => $v) {
|
||||
$_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
|
||||
}
|
||||
} elseif (!class_exists(Dotenv::class)) {
|
||||
throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
|
||||
} else {
|
||||
// load all the .env files
|
||||
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
$_SERVER += $_ENV;
|
||||
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
|
||||
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
|
||||
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
|
||||
18
config/bundles.php
Normal file
18
config/bundles.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Fidry\PsyshBundle\PsyshBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
];
|
||||
10
config/packages/api_platform.yaml
Normal file
10
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
api_platform:
|
||||
title: Cheeper API
|
||||
show_webby: false
|
||||
mapping:
|
||||
paths:
|
||||
- '%kernel.project_dir%/src/App/API/Resources'
|
||||
patch_formats:
|
||||
json: ['application/merge-patch+json']
|
||||
swagger:
|
||||
versions: [3]
|
||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
||||
4
config/packages/dev/debug.yaml
Normal file
4
config/packages/dev/debug.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
||||
5
config/packages/dev/messenger.yaml
Normal file
5
config/packages/dev/messenger.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
framework:
|
||||
messenger:
|
||||
transports:
|
||||
# This is the trick for fixtures or testing
|
||||
async: 'sync://'
|
||||
19
config/packages/dev/monolog.yaml
Normal file
19
config/packages/dev/monolog.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
6
config/packages/dev/web_profiler.yaml
Normal file
6
config/packages/dev/web_profiler.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
25
config/packages/doctrine.yaml
Normal file
25
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '5.7'
|
||||
schema_filter: '~^(?!messenger_messages)~'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/src/App/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
CheeperDomainModel:
|
||||
is_bundle: false
|
||||
type: xml
|
||||
dir: '%kernel.project_dir%/src/Cheeper/Infrastructure/Persistence/doctrine-mappings'
|
||||
prefix: 'Cheeper\DomainModel'
|
||||
alias: CheeperDomainModel
|
||||
5
config/packages/doctrine_migrations.yaml
Normal file
5
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
doctrine_migrations:
|
||||
dir_name: '%kernel.project_dir%/migrations'
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
namespace: DoctrineMigrations
|
||||
16
config/packages/framework.yaml
Normal file
16
config/packages/framework.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
#http_method_override: true
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
27
config/packages/messenger.yaml
Normal file
27
config/packages/messenger.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
framework:
|
||||
messenger:
|
||||
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
||||
# failure_transport: failed
|
||||
default_bus: none.bus
|
||||
buses:
|
||||
none.bus:
|
||||
default_middleware: allow_no_handlers
|
||||
command.bus:
|
||||
middleware:
|
||||
- doctrine_ping_connection
|
||||
- doctrine_close_connection
|
||||
- doctrine_transaction
|
||||
transports:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
|
||||
# failed: 'doctrine://default?queue_name=failed'
|
||||
sync: 'sync://'
|
||||
routing:
|
||||
# Route your messages to the transports
|
||||
# One way to route the messages is using Interfaces or Parent Classes
|
||||
# that helps on not having to maintaining this routing 1 to 1 approach
|
||||
'Cheeper\DomainModel\DomainEvent': async
|
||||
'Cheeper\Application\Command\AsyncCommand': async
|
||||
'Cheeper\Application\Command\SyncCommand': sync
|
||||
10
config/packages/nelmio_cors.yaml
Normal file
10
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
origin_regex: true
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
paths:
|
||||
'^/': null
|
||||
20
config/packages/prod/doctrine.yaml
Normal file
20
config/packages/prod/doctrine.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
26
config/packages/prod/monolog.yaml
Normal file
26
config/packages/prod/monolog.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
|
||||
# Uncomment to log deprecations
|
||||
#deprecation:
|
||||
# type: stream
|
||||
# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
||||
#deprecation_filter:
|
||||
# type: filter
|
||||
# handler: deprecation
|
||||
# max_level: info
|
||||
# channels: ["php"]
|
||||
3
config/packages/prod/routing.yaml
Normal file
3
config/packages/prod/routing.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
7
config/packages/ramsey_uuid_doctrine.yaml
Normal file
7
config/packages/ramsey_uuid_doctrine.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
types:
|
||||
uuid: 'Ramsey\Uuid\Doctrine\UuidType'
|
||||
uuid_binary: 'Ramsey\Uuid\Doctrine\UuidBinaryType'
|
||||
mapping_types:
|
||||
uuid_binary: binary
|
||||
3
config/packages/routing.yaml
Normal file
3
config/packages/routing.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
23
config/packages/security.yaml
Normal file
23
config/packages/security.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
anonymous: lazy
|
||||
provider: users_in_memory
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#firewalls-authentication
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
4
config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
4
config/packages/test/dama_doctrine_test_bundle.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
||||
4
config/packages/test/framework.yaml
Normal file
4
config/packages/test/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
||||
12
config/packages/test/monolog.yaml
Normal file
12
config/packages/test/monolog.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
2
config/packages/test/twig.yaml
Normal file
2
config/packages/test/twig.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
twig:
|
||||
strict_variables: true
|
||||
3
config/packages/test/validator.yaml
Normal file
3
config/packages/test/validator.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
6
config/packages/test/web_profiler.yaml
Normal file
6
config/packages/test/web_profiler.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
2
config/packages/twig.yaml
Normal file
2
config/packages/twig.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
8
config/packages/validator.yaml
Normal file
8
config/packages/validator.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
framework:
|
||||
validation:
|
||||
email_validation_mode: html5
|
||||
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
3
config/routes.yaml
Normal file
3
config/routes.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
#index:
|
||||
# path: /
|
||||
# controller: App\Controller\DefaultController::index
|
||||
7
config/routes/annotations.yaml
Normal file
7
config/routes/annotations.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
controllers:
|
||||
resource: ../../src/App/Controller/
|
||||
type: annotation
|
||||
|
||||
kernel:
|
||||
resource: ../../src/App/Kernel.php
|
||||
type: annotation
|
||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
resource: .
|
||||
type: api_platform
|
||||
prefix: /api
|
||||
3
config/routes/dev/framework.yaml
Normal file
3
config/routes/dev/framework.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
7
config/routes/dev/web_profiler.yaml
Normal file
7
config/routes/dev/web_profiler.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
43
config/services.yaml
Normal file
43
config/services.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
bind:
|
||||
Cheeper\DomainModel\Cheep\Cheeps $cheeps: '@Cheeper\Infrastructure\Persistence\DoctrineOrmCheeps'
|
||||
Cheeper\DomainModel\Author\Authors $authors: '@Cheeper\Infrastructure\Persistence\DoctrineOrmAuthors'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/App/*'
|
||||
exclude: '../src/App/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Helpers}'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
resource: '../src/App/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
App\ArgumentResolver\UuidResolver:
|
||||
tags:
|
||||
- { name: controller.argument_value_resolver, priority: 200 }
|
||||
Cheeper\Infrastructure\:
|
||||
resource: '../src/Cheeper/Infrastructure/**/*.php'
|
||||
Cheeper\Application\Command\:
|
||||
resource: '../src/Cheeper/Application/Command/**/*Handler.php'
|
||||
autoconfigure: false
|
||||
tags:
|
||||
- { name: messenger.message_handler, bus: command.bus }
|
||||
ApiPlatform\Core\Bridge\RamseyUuid\Identifier\Normalizer\UuidNormalizer:
|
||||
tags:
|
||||
- { name: api_platform.identifier.denormalizer }
|
||||
4
config/services_test.yaml
Normal file
4
config/services_test.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
services:
|
||||
App\Helpers\ServiceLocatorForTests:
|
||||
public: true
|
||||
autowire: true
|
||||
45
docker-compose.yaml
Normal file
45
docker-compose.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:5.0
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.0.0
|
||||
environment:
|
||||
ES_JAVA_OPTS: "-Xmx256m -Xms256m"
|
||||
ports:
|
||||
- 9200:9200
|
||||
- 9300:9300
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.7.0
|
||||
ports:
|
||||
- 4369:4369
|
||||
- 5671:5671
|
||||
- 5672:5672
|
||||
- 25672:25672
|
||||
|
||||
mysql:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: example
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: pass
|
||||
MYSQL_DATABASE: db
|
||||
volumes:
|
||||
- ./.docker/mysql:/docker-entrypoint-initdb.d
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
depends_on:
|
||||
- mysql
|
||||
- rabbitmq
|
||||
- elasticsearch
|
||||
- redis
|
||||
16
infection.json.dist
Normal file
16
infection.json.dist
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"source": {
|
||||
"directories": [
|
||||
"src/Cheeper"
|
||||
],
|
||||
"excludes": [
|
||||
"src/Cheeper/Infrastructure"
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"text": "infection.log"
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200309224330.php
Normal file
35
migrations/Version20200309224330.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200309224330 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE follow_relationships (id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', followee_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', followed_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', followee_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', followed_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', PRIMARY KEY(id_id, followee_id, followed_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE follow_relationships');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200309225606.php
Normal file
35
migrations/Version20200309225606.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200309225606 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE users (user_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', name VARCHAR(100) NOT NULL, biography LONGTEXT NOT NULL, location VARCHAR(100) NOT NULL, user_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', user_name_user_name VARCHAR(100) NOT NULL, website_uri VARCHAR(255) NOT NULL, birth_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(user_id_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE users');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200309230710.php
Normal file
35
migrations/Version20200309230710.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200309230710 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE tweets (user_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', tweet_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', user_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', tweet_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', tweet_message_message VARCHAR(260) NOT NULL, PRIMARY KEY(user_id_id, tweet_id_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE tweets');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200309235223.php
Normal file
35
migrations/Version20200309235223.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200309235223 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE tweets ADD tweet_date_message VARCHAR(260) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE tweets DROP tweet_date_message');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200310155949.php
Normal file
35
migrations/Version20200310155949.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200310155949 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE tweets ADD tweet_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', DROP tweet_date_message');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE tweets ADD tweet_date_message VARCHAR(260) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, DROP tweet_date_date');
|
||||
}
|
||||
}
|
||||
41
migrations/Version20200315170724.php
Normal file
41
migrations/Version20200315170724.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200315170724 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE cheeps (cheep_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', author_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', cheep_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', author_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', cheep_message_message VARCHAR(260) NOT NULL, cheep_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(cheep_id_id, author_id_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('CREATE TABLE authors (author_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', name VARCHAR(100) NOT NULL, biography LONGTEXT NOT NULL, location VARCHAR(100) NOT NULL, author_id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', user_name_user_name VARCHAR(100) NOT NULL, website_uri VARCHAR(255) NOT NULL, birth_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(author_id_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('DROP TABLE tweets');
|
||||
$this->addSql('DROP TABLE users');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE tweets (user_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', tweet_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', user_id_id_as_string CHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci` COMMENT \'(DC2Type:uuid)\', tweet_id_id_as_string CHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci` COMMENT \'(DC2Type:uuid)\', tweet_message_message VARCHAR(260) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tweet_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(user_id_id, tweet_id_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
|
||||
$this->addSql('CREATE TABLE users (user_id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', name VARCHAR(100) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, biography LONGTEXT CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, location VARCHAR(100) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, user_id_id_as_string CHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci` COMMENT \'(DC2Type:uuid)\', user_name_user_name VARCHAR(100) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, website_uri VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, birth_date_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(user_id_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
|
||||
$this->addSql('DROP TABLE cheeps');
|
||||
$this->addSql('DROP TABLE authors');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20200514194647.php
Normal file
35
migrations/Version20200514194647.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200514194647 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE authors ADD following LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE authors DROP following');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20200515105553.php
Normal file
33
migrations/Version20200515105553.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200515105553 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return 'Remove table follow_relationships';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE follow_relationships');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE follow_relationships (id_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', followee_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', followed_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', id_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', followee_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', followed_id_as_string CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', PRIMARY KEY(id_id, followee_id, followed_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
}
|
||||
33
phpunit.xml.dist
Normal file
33
phpunit.xml.dist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src/Cheeper</directory>
|
||||
<exclude>
|
||||
<directory>src/Cheeper/Infrastructure</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
<extensions>
|
||||
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
|
||||
</extensions>
|
||||
</phpunit>
|
||||
32
psalm.xml
Normal file
32
psalm.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="true"
|
||||
resolveFromConfigFile="true"
|
||||
strictBinaryOperands="true"
|
||||
allowPhpStormGenerics="true"
|
||||
findUnusedVariablesAndParams="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src"/>
|
||||
<ignoreFiles>
|
||||
<directory name="vendor"/>
|
||||
<directory name="src/CheeperSpaghetti/"/>
|
||||
<file name="src/CheeperLayered/CheepController.php"/>
|
||||
<file name="src/App/Kernel.php"/>
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
|
||||
<stubs>
|
||||
<file name=".psalm-stubs/lstrojny-functional-php.php"/>
|
||||
<file name=".psalm-stubs/doctrine-orm.php"/>
|
||||
<file name=".psalm-stubs/symfony-messenger.php"/>
|
||||
<file name=".psalm-stubs/api-platform.php"/>
|
||||
</stubs>
|
||||
|
||||
<plugins>
|
||||
<pluginClass class="Weirdan\DoctrinePsalmPlugin\Plugin"/>
|
||||
</plugins>
|
||||
</psalm>
|
||||
27
public/index.php
Normal file
27
public/index.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Component\ErrorHandler\Debug;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
|
||||
Debug::enable();
|
||||
}
|
||||
|
||||
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
|
||||
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
|
||||
}
|
||||
|
||||
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
|
||||
Request::setTrustedHosts([$trustedHosts]);
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
||||
56
src/App/API/DataPersister/AuthorDataPersister.php
Normal file
56
src/App/API/DataPersister/AuthorDataPersister.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\DataPersister;
|
||||
|
||||
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
|
||||
use App\API\Resources\Author;
|
||||
use App\Messenger\CommandBus;
|
||||
use Cheeper\Application\Command\Author\SignUp;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/** @template-implements ContextAwareDataPersisterInterface<Author> */
|
||||
final class AuthorDataPersister implements ContextAwareDataPersisterInterface
|
||||
{
|
||||
private CommandBus $commandBus;
|
||||
|
||||
public function __construct(CommandBus $commandBus)
|
||||
{
|
||||
$this->commandBus = $commandBus;
|
||||
}
|
||||
|
||||
/** @param mixed $data */
|
||||
public function supports($data, array $context = []): bool
|
||||
{
|
||||
return $data instanceof Author && null === $data->id;
|
||||
}
|
||||
|
||||
/** @param Author $data */
|
||||
public function persist($data, array $context = []): Author
|
||||
{
|
||||
$authorId = Uuid::uuid4();
|
||||
|
||||
$this->commandBus->execute(
|
||||
new SignUp(
|
||||
$authorId,
|
||||
$data->userName,
|
||||
$data->name,
|
||||
$data->biography,
|
||||
$data->location,
|
||||
$data->website,
|
||||
$data->birthDate
|
||||
)
|
||||
);
|
||||
|
||||
$data->id = $authorId;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** @param Author $data */
|
||||
public function remove($data, array $context = []): void
|
||||
{
|
||||
// TODO: Implement remove() method.
|
||||
}
|
||||
}
|
||||
50
src/App/API/DataPersister/FollowerDataPersister.php
Normal file
50
src/App/API/DataPersister/FollowerDataPersister.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\DataPersister;
|
||||
|
||||
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
|
||||
use App\API\Resources\Follower;
|
||||
use App\Messenger\CommandBus;
|
||||
use Cheeper\Application\Command\Author\Follow;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/** @template-implements ContextAwareDataPersisterInterface<Follower> */
|
||||
final class FollowerDataPersister implements ContextAwareDataPersisterInterface
|
||||
{
|
||||
private CommandBus $commandBus;
|
||||
|
||||
public function __construct(CommandBus $commandBus)
|
||||
{
|
||||
$this->commandBus = $commandBus;
|
||||
}
|
||||
|
||||
/** @param mixed $data */
|
||||
public function supports($data, array $context = []): bool
|
||||
{
|
||||
return $data instanceof Follower;
|
||||
}
|
||||
|
||||
/** @param Follower $data */
|
||||
public function persist($data, array $context = []): Follower
|
||||
{
|
||||
$data->id = Uuid::uuid4();
|
||||
|
||||
$this->commandBus->execute(
|
||||
new Follow(
|
||||
$data->id,
|
||||
$data->from,
|
||||
$data->to
|
||||
)
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** @param Follower $data */
|
||||
public function remove($data, array $context = []): void
|
||||
{
|
||||
// TODO: Implement remove() method.
|
||||
}
|
||||
}
|
||||
48
src/App/API/Resources/Author.php
Normal file
48
src/App/API/Resources/Author.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Resources;
|
||||
|
||||
use ApiPlatform\Core\Annotation\ApiProperty;
|
||||
use ApiPlatform\Core\Annotation\ApiResource;
|
||||
use DateTimeImmutable;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
/**
|
||||
* Properties in DTOs always need to be annotated with the @var annotation in order to work properly. This is so because
|
||||
* API Platform relies heavily on it to guess property types.
|
||||
*
|
||||
* @psalm-suppress MissingConstructor
|
||||
*
|
||||
* @ApiResource(
|
||||
* collectionOperations={"post"},
|
||||
* itemOperations={"get"}
|
||||
* )
|
||||
*/
|
||||
final class Author
|
||||
{
|
||||
/**
|
||||
* @var UuidInterface|null
|
||||
* @ApiProperty(identifier=true)
|
||||
*/
|
||||
public ?UuidInterface $id = null;
|
||||
|
||||
/** @var string */
|
||||
public string $userName;
|
||||
|
||||
/** @var string */
|
||||
public string $name;
|
||||
|
||||
/** @var string */
|
||||
public string $biography;
|
||||
|
||||
/** @var string */
|
||||
public string $location;
|
||||
|
||||
/** @var string */
|
||||
public string $website;
|
||||
|
||||
/** @var DateTimeImmutable */
|
||||
public DateTimeImmutable $birthDate;
|
||||
}
|
||||
35
src/App/API/Resources/Follower.php
Normal file
35
src/App/API/Resources/Follower.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Resources;
|
||||
|
||||
use ApiPlatform\Core\Annotation\ApiProperty;
|
||||
use ApiPlatform\Core\Annotation\ApiResource;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
/**
|
||||
* Properties in DTOs always need to be annotated with the "@var" annotation in order to work properly. This is so
|
||||
* because API Platform relies heavily on it to guess property types.
|
||||
*
|
||||
* @psalm-suppress MissingConstructor
|
||||
*
|
||||
* @ApiResource(
|
||||
* collectionOperations={"post"},
|
||||
* itemOperations={"get"}
|
||||
* )
|
||||
*/
|
||||
final class Follower
|
||||
{
|
||||
/**
|
||||
* @var UuidInterface|null
|
||||
* @ApiProperty(identifier=true)
|
||||
*/
|
||||
public ?UuidInterface $id;
|
||||
|
||||
/** @var string */
|
||||
public string $from;
|
||||
|
||||
/** @var string */
|
||||
public string $to;
|
||||
}
|
||||
28
src/App/ArgumentResolver/UuidResolver.php
Normal file
28
src/App/ArgumentResolver/UuidResolver.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ArgumentResolver;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
|
||||
final class UuidResolver implements ArgumentValueResolverInterface
|
||||
{
|
||||
public function supports(Request $request, ArgumentMetadata $argument): bool
|
||||
{
|
||||
return UuidInterface::class === $argument->getType();
|
||||
}
|
||||
|
||||
public function resolve(Request $request, ArgumentMetadata $argument): iterable
|
||||
{
|
||||
yield Uuid::fromString(
|
||||
$request->attributes->getAlnum(
|
||||
$argument->getName()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
0
src/App/Controller/.gitignore
vendored
Normal file
0
src/App/Controller/.gitignore
vendored
Normal file
17
src/App/DataFixtures/AppFixtures.php
Normal file
17
src/App/DataFixtures/AppFixtures.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
// $product = new Product();
|
||||
// $manager->persist($product);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
0
src/App/Entity/.gitignore
vendored
Normal file
0
src/App/Entity/.gitignore
vendored
Normal file
38
src/App/Helpers/ServiceLocatorForTests.php
Normal file
38
src/App/Helpers/ServiceLocatorForTests.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Cheeper\Infrastructure\Persistence\DoctrineOrmAuthors;
|
||||
use Cheeper\Infrastructure\Persistence\DoctrineOrmCheeps;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
final class ServiceLocatorForTests
|
||||
{
|
||||
private DoctrineOrmAuthors $authors;
|
||||
private DoctrineOrmCheeps $cheeps;
|
||||
private ManagerRegistry $doctrine;
|
||||
|
||||
public function __construct(ManagerRegistry $doctrine, DoctrineOrmAuthors $authors, DoctrineOrmCheeps $cheeps)
|
||||
{
|
||||
$this->authors = $authors;
|
||||
$this->cheeps = $cheeps;
|
||||
$this->doctrine = $doctrine;
|
||||
}
|
||||
|
||||
public function authors(): DoctrineOrmAuthors
|
||||
{
|
||||
return $this->authors;
|
||||
}
|
||||
|
||||
public function cheeps(): DoctrineOrmCheeps
|
||||
{
|
||||
return $this->cheeps;
|
||||
}
|
||||
|
||||
public function doctrine(): ManagerRegistry
|
||||
{
|
||||
return $this->doctrine;
|
||||
}
|
||||
}
|
||||
54
src/App/Kernel.php
Normal file
54
src/App/Kernel.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\Config\Resource\FileResource;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
use Symfony\Component\Routing\RouteCollectionBuilder;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public function registerBundles(): iterable
|
||||
{
|
||||
$contents = require $this->getProjectDir().'/config/bundles.php';
|
||||
foreach ($contents as $class => $envs) {
|
||||
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
|
||||
yield new $class();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getProjectDir(): string
|
||||
{
|
||||
return \dirname(__DIR__, 2);
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
|
||||
{
|
||||
$container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
|
||||
$container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
|
||||
$container->setParameter('container.dumper.inline_factories', true);
|
||||
$confDir = $this->getProjectDir().'/config';
|
||||
|
||||
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes): void
|
||||
{
|
||||
$confDir = $this->getProjectDir().'/config';
|
||||
|
||||
$routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
|
||||
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
|
||||
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
|
||||
}
|
||||
}
|
||||
33
src/App/Messenger/CommandBus.php
Normal file
33
src/App/Messenger/CommandBus.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Messenger;
|
||||
|
||||
use Cheeper\Application\Command\AsyncCommand;
|
||||
use Cheeper\Application\Command\SyncCommand;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\HandleTrait;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final class CommandBus
|
||||
{
|
||||
use HandleTrait;
|
||||
|
||||
public function __construct(MessageBusInterface $commandBus)
|
||||
{
|
||||
$this->messageBus = $commandBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param AsyncCommand|SyncCommand|object $command
|
||||
* @return T|Envelope The handler returned value
|
||||
*/
|
||||
public function execute($command)
|
||||
{
|
||||
if ($command instanceof SyncCommand) {
|
||||
return $this->handle($command);
|
||||
}
|
||||
|
||||
return $this->messageBus->dispatch($command);
|
||||
}
|
||||
}
|
||||
0
src/App/Migrations/.gitignore
vendored
Normal file
0
src/App/Migrations/.gitignore
vendored
Normal file
0
src/App/Repository/.gitignore
vendored
Normal file
0
src/App/Repository/.gitignore
vendored
Normal file
48
src/Architecture/CQRS/Domain/AggregateRoot.php
Normal file
48
src/Architecture/CQRS/Domain/AggregateRoot.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
//snippet aggregate-root
|
||||
class AggregateRoot
|
||||
{
|
||||
/** @var DomainEvent[] */
|
||||
private array $recordedEvents = [];
|
||||
|
||||
protected function recordApplyAndPublishThat(DomainEvent $event): void
|
||||
{
|
||||
$this->recordThat($event);
|
||||
$this->applyThat($event);
|
||||
$this->publishThat($event);
|
||||
}
|
||||
|
||||
protected function recordThat(DomainEvent $event): void
|
||||
{
|
||||
$this->recordedEvents[] = $event;
|
||||
}
|
||||
|
||||
protected function applyThat(DomainEvent $event): void
|
||||
{
|
||||
$className = (new \ReflectionClass($event))->getShortName();
|
||||
|
||||
$modifier = 'apply' . $className;
|
||||
|
||||
$this->$modifier($event);
|
||||
}
|
||||
|
||||
protected function publishThat(DomainEvent $event): void
|
||||
{
|
||||
DomainEventPublisher::instance()->publish($event);
|
||||
}
|
||||
|
||||
/** @return DomainEvent[] */
|
||||
public function recordedEvents(): array
|
||||
{
|
||||
return $this->recordedEvents;
|
||||
}
|
||||
|
||||
public function clearEvents(): void
|
||||
{
|
||||
$this->recordedEvents = [];
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
25
src/Architecture/CQRS/Domain/CategoryId.php
Normal file
25
src/Architecture/CQRS/Domain/CategoryId.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class CategoryId
|
||||
{
|
||||
private string $id;
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public static function create(): CategoryId
|
||||
{
|
||||
return new static(Uuid::uuid4()->toString());
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
7
src/Architecture/CQRS/Domain/DomainEvent.php
Normal file
7
src/Architecture/CQRS/Domain/DomainEvent.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class DomainEvent
|
||||
{
|
||||
}
|
||||
47
src/Architecture/CQRS/Domain/DomainEventPublisher.php
Normal file
47
src/Architecture/CQRS/Domain/DomainEventPublisher.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class DomainEventPublisher
|
||||
{
|
||||
/** @var Subscriber[] */
|
||||
private array $subscribers = [];
|
||||
private static ?DomainEventPublisher $instance = null;
|
||||
private int $id = 0;
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
if (null === static::$instance) {
|
||||
static::$instance = new self();
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public function subscribe(Subscriber $aDomainEventSubscriber): int
|
||||
{
|
||||
$id = $this->id;
|
||||
$this->subscribers[$id] = $aDomainEventSubscriber;
|
||||
$this->id++;
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function ofId(int $id): ?Subscriber
|
||||
{
|
||||
return isset($this->subscribers[$id]) ? $this->subscribers[$id] : null;
|
||||
}
|
||||
|
||||
public function unsubscribe(int $id): void
|
||||
{
|
||||
unset($this->subscribers[$id]);
|
||||
}
|
||||
|
||||
public function publish(DomainEvent $aDomainEvent): void
|
||||
{
|
||||
foreach ($this->subscribers as $aSubscriber) {
|
||||
if ($aSubscriber->isSubscribedTo($aDomainEvent)) {
|
||||
$aSubscriber->handle($aDomainEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/Architecture/CQRS/Domain/Post.php
Normal file
114
src/Architecture/CQRS/Domain/Post.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
//snippet post
|
||||
class Post extends AggregateRoot
|
||||
{
|
||||
//ignore
|
||||
private PostId $id;
|
||||
private ?string $title = null;
|
||||
private ?string $content = null;
|
||||
private bool $published = false;
|
||||
private array $categories = [];
|
||||
|
||||
protected function __construct(PostId $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function id(): PostId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function title(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function content(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function categories(): array
|
||||
{
|
||||
return array_values($this->categories);
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->published === true;
|
||||
}
|
||||
//end-ignore
|
||||
|
||||
public static function writeNewFrom(string $title, string $content): self
|
||||
{
|
||||
$postId = PostId::create();
|
||||
|
||||
$post = new static($postId);
|
||||
|
||||
$post->recordApplyAndPublishThat(
|
||||
new PostWasCreated($postId, $title, $content)
|
||||
);
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
public function publish(): void
|
||||
{
|
||||
$this->recordApplyAndPublishThat(
|
||||
new PostWasPublished($this->id)
|
||||
);
|
||||
}
|
||||
|
||||
public function categorizeIn(CategoryId $categoryId): void
|
||||
{
|
||||
$this->recordApplyAndPublishThat(
|
||||
new PostWasCategorized($this->id, $categoryId)
|
||||
);
|
||||
}
|
||||
|
||||
public function changeContentFor(string $newContent): void
|
||||
{
|
||||
$this->recordApplyAndPublishThat(
|
||||
new PostContentWasChanged($this->id, $newContent)
|
||||
);
|
||||
}
|
||||
|
||||
public function changeTitleFor(string $newTitle): void
|
||||
{
|
||||
$this->recordApplyAndPublishThat(
|
||||
new PostTitleWasChanged($this->id, $newTitle)
|
||||
);
|
||||
}
|
||||
|
||||
protected function applyPostWasCreated(PostWasCreated $event): void
|
||||
{
|
||||
$this->id = $event->postId();
|
||||
$this->title = $event->title();
|
||||
$this->content = $event->content();
|
||||
}
|
||||
|
||||
protected function applyPostWasPublished(PostWasPublished $event): void
|
||||
{
|
||||
$this->published = true;
|
||||
}
|
||||
|
||||
protected function applyPostWasCategorized(PostWasCategorized $event): void
|
||||
{
|
||||
$this->categories[$event->categoryId()->id()] = $event->categoryId();
|
||||
}
|
||||
|
||||
protected function applyPostContentWasChanged(PostContentWasChanged $event): void
|
||||
{
|
||||
$this->content = $event->content();
|
||||
}
|
||||
|
||||
protected function applyPostTitleWasChanged(PostTitleWasChanged $event): void
|
||||
{
|
||||
$this->title = $event->title();
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
25
src/Architecture/CQRS/Domain/PostContentWasChanged.php
Normal file
25
src/Architecture/CQRS/Domain/PostContentWasChanged.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class PostContentWasChanged extends DomainEvent
|
||||
{
|
||||
private PostId $postId;
|
||||
private string $content;
|
||||
|
||||
public function __construct(PostId $postId, string $content)
|
||||
{
|
||||
$this->postId = $postId;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function postId(): PostId
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function content(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
30
src/Architecture/CQRS/Domain/PostId.php
Normal file
30
src/Architecture/CQRS/Domain/PostId.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class PostId
|
||||
{
|
||||
private string $id;
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public static function create(): PostId
|
||||
{
|
||||
return new static(Uuid::uuid4()->toString());
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
11
src/Architecture/CQRS/Domain/PostRepository.php
Normal file
11
src/Architecture/CQRS/Domain/PostRepository.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
//snippet post-repository
|
||||
interface PostRepository
|
||||
{
|
||||
public function save(Post $post): void;
|
||||
public function byId(PostId $id): ?Post;
|
||||
}
|
||||
//end-snippet
|
||||
25
src/Architecture/CQRS/Domain/PostTitleWasChanged.php
Normal file
25
src/Architecture/CQRS/Domain/PostTitleWasChanged.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class PostTitleWasChanged extends DomainEvent
|
||||
{
|
||||
private PostId $postId;
|
||||
private string $title;
|
||||
|
||||
public function __construct(PostId $postId, string $title)
|
||||
{
|
||||
$this->postId = $postId;
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function postId(): PostId
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
}
|
||||
25
src/Architecture/CQRS/Domain/PostWasCategorized.php
Normal file
25
src/Architecture/CQRS/Domain/PostWasCategorized.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class PostWasCategorized extends DomainEvent
|
||||
{
|
||||
private PostId $postId;
|
||||
private CategoryId $categoryId;
|
||||
|
||||
public function __construct(PostId $postId, CategoryId $categoryId)
|
||||
{
|
||||
$this->postId = $postId;
|
||||
$this->categoryId = $categoryId;
|
||||
}
|
||||
|
||||
public function postId(): PostId
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function categoryId(): CategoryId
|
||||
{
|
||||
return $this->categoryId;
|
||||
}
|
||||
}
|
||||
32
src/Architecture/CQRS/Domain/PostWasCreated.php
Normal file
32
src/Architecture/CQRS/Domain/PostWasCreated.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class PostWasCreated extends DomainEvent
|
||||
{
|
||||
private PostId $postId;
|
||||
private string $title;
|
||||
private string $content;
|
||||
|
||||
public function __construct(PostId $postId, string $title, string $content)
|
||||
{
|
||||
$this->postId = $postId;
|
||||
$this->title = $title;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function postId(): PostId
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function content(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
18
src/Architecture/CQRS/Domain/PostWasPublished.php
Normal file
18
src/Architecture/CQRS/Domain/PostWasPublished.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
class PostWasPublished extends DomainEvent
|
||||
{
|
||||
private PostId $postId;
|
||||
|
||||
public function __construct(PostId $postId)
|
||||
{
|
||||
$this->postId = $postId;
|
||||
}
|
||||
|
||||
public function postId(): PostId
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
}
|
||||
11
src/Architecture/CQRS/Domain/Projection.php
Normal file
11
src/Architecture/CQRS/Domain/Projection.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
//snippet projection
|
||||
interface Projection
|
||||
{
|
||||
public function listensTo(): string;
|
||||
public function project(DomainEvent $event): void;
|
||||
}
|
||||
//end-snippet
|
||||
9
src/Architecture/CQRS/Domain/Subscriber.php
Normal file
9
src/Architecture/CQRS/Domain/Subscriber.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Domain;
|
||||
|
||||
interface Subscriber
|
||||
{
|
||||
public function isSubscribedTo(DomainEvent $event): bool;
|
||||
public function handle(DomainEvent $event): void;
|
||||
}
|
||||
25
src/Architecture/CQRS/Example/BloatedPostRepository.php
Normal file
25
src/Architecture/CQRS/Example/BloatedPostRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Example;
|
||||
|
||||
use Architecture\CQRS\Domain\CategoryId;
|
||||
use Architecture\CQRS\Domain\Post;
|
||||
use Architecture\CQRS\Domain\PostId;
|
||||
|
||||
class TagId
|
||||
{
|
||||
}
|
||||
|
||||
//snippet bloated-post-repository
|
||||
interface PostRepository
|
||||
{
|
||||
public function save(Post $post): void;
|
||||
public function byId(PostId $id): Post;
|
||||
public function all(): array;
|
||||
public function byCategory(CategoryId $categoryId): Post;
|
||||
public function byTag(TagId $tagId): array;
|
||||
public function withComments(PostId $id): Post;
|
||||
public function groupedByMonth(): array;
|
||||
// ...
|
||||
}
|
||||
//end-snippet
|
||||
29
src/Architecture/CQRS/Example/model.sql
Normal file
29
src/Architecture/CQRS/Example/model.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- snippet model-example
|
||||
-- Definition of a UI view of a single post with its comments
|
||||
CREATE TABLE single_post_with_comments (
|
||||
id INTEGER NOT NULL,
|
||||
post_id INTEGER NOT NULL,
|
||||
post_title VARCHAR(100) NOT NULL,
|
||||
post_content TEXT NOT NULL,
|
||||
post_created_at DATETIME NOT NULL,
|
||||
comment_content TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Set up some data
|
||||
INSERT INTO single_post_with_comments VALUES
|
||||
(1, 1, "Layered architecture", "Lorem ipsum ...", NOW(), "Lorem ipsum ..."),
|
||||
(2, 1, "Layered architecture", "Lorem ipsum ...", NOW(), "Lorem ipsum ..."),
|
||||
(3, 2, "Hexagonal architecture", "Lorem ipsum ...", NOW(), "Lorem ipsum ..."),
|
||||
(4, 2, "Hexagonal architecture", "Lorem ipsum ...", NOW(), "Lorem ipsum ..."),
|
||||
(5, 3, "CQRS", "Lorem ipsum ...", NOW(), "Lorem ipsum ..."),
|
||||
(6, 3, "CQRS", "Lorem ipsum ...", NOW(), "Lorem ipsum ...");
|
||||
|
||||
-- Query it
|
||||
SELECT * FROM single_post_with_comments WHERE post_id = 1;
|
||||
-- end-snippet
|
||||
|
||||
-- snippet query-example
|
||||
SELECT * FROM posts_grouped_by_month_and_year ORDER BY month DESC, year ASC;
|
||||
SELECT * FROM posts_by_tags WHERE tag = "ddd";
|
||||
SELECT * FROM posts_by_author WHERE author_id = 1;
|
||||
-- end-snippet
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use Architecture\CQRS\Domain\Post;
|
||||
use Architecture\CQRS\Domain\PostId;
|
||||
use Architecture\CQRS\Domain\PostRepository;
|
||||
use Architecture\CQRS\Infrastructure\Projection\Projector;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
//snippet doctrine-post-repository
|
||||
class DoctrinePostRepository implements PostRepository
|
||||
{
|
||||
private EntityManager $em;
|
||||
private Projector $projector;
|
||||
|
||||
public function __construct(EntityManager $em, Projector $projector)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->projector = $projector;
|
||||
}
|
||||
|
||||
public function save(Post $post): void
|
||||
{
|
||||
$this->em->transactional(function (EntityManager $em) use ($post) {
|
||||
$em->persist($post);
|
||||
|
||||
foreach ($post->recordedEvents() as $event) {
|
||||
$em->persist($event);
|
||||
}
|
||||
});
|
||||
|
||||
$this->projector->project($post->recordedEvents());
|
||||
}
|
||||
|
||||
public function byId(PostId $id): ?Post
|
||||
{
|
||||
return $this->em->find(Post::class, $id);
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Architecture\CQRS\Domain\DomainEvent" inheritance-type="SINGLE_TABLE">
|
||||
<discriminator-column name="type" type="string" />
|
||||
<discriminator-map>
|
||||
<discriminator-mapping value="post_created" class="Architecture\CQRS\Domain\PostWasCreated" />
|
||||
</discriminator-map>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Architecture\CQRS\Domain\Post" table="posts">
|
||||
<id name="id" type="post_id" column="id">
|
||||
<generator strategy="NONE" />
|
||||
</id>
|
||||
|
||||
<field name="title" type="string" length="250" column="title"/>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Architecture\CQRS\Domain\PostWasCreated">
|
||||
<id name="postId" type="post_id" column="post_id"/>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Infrastructure\Persistence\Doctrine\Types;
|
||||
|
||||
use Architecture\CQRS\Domain\PostId;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
class PostIdType extends Type
|
||||
{
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
{
|
||||
return $platform->getGuidTypeDeclarationSQL($fieldDeclaration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return PostId
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
return new PostId((string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
/** @var PostId $value */
|
||||
return $value->id();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'post_id';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Infrastructure\Projection;
|
||||
|
||||
use Architecture\CQRS\Domain\DomainEvent;
|
||||
use Bunny\Channel;
|
||||
use Zumba\JsonSerializer\JsonSerializer;
|
||||
|
||||
//snippet async-projector
|
||||
class AsyncProjector
|
||||
{
|
||||
private Channel $channel;
|
||||
private JsonSerializer $serializer;
|
||||
|
||||
public function __construct(
|
||||
Channel $channel,
|
||||
JsonSerializer $serializer
|
||||
) {
|
||||
$this->channel = $channel;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/** @param DomainEvent[] $events */
|
||||
public function project(array $events): void
|
||||
{
|
||||
foreach ($events as $event) {
|
||||
$this->channel->publish(
|
||||
$this->serializer->serialize($event),
|
||||
[],
|
||||
'events'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Infrastructure\Projection\Elasticsearch;
|
||||
|
||||
use Architecture\CQRS\Domain\DomainEvent;
|
||||
use Architecture\CQRS\Domain\PostContentWasChanged;
|
||||
use Architecture\CQRS\Domain\Projection;
|
||||
use Elasticsearch\Client;
|
||||
|
||||
class PostContentWasChangedProjection implements Projection
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct(Client $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function listensTo(): string
|
||||
{
|
||||
return PostContentWasChanged::class;
|
||||
}
|
||||
|
||||
public function project(DomainEvent $event): void
|
||||
{
|
||||
/** @var PostContentWasChanged $event */
|
||||
$id = $event->postId()->id();
|
||||
|
||||
$this->client->update([
|
||||
'index' => 'posts',
|
||||
'type' => 'post',
|
||||
'id' => $id,
|
||||
'body' => ['doc' => [
|
||||
'content' => $event->content()
|
||||
]]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\CQRS\Infrastructure\Projection\Elasticsearch;
|
||||
|
||||
use Architecture\CQRS\Domain\DomainEvent;
|
||||
use Architecture\CQRS\Domain\PostTitleWasChanged;
|
||||
use Architecture\CQRS\Domain\Projection;
|
||||
use Elasticsearch\Client;
|
||||
|
||||
class PostTitleWasChangedProjection implements Projection
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct(Client $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function listensTo(): string
|
||||
{
|
||||
return PostTitleWasChanged::class;
|
||||
}
|
||||
|
||||
public function project(DomainEvent $event): void
|
||||
{
|
||||
/** @var PostTitleWasChanged $event */
|
||||
$id = $event->postId()->id();
|
||||
|
||||
$this->client->update([
|
||||
'index' => 'posts',
|
||||
'type' => 'post',
|
||||
'id' => $id,
|
||||
'body' => ['doc' => [
|
||||
'title' => $event->title()
|
||||
]]
|
||||
]);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user