Moved all reference code to a subfolder in order to do subtree splitting

This commit is contained in:
theUniC
2020-05-21 16:35:54 +02:00
commit 35c5fe1ea7
198 changed files with 18026 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
CREATE DATABASE test_db;
GRANT ALL PRIVILEGES ON test_db.* to 'user'@'%' WITH GRANT OPTION;

10
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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);

View 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 { }
}

View 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 { }
}

View 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) {}

View 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
View 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
View 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

Binary file not shown.

109
composer.json Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

23
config/bootstrap.php Normal file
View 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
View 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],
];

View 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]

View 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

View 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)%"

View File

@@ -0,0 +1,5 @@
framework:
messenger:
transports:
# This is the trick for fixtures or testing
async: 'sync://'

View 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"]

View File

@@ -0,0 +1,6 @@
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler: { only_exceptions: false }

View 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

View 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

View 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

View 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

View 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

View 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

View 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"]

View File

@@ -0,0 +1,3 @@
framework:
router:
strict_requirements: null

View File

@@ -0,0 +1,7 @@
doctrine:
dbal:
types:
uuid: 'Ramsey\Uuid\Doctrine\UuidType'
uuid_binary: 'Ramsey\Uuid\Doctrine\UuidBinaryType'
mapping_types:
uuid_binary: binary

View File

@@ -0,0 +1,3 @@
framework:
router:
utf8: true

View 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 }

View File

@@ -0,0 +1,4 @@
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@@ -0,0 +1,4 @@
framework:
test: true
session:
storage_id: session.storage.mock_file

View 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

View File

@@ -0,0 +1,2 @@
twig:
strict_variables: true

View File

@@ -0,0 +1,3 @@
framework:
validation:
not_compromised_password: false

View File

@@ -0,0 +1,6 @@
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

View File

@@ -0,0 +1,2 @@
twig:
default_path: '%kernel.project_dir%/templates'

View 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
View File

@@ -0,0 +1,3 @@
#index:
# path: /
# controller: App\Controller\DefaultController::index

View File

@@ -0,0 +1,7 @@
controllers:
resource: ../../src/App/Controller/
type: annotation
kernel:
resource: ../../src/App/Kernel.php
type: annotation

View File

@@ -0,0 +1,4 @@
api_platform:
resource: .
type: api_platform
prefix: /api

View File

@@ -0,0 +1,3 @@
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View 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
View 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 }

View File

@@ -0,0 +1,4 @@
services:
App\Helpers\ServiceLocatorForTests:
public: true
autowire: true

45
docker-compose.yaml Normal file
View 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
View File

@@ -0,0 +1,16 @@
{
"source": {
"directories": [
"src/Cheeper"
],
"excludes": [
"src/Cheeper/Infrastructure"
]
},
"logs": {
"text": "infection.log"
},
"mutators": {
"@default": true
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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
View 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
View 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
View 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);

View 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.
}
}

View 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.
}
}

View 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;
}

View 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;
}

View 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
View File

View 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
View File

View 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
View 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');
}
}

View 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
View File

0
src/App/Repository/.gitignore vendored Normal file
View File

View 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

View 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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Architecture\CQRS\Domain;
class DomainEvent
{
}

View 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);
}
}
}
}

View 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

View 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;
}
}

View 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;
}
}

View 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

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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

View File

@@ -0,0 +1,9 @@
<?php
namespace Architecture\CQRS\Domain;
interface Subscriber
{
public function isSubscribedTo(DomainEvent $event): bool;
public function handle(DomainEvent $event): void;
}

View 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

View 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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';
}
}

View File

@@ -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

View File

@@ -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()
]]
]);
}
}

View File

@@ -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