Started CommandBus. Changed Psalm for PHPStan as it's more strict and code ends up being more type-safe

This commit is contained in:
theUniC
2020-06-23 11:36:02 +02:00
parent 7c453bf8e6
commit e4379f1c21
56 changed files with 753 additions and 209 deletions

4
assets/js/custom.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.svg" {
const content: string;
export default content;
}

View File

@@ -39,6 +39,13 @@
"infection/infection": "^0.16.0",
"keyvanakbary/mimic": "^1.0",
"mockery/mockery": "^1.3",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12.30",
"phpstan/phpstan-beberlei-assert": "^0.12.2",
"phpstan/phpstan-deprecation-rules": "^0.12.4",
"phpstan/phpstan-doctrine": "^0.12.16",
"phpstan/phpstan-strict-rules": "^0.12.2",
"phpstan/phpstan-symfony": "^0.12.6",
"phpunit/phpunit": "^9.0",
"predis/predis": "^1.1",
"spatie/async": "^1.1",
@@ -46,6 +53,7 @@
"symfony/debug-pack": "^1.0",
"symfony/maker-bundle": "^1.14",
"symfony/profiler-pack": "^1.0",
"thecodingmachine/phpstan-safe-rule": "^1.0",
"theofidry/psysh-bundle": "^4.3",
"vimeo/psalm": "^3.9",
"weirdan/doctrine-psalm-plugin": "^0.11",
@@ -89,7 +97,7 @@
],
"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",
"phpstan": "php vendor/bin/phpstan analyse",
"clear-db": [
"php bin/console doctrine:database:drop --force",
"php bin/console doctrine:database:create",

447
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6270ddc380e804074cd7a0b84b947a87",
"content-hash": "f40392689f639eeeed4fd44d5bb777a1",
"packages": [
{
"name": "api-platform/core",
@@ -9358,6 +9358,398 @@
],
"time": "2020-03-05T15:02:03+00:00"
},
{
"name": "phpstan/extension-installer",
"version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/extension-installer.git",
"reference": "2e041def501d661b806f50000c8a4dccbd4907b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/2e041def501d661b806f50000c8a4dccbd4907b4",
"reference": "2e041def501d661b806f50000c8a4dccbd4907b4",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1 || ^2.0",
"php": "^7.1",
"phpstan/phpstan": ">=0.11.6"
},
"require-dev": {
"composer/composer": "^1.8",
"consistence/coding-standard": "^3.8",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16",
"phpstan/phpstan-strict-rules": "^0.11",
"slevomat/coding-standard": "^5.0.4"
},
"type": "composer-plugin",
"extra": {
"class": "PHPStan\\ExtensionInstaller\\Plugin"
},
"autoload": {
"psr-4": {
"PHPStan\\ExtensionInstaller\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Composer plugin for automatic installation of PHPStan extensions",
"time": "2020-03-31T16:00:42+00:00"
},
{
"name": "phpstan/phpstan",
"version": "0.12.30",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "1f2c16d3fbb5eec6e55fbe2358e32570cefa20e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1f2c16d3fbb5eec6e55fbe2358e32570cefa20e5",
"reference": "1f2c16d3fbb5eec6e55fbe2358e32570cefa20e5",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://www.patreon.com/phpstan",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2020-06-21T14:08:19+00:00"
},
{
"name": "phpstan/phpstan-beberlei-assert",
"version": "0.12.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-beberlei-assert.git",
"reference": "c13141d2548ded3724a56ff0f7df37384187a149"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-beberlei-assert/zipball/c13141d2548ded3724a56ff0f7df37384187a149",
"reference": "c13141d2548ded3724a56ff0f7df37384187a149",
"shasum": ""
},
"require": {
"php": "~7.1",
"phpstan/phpstan": "^0.12.3"
},
"require-dev": {
"beberlei/assert": "^2.9.5",
"consistence/coding-standard": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16.0",
"phpstan/phpstan-phpunit": "^0.12.3",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.5.18",
"slevomat/coding-standard": "^4.5.2"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan beberlei/assert extension",
"time": "2020-01-03T10:02:05+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
"version": "0.12.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
"reference": "9b4b8851fb5d59fd0eed00fbe9c22cfc328e0187"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/9b4b8851fb5d59fd0eed00fbe9c22cfc328e0187",
"reference": "9b4b8851fb5d59fd0eed00fbe9c22cfc328e0187",
"shasum": ""
},
"require": {
"php": "~7.1",
"phpstan/phpstan": "^0.12"
},
"require-dev": {
"consistence/coding-standard": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16.0",
"phpstan/phpstan-phpunit": "^0.12",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.5.2"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.",
"time": "2020-05-30T18:02:31+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
"version": "0.12.16",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "65146e35905478bfb4e2ba078ffca1a16029d4ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/65146e35905478bfb4e2ba078ffca1a16029d4ee",
"reference": "65146e35905478bfb4e2ba078ffca1a16029d4ee",
"shasum": ""
},
"require": {
"php": "~7.1",
"phpstan/phpstan": "^0.12.26"
},
"conflict": {
"doctrine/collections": "<1.0",
"doctrine/common": "<2.7",
"doctrine/mongodb-odm": "<1.2",
"doctrine/orm": "<2.5",
"doctrine/persistence": "<1.3"
},
"require-dev": {
"consistence/coding-standard": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"doctrine/collections": "^1.0",
"doctrine/common": "^2.7 || ^3.0",
"doctrine/mongodb-odm": "^1.3 || ^2.1",
"doctrine/orm": "^2.5",
"doctrine/persistence": "^1.1 || ^2.0",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16.0",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.0",
"ramsey/uuid-doctrine": "^1.5.0",
"slevomat/coding-standard": "^4.5.2"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
"extension.neon",
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Doctrine extensions for PHPStan",
"time": "2020-06-14T11:03:59+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "0.12.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "a670a59aff7cf96f75d21b974860ada10e25b2ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a670a59aff7cf96f75d21b974860ada10e25b2ee",
"reference": "a670a59aff7cf96f75d21b974860ada10e25b2ee",
"shasum": ""
},
"require": {
"php": "~7.1",
"phpstan/phpstan": "^0.12.6"
},
"require-dev": {
"consistence/coding-standard": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16.0",
"phpstan/phpstan-phpunit": "^0.12",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.5.2"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"time": "2020-01-20T13:08:52+00:00"
},
{
"name": "phpstan/phpstan-symfony",
"version": "0.12.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "ba69dcd8e57c1a8580bf190e0554bea0fc37fe2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/ba69dcd8e57c1a8580bf190e0554bea0fc37fe2f",
"reference": "ba69dcd8e57c1a8580bf190e0554bea0fc37fe2f",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"php": "^7.1",
"phpstan/phpstan": "^0.12"
},
"conflict": {
"symfony/framework-bundle": "<3.0"
},
"require-dev": {
"consistence/coding-standard": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^1.0",
"phing/phing": "^2.16.0",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.5.2",
"squizlabs/php_codesniffer": "^3.3.2",
"symfony/console": "^4.0",
"symfony/framework-bundle": "^4.0",
"symfony/http-foundation": "^4.0",
"symfony/messenger": "^4.2",
"symfony/serializer": "^4.0"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
},
"phpstan": {
"includes": [
"extension.neon",
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Lukáš Unger",
"email": "looky.msc@gmail.com",
"homepage": "https://lookyman.net"
}
],
"description": "Symfony Framework extensions and rules for PHPStan",
"time": "2020-04-15T20:26:41+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "8.0.2",
@@ -11468,6 +11860,59 @@
],
"time": "2020-05-28T08:20:44+00:00"
},
{
"name": "thecodingmachine/phpstan-safe-rule",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/phpstan-safe-rule.git",
"reference": "ba333eb573167371309c8ae8fdab932991925e96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/ba333eb573167371309c8ae8fdab932991925e96",
"reference": "ba333eb573167371309c8ae8fdab932991925e96",
"shasum": ""
},
"require": {
"php": "^7.1",
"phpstan/phpstan": "^0.10 | ^0.11 | ^0.12",
"thecodingmachine/safe": "^1.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^7.5.2",
"squizlabs/php_codesniffer": "^3.4"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
},
"phpstan": {
"includes": [
"phpstan-safe-rule.neon"
]
}
},
"autoload": {
"psr-4": {
"TheCodingMachine\\Safe\\PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David Négrier",
"email": "d.negrier@thecodingmachine.com"
}
],
"description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe",
"time": "2020-01-02T14:59:39+00:00"
},
{
"name": "theofidry/psysh-bundle",
"version": "4.3.0",

13
phpstan.neon Normal file
View File

@@ -0,0 +1,13 @@
parameters:
level: 8
excludes_analyse:
- src/CheeperSpaghetti/*
- src/CheeperLayered/*
- tests/bootstrap.php
paths:
- src
symfony:
container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
console_application_loader: tests/console-application.php
typeAliases:
PostEvents: Architecture\CQRS\Domain\PostWasCreated|Architecture\CQRS\Domain\PostWasPublished|Architecture\CQRS\Domain\PostWasCategorized|Architecture\CQRS\Domain\PostContentWasChanged|Architecture\CQRS\Domain\PostTitleWasChanged

View File

@@ -1,41 +0,0 @@
<?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/"/>
<directory name="src/CheeperLayered/"/>
<file name="src/App/Kernel.php"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<PropertyNotSetInConstructor>
<errorLevel type="suppress">
<!-- Due to a bug in PSALM. See https://github.com/vimeo/psalm/issues/2319 -->
<file name="src/Architecture/CQRS/Infrastructure/Persistence/Doctrine/DoctrineFollowersRepository.php" />
</errorLevel>
</PropertyNotSetInConstructor>
</issueHandlers>
<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>

View File

@@ -4,14 +4,13 @@ declare(strict_types=1);
namespace App\API\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
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
final class AuthorDataPersister implements DataPersisterInterface
{
private CommandBus $commandBus;
@@ -21,13 +20,13 @@ final class AuthorDataPersister implements ContextAwareDataPersisterInterface
}
/** @param mixed $data */
public function supports($data, array $context = []): bool
public function supports($data): bool
{
return $data instanceof Author && null === $data->id;
}
/** @param Author $data */
public function persist($data, array $context = []): Author
/** @param mixed|Author $data */
public function persist($data): Author
{
$authorId = Uuid::uuid4();
@@ -49,8 +48,8 @@ final class AuthorDataPersister implements ContextAwareDataPersisterInterface
return $data;
}
/** @param Author $data */
public function remove($data, array $context = []): void
/** @param mixed|Author $data */
public function remove($data): void
{
// TODO: Implement remove() method.
}

View File

@@ -4,14 +4,12 @@ declare(strict_types=1);
namespace App\API\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
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
final class FollowerDataPersister implements DataPersisterInterface
{
private CommandBus $commandBus;
@@ -21,13 +19,13 @@ final class FollowerDataPersister implements ContextAwareDataPersisterInterface
}
/** @param mixed $data */
public function supports($data, array $context = []): bool
public function supports($data): bool
{
return $data instanceof Follower;
}
/** @param Follower $data */
public function persist($data, array $context = []): Follower
/** @param mixed|Follower $data */
public function persist($data): Follower
{
$this->commandBus->execute(
Follow::anAuthor(
@@ -39,8 +37,8 @@ final class FollowerDataPersister implements ContextAwareDataPersisterInterface
return $data;
}
/** @param Follower $data */
public function remove($data, array $context = []): void
/** @param mixed|Follower $data */
public function remove($data): void
{
// TODO: Implement remove() method.
}

View File

@@ -17,6 +17,7 @@ final class UuidResolver implements ArgumentValueResolverInterface
return UuidInterface::class === $argument->getType();
}
/** @return iterable<UuidInterface> */
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
yield Uuid::fromString(

View File

@@ -38,10 +38,6 @@ final class SignupCommand extends Command
$website = $input->getArgument('website');
$birthdate = $input->getArgument('birthdate');
if (!is_string($username) || !is_string($email) || !is_string($name) || !is_string($biography) || !is_string($location) || !is_string($website) || !is_string($birthdate)) {
return 2;
}
$command = new SignUp(
Uuid::uuid4()->toString(),
$username,

View File

@@ -3,52 +3,26 @@
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\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
public function registerBundles(): iterable
protected function configureContainer(ContainerConfigurator $container): void
{
$contents = require $this->getProjectDir().'/config/bundles.php';
foreach ($contents as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
yield new $class();
}
}
$container->import('../../config/{packages}/*.yaml');
$container->import('../../config/{packages}/'.$this->environment.'/*.yaml');
$container->import('../../config/{services}.yaml');
$container->import('../../config/{services}_'.$this->environment.'.yaml');
}
public function getProjectDir(): string
protected function configureRoutes(RoutingConfigurator $routes): void
{
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');
$routes->import('../../config/{routes}/'.$this->environment.'/*.yaml');
$routes->import('../../config/{routes}/*.yaml');
$routes->import('../../config/{routes}.yaml');
}
}

View File

@@ -17,12 +17,8 @@ final class CommandBus
$this->messageBus = $commandBus;
}
/**
* @template T
* @param AsyncCommand|SyncCommand|object $command
* @return T|Envelope The handler returned value
*/
public function execute($command)
/** @param AsyncCommand|SyncCommand|object $command */
public function execute($command): Envelope
{
if ($command instanceof SyncCommand) {
return $this->handle($command);

View File

@@ -2,12 +2,14 @@
namespace Architecture\CQRS\Domain;
/** @template T of DomainEvent */
//snippet aggregate-root
class AggregateRoot
{
/** @var DomainEvent[] */
/** @var T[] */
private array $recordedEvents = [];
/** @param T $event */
protected function recordApplyAndPublishThat(DomainEvent $event): void
{
$this->recordThat($event);
@@ -15,6 +17,7 @@ class AggregateRoot
$this->publishThat($event);
}
/** @param T $event */
protected function recordThat(DomainEvent $event): void
{
$this->recordedEvents[] = $event;
@@ -26,6 +29,7 @@ class AggregateRoot
$modifier = 'apply' . $className;
/** @phpstan-ignore-next-line */
$this->$modifier($event);
}
@@ -34,7 +38,7 @@ class AggregateRoot
DomainEventPublisher::instance()->publish($event);
}
/** @return DomainEvent[] */
/** @return T[] */
public function recordedEvents(): array
{
return $this->recordedEvents;

View File

@@ -15,7 +15,7 @@ class CategoryId
public static function create(): CategoryId
{
return new static(Uuid::uuid4()->toString());
return new self(Uuid::uuid4()->toString());
}
public function id(): string

View File

@@ -2,6 +2,7 @@
namespace Architecture\CQRS\Domain;
/** @extends AggregateRoot<PostEvents> */
//snippet post
class Post extends AggregateRoot
{
@@ -10,6 +11,7 @@ class Post extends AggregateRoot
private ?string $title = null;
private ?string $content = null;
private bool $published = false;
/** @var CategoryId[] */
private array $categories = [];
protected function __construct(PostId $id)
@@ -32,6 +34,7 @@ class Post extends AggregateRoot
return $this->content;
}
/** @return CategoryId[] */
public function categories(): array
{
return array_values($this->categories);
@@ -47,7 +50,7 @@ class Post extends AggregateRoot
{
$postId = PostId::create();
$post = new static($postId);
$post = new self($postId);
$post->recordApplyAndPublishThat(
new PostWasCreated($postId, $title, $content)

View File

@@ -15,7 +15,7 @@ class PostId
public static function create(): PostId
{
return new static(Uuid::uuid4()->toString());
return new self(Uuid::uuid4()->toString());
}
public function id(): string

View File

@@ -2,10 +2,14 @@
namespace Architecture\CQRS\Domain;
/**
* @template T of DomainEvent
*/
//snippet projection
interface Projection
{
public function listensTo(): string;
/** @param T $event */
public function project(DomainEvent $event): void;
}
//end-snippet

View File

@@ -15,10 +15,13 @@ interface PostRepository
{
public function save(Post $post): void;
public function byId(PostId $id): Post;
/** @return Post[] */
public function all(): array;
public function byCategory(CategoryId $categoryId): Post;
/** @return Post[] */
public function byTag(TagId $tagId): array;
public function withComments(PostId $id): Post;
/** @return Post[] */
public function groupedByMonth(): array;
// ...
}

View File

@@ -13,8 +13,10 @@ use Doctrine\ORM\EntityManager;
class DoctrinePostRepository implements PostRepository
{
private EntityManager $em;
/** @var Projector<PostEvents> */
private Projector $projector;
/** @param Projector<PostEvents> $projector */
public function __construct(EntityManager $em, Projector $projector)
{
$this->em = $em;
@@ -23,7 +25,7 @@ class DoctrinePostRepository implements PostRepository
public function save(Post $post): void
{
$this->em->transactional(function (EntityManager $em) use ($post) {
$this->em->transactional(static function(EntityManager $em) use ($post): void {
$em->persist($post);
foreach ($post->recordedEvents() as $event) {

View File

@@ -7,6 +7,7 @@ use Architecture\CQRS\Domain\PostContentWasChanged;
use Architecture\CQRS\Domain\Projection;
use Elasticsearch\Client;
/** @implements Projection<PostContentWasChanged> */
class PostContentWasChangedProjection implements Projection
{
private Client $client;
@@ -21,11 +22,11 @@ class PostContentWasChangedProjection implements Projection
return PostContentWasChanged::class;
}
/** @param PostContentWasChanged $event */
public function project(DomainEvent $event): void
{
/** @var PostContentWasChanged $event */
$id = $event->postId()->id();
$this->client->update([
'index' => 'posts',
'type' => 'post',

View File

@@ -7,6 +7,7 @@ use Architecture\CQRS\Domain\PostTitleWasChanged;
use Architecture\CQRS\Domain\Projection;
use Elasticsearch\Client;
/** @implements Projection<PostTitleWasChanged> */
class PostTitleWasChangedProjection implements Projection
{
private Client $client;
@@ -21,9 +22,9 @@ class PostTitleWasChangedProjection implements Projection
return PostTitleWasChanged::class;
}
/** @param PostTitleWasChanged $event */
public function project(DomainEvent $event): void
{
/** @var PostTitleWasChanged $event */
$id = $event->postId()->id();
$this->client->update([

View File

@@ -7,6 +7,7 @@ use Architecture\CQRS\Domain\PostWasCategorized;
use Architecture\CQRS\Domain\Projection;
use Elasticsearch\Client;
/** @implements Projection<PostWasCategorized> */
class PostWasCategorizedProjection implements Projection
{
private Client $client;
@@ -21,9 +22,9 @@ class PostWasCategorizedProjection implements Projection
return PostWasCategorized::class;
}
/** @param PostWasCategorized $event */
public function project(DomainEvent $event): void
{
/** @var PostWasCategorized $event */
$id = $event->postId()->id();
$this->client->update([

View File

@@ -7,6 +7,7 @@ use Architecture\CQRS\Domain\PostWasCreated;
use Architecture\CQRS\Domain\Projection;
use Elasticsearch\Client;
/** @implements Projection<PostWasCreated> */
//snippet elasticsearch-projection
class PostWasCreatedProjection implements Projection
{
@@ -22,9 +23,9 @@ class PostWasCreatedProjection implements Projection
return PostWasCreated::class;
}
/** @param PostWasCreated $event */
public function project(DomainEvent $event): void
{
/** @var PostWasCreated $event */
$id = $event->postId()->id();
$this->client->index([

View File

@@ -7,6 +7,7 @@ use Architecture\CQRS\Domain\PostWasPublished;
use Architecture\CQRS\Domain\Projection;
use Elasticsearch\Client;
/** @implements Projection<PostWasPublished> */
class PostWasPublishedProjection implements Projection
{
private Client $client;
@@ -21,9 +22,9 @@ class PostWasPublishedProjection implements Projection
return PostWasPublished::class;
}
/** @param PostWasPublished $event */
public function project(DomainEvent $event): void
{
/** @var PostWasPublished $event */
$id = $event->postId()->id();
$this->client->update([

View File

@@ -5,13 +5,14 @@ namespace Architecture\CQRS\Infrastructure\Projection;
use Architecture\CQRS\Domain\DomainEvent;
use Architecture\CQRS\Domain\Projection;
/** @template T of DomainEvent */
//snippet projector
class Projector
{
/** @var Projection[] */
/** @var Projection<T>[] */
private array $projections = [];
/** @param Projection[] $projections */
/** @param Projection<T>[] $projections */
public function register(array $projections): void
{
foreach ($projections as $projection) {
@@ -19,7 +20,7 @@ class Projector
}
}
/** @param DomainEvent[] $events */
/** @param T[] $events */
public function project(array $events): void
{
foreach ($events as $event) {

View File

@@ -5,6 +5,7 @@ namespace Architecture\CQRS\Presentation;
//snippet post-controller
class PostController
{
/** @return array{posts: array} */
public function listAction(): array
{
$client = \Elasticsearch\ClientBuilder::create()->build();

View File

@@ -5,15 +5,23 @@ namespace Architecture\ES\Domain;
use Architecture\CQRS\Domain\AggregateRoot;
use Architecture\CQRS\Domain\DomainEvent;
/**
* @template T of DomainEvent
* @extends AggregateRoot<T>
*/
//snippet event-sourced-aggregate-root
abstract class EventSourcedAggregateRoot extends AggregateRoot
{
/**
* @param EventStream<T> $events
* @return EventSourcedAggregateRoot<T>
*/
abstract public static function reconstitute(EventStream $events):
EventSourcedAggregateRoot;
/** @param EventStream<T> $history */
public function replay(EventStream $history): void
{
/** @var DomainEvent */
foreach ($history as $event) {
$this->applyThat($event);
}

View File

@@ -4,15 +4,19 @@ namespace Architecture\ES\Domain;
use Architecture\CQRS\Domain\DomainEvent;
/**
* @template T of DomainEvent
* @implements \Iterator<T>
*/
class EventStream implements \Iterator
{
private string $aggregateId;
/** @var DomainEvent[] */
/** @var T[] */
private array $events;
/**
* @param string $aggregateId
* @param DomainEvent[] $events
* @param T[] $events
*/
public function __construct(string $aggregateId, array $events)
{
@@ -25,11 +29,12 @@ class EventStream implements \Iterator
return $this->aggregateId;
}
public function rewind()
public function rewind(): void
{
reset($this->events);
}
/** @return T */
public function current()
{
return current($this->events);
@@ -40,12 +45,12 @@ class EventStream implements \Iterator
return key($this->events);
}
public function next()
public function next(): void
{
next($this->events);
}
public function valid()
public function valid(): bool
{
return key($this->events) !== null;
}

View File

@@ -3,6 +3,7 @@
namespace Architecture\ES\Domain;
use Architecture\CQRS\Domain\CategoryId;
use Architecture\CQRS\Domain\DomainEvent;
use Architecture\CQRS\Domain\PostContentWasChanged;
use Architecture\CQRS\Domain\PostId;
use Architecture\CQRS\Domain\PostTitleWasChanged;
@@ -10,6 +11,7 @@ use Architecture\CQRS\Domain\PostWasCategorized;
use Architecture\CQRS\Domain\PostWasCreated;
use Architecture\CQRS\Domain\PostWasPublished;
/** @extends EventSourcedAggregateRoot<PostEvents> */
//snippet post
class Post extends EventSourcedAggregateRoot
{
@@ -18,6 +20,7 @@ class Post extends EventSourcedAggregateRoot
private ?string $title = null;
private ?string $content = null;
private bool $published = false;
/** @var CategoryId[] */
private array $categories = [];
protected function __construct(PostId $id)
@@ -29,7 +32,7 @@ class Post extends EventSourcedAggregateRoot
{
$postId = PostId::create();
$post = new static($postId);
$post = new self($postId);
$post->recordApplyAndPublishThat(
new PostWasCreated($postId, $title, $content)
@@ -81,6 +84,7 @@ class Post extends EventSourcedAggregateRoot
return $this->content;
}
/** @return CategoryId[] */
public function categories(): array
{
return array_values($this->categories);
@@ -119,10 +123,11 @@ class Post extends EventSourcedAggregateRoot
}
//end-ignore
/** @return self */
public static function reconstitute(EventStream $history):
EventSourcedAggregateRoot
{
$post = new static(new PostId($history->getAggregateId()));
$post = new self(new PostId($history->getAggregateId()));
$post->replay($history);

View File

@@ -7,55 +7,65 @@ use Architecture\CQRS\Domain\DomainEvent;
use Architecture\ES\Domain\EventStream;
use Predis\Client;
use Zumba\JsonSerializer\JsonSerializer;
use Safe\DateTimeImmutable;
/**
* @template T of DomainEvent
*/
//snippet event-store
class EventStore
{
/** @var Client<?string> */
private Client $redis;
private JsonSerializer $serializer;
/** @param Client<?string> $redis */
public function __construct(Client $redis, JsonSerializer $serializer)
{
$this->redis = $redis;
$this->serializer = $serializer;
}
/** @param EventStream<T> $eventstream */
public function append(EventStream $eventstream): void
{
/** @var DomainEvent */
/** @var DomainEvent $event */
foreach ($eventstream as $event) {
$data = $this->serializer->serialize($event);
$date = (new \DateTimeImmutable())->format('YmdHis');
$date = (new DateTimeImmutable())->format('YmdHis');
$this->redis->rpush(
'events:' . $eventstream->getAggregateId(),
$this->serializer->serialize([
'type' => get_class($event),
'created_on' => $date,
'data' => $data
])
[
$this->serializer->serialize([
'type' => get_class($event),
'created_on' => $date,
'data' => $data
])
]
);
}
}
/** @return EventStream<T> */
public function getEventsFor(string $id): EventStream
{
return $this->fromVersion($id, 0);
}
/** @return EventStream<T> */
public function fromVersion(string $id, int $version): EventStream
{
$serializedEvents = (array) $this->redis->lrange(
$serializedEvents = $this->redis->lrange(
'events:' . $id,
$version,
-1
);
/** @var DomainEvent[] */
$events = [];
/** @var string */
/** @var string $serializedEvent */
foreach ($serializedEvents as $serializedEvent) {
$event = (array) $this->serializer->unserialize($serializedEvent);
@@ -70,7 +80,7 @@ class EventStore
public function countEventsFor(string $id): int
{
return (int) $this->redis->llen('events:' . $id);
return $this->redis->llen('events:' . $id);
}
}
//end-snippet

View File

@@ -4,17 +4,21 @@ namespace Architecture\ES\Infrastructure;
use Architecture\CQRS\Domain\AggregateRoot;
/** @template T of AggregateRoot */
class Snapshot
{
/** @phpstan-var T */
private AggregateRoot $aggregate;
private int $version;
/** @phpstan-param T $aggregate */
public function __construct(AggregateRoot $aggregate, int $version)
{
$this->aggregate = $aggregate;
$this->version = $version;
}
/** @return T */
public function aggregate(): AggregateRoot
{
return $this->aggregate;

View File

@@ -7,18 +7,24 @@ use Architecture\CQRS\Domain\AggregateRoot;
use Predis\Client;
use Zumba\JsonSerializer\JsonSerializer;
/**
* @template T of AggregateRoot
*/
//snippet snapshot-repository
class SnapshotRepository
{
/** @var Client<?string> */
private Client $redis;
private JsonSerializer $serializer;
/** @param Client<?string> $redis */
public function __construct(Client $redis, JsonSerializer $serializer)
{
$this->redis = $redis;
$this->serializer = $serializer;
}
/** @phpstan-return Snapshot<T>|null */
public function byId(string $id): ?Snapshot
{
$key = 'snapshots:' . $id;
@@ -31,10 +37,9 @@ class SnapshotRepository
}
$metadata = (array) $this->serializer->unserialize($data);
$snapshot = (array) $metadata['snapshot'];
/** @var AggregateRoot */
$aggregate = $this->serializer->unserialize(
(string) $snapshot['data']
);
@@ -45,24 +50,25 @@ class SnapshotRepository
);
}
/** @phpstan-param Snapshot<T> $snapshot */
public function save(string $id, Snapshot $snapshot): void
{
$key = 'snapshots:' . $id;
$aggregate = $snapshot->aggregate();
$snapshot = [
'version' => $snapshot->version(),
'snapshot' => [
'type' => get_class($aggregate),
'data' => $this->serializer->serialize(
$aggregate
)
]
];
$this->redis->set(
$key,
$this->serializer->serialize($snapshot)
$this->serializer->serialize(
[
'version' => $snapshot->version(),
'snapshot' => [
'type' => get_class($aggregate),
'data' => $this->serializer->serialize(
$aggregate
)
]
]
)
);
}

View File

@@ -12,9 +12,15 @@ use Architecture\ES\Infrastructure\EventStore;
//snippet event-store-post-repository
class EventStorePostRepository implements PostRepository
{
/** @var EventStore<PostEvents> */
private EventStore $eventStore;
/** @var Projector<PostEvents> */
private Projector $projector;
/**
* @param EventStore<PostEvents> $eventStore
* @param Projector<PostEvents> $projector
*/
public function __construct(EventStore $eventStore, Projector $projector)
{
$this->eventStore = $eventStore;
@@ -33,7 +39,6 @@ class EventStorePostRepository implements PostRepository
public function byId(PostId $id): Post
{
/** @var Post */
return Post::reconstitute(
$this->eventStore->getEventsFor($id->id())
);

View File

@@ -13,10 +13,18 @@ use Architecture\ES\Infrastructure\SnapshotRepository;
class EventStorePostRepository implements PostRepository
{
private SnapshotRepository $snapshotRepository;
private EventStore $eventStore;
/** @var Projector<PostEvents> */
private Projector $projector;
/** @var EventStore<PostEvents> */
private EventStore $eventStore;
/** @var SnapshotRepository<Post> */
private SnapshotRepository $snapshotRepository;
/**
* @param SnapshotRepository<Post> $snapshotRepository
* @param EventStore<PostEvents> $eventStore
* @param Projector<PostEvents> $projector
*/
public function __construct(
SnapshotRepository $snapshotRepository,
EventStore $eventStore,

View File

@@ -35,8 +35,8 @@ final class SignUpHandler
$authorId = AuthorId::fromString($command->authorId());
$email = new EmailAddress($command->email());
$website = $command->website() ? new Website($command->website()) : null;
$birthDate = $command->birthDate() ? new BirthDate($command->birthDate()) : null;
$website = null !== $command->website() ? new Website($command->website()) : null;
$birthDate = null !== $command->birthDate() ? new BirthDate($command->birthDate()) : null;
$this->authors->save(
Author::signUp(

View File

@@ -14,12 +14,13 @@ final class PostCheep
private string $authorId;
private string $message;
/** @param array{author_id: string, cheep_id: string, message: string} $array */
public static function fromArray(array $array): self
{
return new static(
(string) $array['author_id'],
(string) $array['cheep_id'],
(string) $array['message'],
$array['author_id'],
$array['cheep_id'],
$array['message'],
);
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Author;
use Assert\Assertion;
use function Functional\filter;
class Author
@@ -65,7 +64,7 @@ class Author
private function setName(?string $name): void
{
if ($name !== null && empty($name)) {
if (null !== $name && '' === $name) {
throw new \InvalidArgumentException('Name cannot be empty');
}
@@ -74,7 +73,7 @@ class Author
private function setBiography(?string $biography): void
{
if ($biography !== null && empty($biography)) {
if ($biography !== null && '' === $biography) {
throw new \InvalidArgumentException('Biography cannot be empty');
}
@@ -83,7 +82,7 @@ class Author
private function setLocation(?string $location): void
{
if ($location !== null && empty($location)) {
if ($location !== null && '' === $location) {
throw new \InvalidArgumentException('Location cannot be empty');
}
@@ -146,6 +145,7 @@ class Author
$this->following[] = $followed;
}
/** @return AuthorId[] */
public function following(): array
{
return $this->following;

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Author;
use RuntimeException;
use function Safe\sprintf;
final class AuthorAlreadyExists extends RuntimeException
{

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Author;
use RuntimeException;
use function Safe\sprintf;
final class AuthorDoesNotExist extends RuntimeException
{

View File

@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Author;
use Assert\Assertion;
use Cheeper\DomainModel\Common\ValueObject;
use DateTimeImmutable;
use DateTimeInterface;
use Safe\DateTimeImmutable;
use Safe\Exceptions\DatetimeException;
final class BirthDate extends ValueObject
{
private DateTimeImmutable $date;
private DateTimeInterface $date;
public function __construct(string $date)
{
@@ -24,12 +25,10 @@ final class BirthDate extends ValueObject
private function setDate(string $date): void
{
$date = DateTimeImmutable::createFromFormat('Y-m-d', $date);
if (!$date) {
throw new \InvalidArgumentException("'$date' is not a valid datetime (Y-m-d formatted).");
try {
$this->date = DateTimeImmutable::createFromFormat('Y-m-d', $date);
} catch (DatetimeException $exception) {
throw new \InvalidArgumentException("'$date' is not a valid datetime (Y-m-d formatted).", 0, $exception);
}
$this->date = $date;
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Author;
use Cheeper\DomainModel\Common\ValueObject;
use function Safe\sprintf;
final class EmailAddress extends ValueObject
{

View File

@@ -40,7 +40,7 @@ final class UserName extends ValueObject
private function assertNotEmpty(string $userName): void
{
if (empty($userName)) {
if ('' === $userName) {
throw new \InvalidArgumentException("Username cannot be empty");
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Cheep;
use Cheeper\DomainModel\Author\AuthorId;
use Safe\DateTimeImmutable;
class Cheep
{
@@ -15,12 +16,12 @@ class Cheep
public static function compose(AuthorId $authorId, CheepId $cheepId, CheepMessage $cheepMessage): self
{
return new static(
return new self(
$authorId,
$cheepId,
$cheepMessage,
new CheepDate(
(new \DateTimeImmutable('now'))->format('Y-m-d')
(new DateTimeImmutable('now'))->format('Y-m-d')
)
);
}

View File

@@ -4,9 +4,10 @@ declare(strict_types=1);
namespace Cheeper\DomainModel\Cheep;
use Assert\Assertion;
use Cheeper\DomainModel\Common\ValueObject;
use DateTimeImmutable;
use InvalidArgumentException;
use Safe\DateTimeImmutable;
use Safe\Exceptions\DatetimeException;
final class CheepDate extends ValueObject
{
@@ -24,12 +25,10 @@ final class CheepDate extends ValueObject
private function setDate(string $date): void
{
$date = DateTimeImmutable::createFromFormat('Y-m-d', $date);
if (!$date) {
throw new \InvalidArgumentException("'$date' is not a valid datetime (Y-m-d formatted).");
try {
$this->date = DateTimeImmutable::createFromFormat('Y-m-d', $date);
} catch (DatetimeException $exception) {
throw new InvalidArgumentException("'$date' is not a valid datetime (Y-m-d formatted).");
}
$this->date = $date;
}
}

View File

@@ -12,7 +12,7 @@ abstract class UuidBasedIdentity extends ValueObject
protected string $id;
protected string $idAsString;
private function __construct(string $id)
final private function __construct(string $id)
{
$this->id = $this->idAsString = $id;
}

View File

@@ -32,7 +32,7 @@ final class DoctrineOrmAuthors implements Authors
'authorId.id' => Uuid::fromString($authorId->id())
]);
if (!$author) {
if (null === $author) {
return null;
}
@@ -49,7 +49,7 @@ final class DoctrineOrmAuthors implements Authors
->getRepository(Author::class)
->findOneBy(['userName.userName' => $userName->userName()]);
if (!$author) {
if (null === $author) {
return null;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CheeperCommandBus\Simplest;
use Cheeper\Application\Command\Cheep\PostCheep;
use Cheeper\Application\Command\Cheep\PostCheepHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class SignUpController extends AbstractController
{
private PostCheepHandler $postCheepHandler;
public function __invoke(Request $request): Response
{
// $command = PostCheep::fromArray(['test' => 'test']);
return new Response();
}
}

View File

@@ -50,6 +50,7 @@ final class SignUpController extends AbstractController
//end-ignore
}
/** @param ConstraintViolationListInterface<ConstraintViolationInterface> $errors */
private function toJson(ConstraintViolationListInterface $errors): string
{
$json = [];

View File

@@ -4,6 +4,7 @@ namespace CheeperHexagonal;
use CheeperLayered\Authors;
use CheeperLayered\Author;
use function Safe\sprintf;
//snippet author-service
class AuthorService
@@ -22,10 +23,12 @@ class AuthorService
?string $bio
): Author
{
if (!$author = $this->authors->byId($id)) {
$author = $this->authors->byId($id);
if (null === $author) {
throw new \RuntimeException(sprintf('%s author not found', $username));
}
$author->setUsername($username);
$author->setWebsite($website);
$author->setBio($bio);

View File

@@ -5,6 +5,7 @@ namespace CheeperHexagonal;
use CheeperLayered\Authors;
use CheeperLayered\Cheeps;
use CheeperLayered\Cheep;
use function Safe\sprintf;
//snippet cheep-service
class CheepService
@@ -20,10 +21,12 @@ class CheepService
public function postCheep(string $username, string $message): Cheep
{
if (!$author = $this->authors->byUsername($username)) {
$author = $this->authors->byUsername($username);
if (null === $author) {
throw new \RuntimeException(sprintf('%s username not found', $username));
}
$cheep = $author->compose($message);
$this->cheeps->add($cheep);

View File

@@ -10,7 +10,7 @@ class Post
public static function writeNewFrom(string $title, string $content): self
{
return new static($title, $content);
return new self($title, $content);
}
private function __construct(string $title, string $content)
@@ -21,7 +21,7 @@ class Post
private function setTitle(string $title): void
{
if (empty($title)) {
if ('' === $title) {
throw new \RuntimeException('Title cannot be empty');
}
@@ -30,7 +30,7 @@ class Post
private function setContent(string $content): void
{
if (empty($content)) {
if ('' === $content) {
throw new \RuntimeException('Content cannot be empty');
}

View File

@@ -59,4 +59,4 @@ SQL, mysqli_real_escape_string($link, $timeline_username)));
</body>
</html>
<?php mysqli_close($link); ?>
<!--end-snippet-->
<!--end-snippet-->

View File

@@ -260,6 +260,27 @@
"phpspec/prophecy": {
"version": "v1.10.2"
},
"phpstan/extension-installer": {
"version": "1.0.4"
},
"phpstan/phpstan": {
"version": "0.12.30"
},
"phpstan/phpstan-beberlei-assert": {
"version": "0.12.2"
},
"phpstan/phpstan-deprecation-rules": {
"version": "0.12.4"
},
"phpstan/phpstan-doctrine": {
"version": "0.12.16"
},
"phpstan/phpstan-strict-rules": {
"version": "0.12.2"
},
"phpstan/phpstan-symfony": {
"version": "0.12.6"
},
"phpunit/php-code-coverage": {
"version": "8.0.1"
},
@@ -713,6 +734,9 @@
"symfony/yaml": {
"version": "v5.0.4"
},
"thecodingmachine/phpstan-safe-rule": {
"version": "v1.0.0"
},
"thecodingmachine/safe": {
"version": "v1.1"
},

View File

@@ -2,6 +2,7 @@
namespace Architecture\ES\Domain;
use Cheeper\DomainModel\DomainEvent;
use PHPUnit\Framework\TestCase;
class EventStreamTest extends TestCase
@@ -11,7 +12,13 @@ class EventStreamTest extends TestCase
*/
public function itShouldBuildAnEventStream(): void
{
$stream = new EventStream('irrelevant', [1, 2, 3]);
$domainEvents = [
new class implements DomainEvent{},
new class implements DomainEvent{},
new class implements DomainEvent{},
];
$stream = new EventStream('irrelevant', $domainEvents);
$collected = [];
foreach ($stream as $key => $event) {
@@ -19,6 +26,6 @@ class EventStreamTest extends TestCase
}
$this->assertEquals('irrelevant', $stream->getAggregateId());
$this->assertEquals($collected, [1, 2, 3]);
$this->assertEquals($collected, $domainEvents);
}
}

View File

@@ -24,27 +24,27 @@ final class PostCheepHandlerTest extends TestCase
}
/** @test */
public function throwsExceptionWhenAuthorIdIsNotString(): void
public function throwsExceptionWhenAuthorIdIsNotUuid(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->postNewCheep(1, Uuid::uuid4()->toString(), 'test');
$this->postNewCheep("1", Uuid::uuid4()->toString(), 'test');
}
/** @test */
public function throwsExceptionWhenCheepIdIsNotString(): void
public function throwsExceptionWhenCheepIdIsNotUuid(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->postNewCheep(Uuid::uuid4()->toString(), 1, 'test');
$this->postNewCheep(Uuid::uuid4()->toString(), "1", 'test');
}
/** @test */
public function throwsExceptionWhenCheepMessageIsNotString(): void
public function throwsExceptionWhenCheepMessageIsEmpty(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->postNewCheep(Uuid::uuid4()->toString(), Uuid::uuid4()->toString(), 0);
$this->postNewCheep(Uuid::uuid4()->toString(), Uuid::uuid4()->toString(), "");
}
/** @test */

View File

@@ -3,17 +3,19 @@
namespace CheeperLayered;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class TemplateTest extends TestCase
{
private static $TEMPLATES_PATH = 'src/CheeperLayered/templates';
private static string $TEMPLATES_PATH = __DIR__ . '/../../src/CheeperLayered/templates';
private $twig;
private Environment $twig;
public function setUp(): void
{
$loader = new \Twig\Loader\FilesystemLoader(self::$TEMPLATES_PATH);
$this->twig = new \Twig\Environment($loader);
$loader = new FilesystemLoader(self::$TEMPLATES_PATH);
$this->twig = new Environment($loader);
}
/**

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
require dirname(__DIR__). '/config/bootstrap.php';
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
return new Application($kernel);