diff --git a/.codecov.yml b/.codecov.yml
index 1acfb8df..73cad4f5 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -5,4 +5,5 @@ ignore:
- "src/*/Event/*Event.php"
- "src/*/Event/Domain/*Event.php"
- "src/*/Infra/Doctrine/Type/*Type.php"
+ - "src/*Bundle/Resources/"
- "src/Domain/Infra/DependencyInjection/*Helper.php"
diff --git a/.env.dist b/.env.dist
index a68e64a6..3b926cd6 100644
--- a/.env.dist
+++ b/.env.dist
@@ -1 +1 @@
-GITHUB_TOKEN=""
+GITHUB_TOKEN=
diff --git a/.lando.yml b/.lando.yml
new file mode 100644
index 00000000..6d4aaa42
--- /dev/null
+++ b/.lando.yml
@@ -0,0 +1,24 @@
+name: msgphp-ci
+recipe: custom
+
+services:
+ appserver:
+ type: php:7.1
+
+tooling:
+ php:
+ service: appserver
+ description: Run PHP commands
+ cmd:
+ - php
+ composer:
+ service: appserver
+ description: Run Composer commands
+ cmd:
+ - composer
+ bash:
+ service: appserver
+ description: Run bash commands
+ cmd:
+ - bash
+ - -xc
diff --git a/.php_cs.dist b/.php_cs.dist
index cd622ee6..1d5961da 100644
--- a/.php_cs.dist
+++ b/.php_cs.dist
@@ -14,6 +14,7 @@ $rules = [
'@PHPUnit60Migration:risky' => true,
'single_import_per_statement' => false,
'array_syntax' => ['syntax' => 'short'],
+ 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
];
if (!is_dir($cacheDir)) {
diff --git a/.travis.yml b/.travis.yml
index db1a1230..142b5952 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,73 +1,54 @@
+dist: trusty
+sudo: false
+language: php
+
notifications:
email: false
-language: php
-
-php:
- - 7.1
- - 7.2
- - nightly
+branches:
+ only:
+ - master
+ - /^\d\.\d$/
-env:
- global:
- - COMPOSER_ARGS="--no-progress --prefer-dist --no-interaction --no-suggest"
- - PHPUNIT_ARGS=""
+php: 7.2
-matrix:
+jobs:
include:
+ - stage: Unit-tests
+ env: CC=true
+ - env: DEPS=low
- php: 7.1
- env:
- - DEPS=low
- - php: 7.1
- env:
- - DEPS=high
- - CC=true
- - CS=true
- - SA=true
- - php: 7.2
- env:
- - DEPS=low
- - php: 7.2
- env:
- - DEPS=high
-
-allow_failures:
-- php:
- - nightly
-
-fast_finish: true
-
-before_script:
- - if [[ $CC != true ]] ; then phpenv config-rm xdebug.ini || true ; fi
- - if [[ $CS = true ]] ; then mkdir -p var/cache ; fi
- - travis_retry composer self-update
+ - stage: Code-quality
+ install: true
+ script:
+ - bin/cs
+ - bin/sa
before_install:
+ - if [[ $CC = true && $TRAVIS_PULL_REQUEST != false ]] ; then unset CC ; fi
+ - if [[ $CC != true ]] ; then phpenv config-rm xdebug.ini || true ; fi
- source .travis/tfold
install:
- - if [[ $DEPS != high || $TRAVIS_BRANCH != master ]] ; then travis_retry php .travis/packager.php HEAD^ $(find src/*/composer.json -type f -printf '%h\n') ; fi
- - if [[ ! $DEPS ]] ; then travis_retry bin/composer install $COMPOSER_ARGS ; fi
- - if [[ $DEPS = high ]] ; then travis_retry bin/composer update $COMPOSER_ARGS --prefer-stable ; fi
- - if [[ $DEPS = low ]] ; then travis_retry bin/composer update $COMPOSER_ARGS --prefer-stable --prefer-lowest ; fi
- - bin/phpunit install
- - if [[ $CS = true || $SA = true ]] ; then composer install $COMPOSER_ARGS ; fi
+ - |
+ COMPOSER_ARGS="--no-progress --prefer-dist --no-interaction --no-suggest"
+ PHPUNIT_ARGS=""
+ if [[ $DEPS = low ]] ; then COMPOSER_ARGS="${COMPOSER_ARGS} --prefer-stable --prefer-lowest" ; fi
+ if [[ $CC = true ]] ; then PHPUNIT_ARGS="${PHPUNIT_ASRGS} --coverage-clover=coverage.xml" ; fi
+ - travis_retry .travis/packager HEAD^ $(find src/*/composer.json -type f -printf '%h\n')
+ - travis_retry bin/composer update $COMPOSER_ARGS
+ - travis_retry bin/phpunit install
script:
- - if [[ ! $CC ]] ; then bin/phpunit $PHPUNIT_ARGS ; fi
- - if [[ $CC = true ]] ; then bin/phpunit --coverage-clover=coverage.xml $PHPUNIT_ARGS ; fi
- - if [[ $CS = true ]] ; then bin/cs ; fi
- - if [[ $SA = true ]] ; then bin/sa ; fi
+ - bin/phpunit $PHPUNIT_ARGS
after_success:
- - if [[ $CC = true ]] ; then .travis/codecov ; fi
+ - if [[ $CC = true ]] ; then bash <(curl -s https://codecov.io/bash) ; fi
+
+before_cache:
+ - rm -rf $HOME/.composer/cache/files/msgphp
cache:
directories:
- - $HOME/.composer/cache/files/
- - var/
-
-branches:
- only:
- - master
- - /^\d\.\d$/
+ - $HOME/.composer/cache
+ - var/cache/
diff --git a/.travis/codecov b/.travis/codecov
deleted file mode 100755
index 2c5832ec..00000000
--- a/.travis/codecov
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-for DIR in $(find src/ -maxdepth 1 -mindepth 1 -type d) ; do
- pushd $DIR &> /dev/null
- if [[ -f coverage.xml ]] ; then bash <(curl -s https://codecov.io/bash) ; fi
- popd &> /dev/null
-done
diff --git a/.travis/packager.php b/.travis/packager
similarity index 99%
rename from .travis/packager.php
rename to .travis/packager
index f7a6f469..7bdc7568 100755
--- a/.travis/packager.php
+++ b/.travis/packager
@@ -1,3 +1,4 @@
+#!/usr/bin/env php
'
+```
diff --git a/README.md b/README.md
index 92c69c59..9889fcfe 100644
--- a/README.md
+++ b/README.md
@@ -19,8 +19,14 @@ Domain | Bundle
[`msgphp/eav`](https://github.com/msgphp/eav) | [`msgphp/eav-bundle`](https://github.com/msgphp/eav-bundle)
[`msgphp/user`](https://github.com/msgphp/user) | [`msgphp/user-bundle`](https://github.com/msgphp/user-bundle)
+## Contributing
+
+See the [contributing](CONTRIBUTING.md) section.
+
## Blog posts
+- [Domain-driven-design: Projections in practice with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-projections-in-practice-with-api-platform-and-elasticsearch-c785ed6d660b)
+- [Adding user management to your Symfony application](https://medium.com/@ro0NL/adding-user-management-to-your-symfony-application-ceeefe2a2e9)
- [Domain-driven-design: Moving forward with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-moving-forward-with-api-platform-and-elasticsearch-f1705614f9e2)
- [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374)
- [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f)
diff --git a/UPGRADE-0.3.md b/UPGRADE-0.3.md
new file mode 100644
index 00000000..c857e42e
--- /dev/null
+++ b/UPGRADE-0.3.md
@@ -0,0 +1,14 @@
+# UPGRADE FROM 0.2 to 0.3
+
+## Domain
+
+- Added `%msgphp.domain.class_mapping%` DI parameter to inject the full domain class mapping
+
+## User
+
+- Enabled role `ROLE_DEFAULT` for users by default if no `UserRolesProviderInterface` is registered
+
+## UserBundle
+
+- Added `make:user` when [`MakerBundle`](https://github.com/symfony/maker-bundle) is enabled. Consider (re-)generating your application code.
+
diff --git a/UPGRADE-0.4.md b/UPGRADE-0.4.md
new file mode 100644
index 00000000..9f851da2
--- /dev/null
+++ b/UPGRADE-0.4.md
@@ -0,0 +1,36 @@
+# UPGRADE FROM 0.3 to 0.4
+
+## Eav
+
+- Refactored `Entity\Fields\AttributeValueField` into `Entity\Features\EntityAttributeValue`
+- Added `Infra\Doctrine\Repository\EntityAttributeValueRepositoryTrait`
+- Added default domain messages (`Command\` and `Event\`)
+
+## User
+
+- Renamed `UserAttributeValue::getAttributeValueId()` to `getId()`
+- Added `UserAttributeValue::changeValue()`
+- Deleted `UserAttributeValue::getAttributeValue()`
+- Renamed `ChangeUserAttributeValueCommand::$attributeValueId` to `$id`
+- Renamed `DeleteUserAttributeValueCommand::$attributeValueId` to `$id`
+
+## UserBundle
+
+- Added support for Symfony Messenger and is favored over SimpleBus by default
+- Renamed default Twig variables
+
+ Before:
+
+ ```
+ {# Nullable #}
+ {{ msgphp_user.user }}
+ {{ msgphp_user.userId }}
+ ```
+
+ After:
+
+ ```
+ {# Not nullable #}
+ {{ msgphp_user.current }}
+ {{ msgphp_user.id }}
+ ```
diff --git a/UPGRADE-0.5.md b/UPGRADE-0.5.md
new file mode 100644
index 00000000..5e4a6c3d
--- /dev/null
+++ b/UPGRADE-0.5.md
@@ -0,0 +1,19 @@
+# UPGRADE FROM 0.4 to 0.5
+
+## Domain
+
+- Renamed `DomainProjectionInterface` to `ProjectionInterface`
+- Renamed `DomainProjectionDocument` to `ProjectionDocument`
+- Renamed `DomainProjectionDocumentProvider` to `ProjectionDocumentProvider`
+- Renamed `DomainProjectionDocumentTransformerInterface` to `ProjectionDocumentTransformerInterface`
+- Updated `DomainProjectionRepositoryInterface::findAll(): DomainProjectionInterface[]` to `findAll(): ProjectionDocument[]`
+- Updated `DomainProjectionRepositoryInterface::find(): ?DomainProjectionInterface` to `find(): ?ProjectionDocument`
+- Renamed `DomainProjectionRepositoryInterface` to `ProjectionRepositoryInterface`
+- Renamed `DomainProjectionSynchronization` to `ProjectionSynchronization`
+- Renamed `DomainProjectionTypeRegistryInterface` to `ProjectionTypeRegistryInterface`
+- Added default projection messages (`Command\` and `Event\`)
+
+## UserBundle
+
+- Updated `make:user` to generate code using Symfony Messenger instead of SimpleBus
+- Disabled legacy credentials in `make:user` (`Email/NicknameSaltedPassword`)
diff --git a/UPGRADE-0.6.md b/UPGRADE-0.6.md
new file mode 100644
index 00000000..93eaaf52
--- /dev/null
+++ b/UPGRADE-0.6.md
@@ -0,0 +1,34 @@
+# UPGRADE FROM 0.5 to 0.6
+
+## Domain
+
+- Updated `ProjectionRepositoryInterface::findAll(): iterable` to `findAll(): PaginatedDomainCollectionInterface`
+- Added `PaginatedDomainCollection`
+- Added `DomainCollectionFactory::createFromCallable()`
+
+## Eav
+
+- Marked `AttributeValue::getChecksum()` static, requiring `$value` as 1st argument
+- Finalized `AttributeValue::get/changeValue()`
+
+## User
+
+- Added `SecurityUser::getUserId(): UserIdInterface`
+
+## UserBundle
+
+- Renamed default Twig variable
+
+ Before:
+
+ ```
+ {{ msgphp_user.id }}
+ ```
+
+ After:
+
+ ```
+ {{ msgphp_user.currentId }}
+ ```
+
+- Changed default value for `msgphp_user.username_lookup.N.mapped_by` to `null`
diff --git a/UPGRADE-0.7.md b/UPGRADE-0.7.md
new file mode 100644
index 00000000..e63c7a70
--- /dev/null
+++ b/UPGRADE-0.7.md
@@ -0,0 +1,45 @@
+# UPGRADE FROM 0.6 to 0.7
+
+## User
+
+- Marked `Username` abstract
+- Renamed CLI command `user:synchronize:usernames` to `user:synchronize-usernames`
+- Replaced `MsgPhp\User\Infra\Security\UserRolesProviderInterface` by `MsgPhp\User\Role\RoleProviderInterface`
+
+## UserBundle
+
+- Replaced generated `App\Security\UserRolesProvider` with config:
+
+ ```yaml
+ msgphp_user:
+ # ...
+ role_providers:
+ #- default: [ROLE_USER]
+ - MsgPhp\User\Role\UserRoleProvider
+ ```
+
+- Create and configure the `Username` entity in case `msgphp_php.username_lookup` is configured
+
+ ```yaml
+ msgphp_user:
+ class_mapping:
+ MsgPhp\User\Entity\Username: App\Entity\User\Username
+ username_lookup:
+ - # ...
+ ```
+
+ ```php
+ /dev/null 2>&1
+if [[ $? -eq 0 ]] ; then
+ lando bash "bin/sami update .sami.dist"
+else
+ bin/sami update .sami.dist
+fi
bin/mkdocs build
[[ $? -ne 0 ]] && exit 1
diff --git a/bin/compile-docrefs b/bin/compile-docrefs
new file mode 100755
index 00000000..24811da8
--- /dev/null
+++ b/bin/compile-docrefs
@@ -0,0 +1,106 @@
+#!/usr/bin/env php
+ [], 'bundles' => []];
+ foreach (glob('src/*/composer.json') as $file) {
+ $package = json_decode(file_get_contents($file));
+ $entry = ['path' => dirname($file), 'package' => $package];
+
+ if ('-bundle' === substr($package->name, -7)) {
+ $result['bundles'][$package->name] = $entry;
+ continue;
+ }
+
+ $result['domains'][$package->name] = $entry + [
+ 'entities' => array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Entity/*.php')),
+ 'entity_fields' => array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Entity/Fields/*Field.php')),
+ 'entity_features' => array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, array_filter(glob(dirname($file).'/Entity/Features/*.php'), function (string $file): bool {
+ return 'Abstract' !== substr(basename($file), 0, 8);
+ })),
+ 'identifiers' => array_map(function (string $file): array {
+ return [
+ 'class' => $class = 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4)),
+ 'scalar' => class_exists($scalar = substr($class, 0, -9)) ? $scalar : null,
+ 'uuid' => class_exists($uuid = 'MsgPhp\\'.basename(dirname($file)).'\\Infra\\Uuid\\'.basename($file, 'Interface.php')) ? $uuid : null,
+ ];
+ }, glob(dirname($file).'/*IdInterface.php')),
+ 'repositories' => array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Repository/*RepositoryInterface.php')),
+ 'commands' => array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Command/*Command.php')),
+ 'events' => array_filter(array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Event/*Event.php')), function (string $class): bool {
+ return !is_subclass_of($class, DomainEventInterface::class);
+ }),
+ 'domain_events' => array_filter(array_map(function (string $file): string {
+ return 'MsgPhp\\'.str_replace('/', '\\', substr($file, 4, -4));
+ }, glob(dirname($file).'/Event/{*,Domain/*}Event.php', \GLOB_BRACE)), function (string $class): bool {
+ return is_subclass_of($class, DomainEventInterface::class);
+ }),
+ ];
+ }
+
+ ksort($result['domains']);
+ ksort($result['bundles']);
+
+ $result['domains'] = [$k = 'msgphp/domain' => $result['domains'][$k]] + $result['domains'];
+
+ return $result;
+};
+
+$getTemplate = function (string $file) use ($msgphpGlobal): array {
+ $oldBaseDir = getcwd();
+ chdir(dirname(__DIR__));
+ extract(['msgphp' => $msgphpGlobal()]);
+ $template = include $file;
+ chdir($oldBaseDir);
+
+ return $template;
+};
+
+$templates = $templateVars = [];
+foreach (glob('docs/.refs/*.php') as $reference) {
+ $vars = $getTemplate($reference);
+ $templates[$name = basename($reference, '.php')] = array_shift($vars);
+ $templateVars[$name] = $vars;
+}
+
+$twig = new Environment(new ArrayLoader($templates));
+$twig->addGlobal('msgphp', $msgphpGlobal());
+$replacements = [];
+
+foreach (array_keys($templates) as $template) {
+ $from = '~'.preg_quote($start = '', '~').'.*'.preg_quote($end = '', '~').'~s';
+ $to = $start."\n".$twig->render($template, $templateVars[$template]).$end;
+ $replacements[$from] = $to;
+}
+
+foreach (glob('docs/{*,**/*}.md', \GLOB_BRACE) as $file) {
+ file_put_contents($file, preg_replace(array_keys($replacements), array_values($replacements), file_get_contents($file)));
+}
+
+echo "\e[34mCompiled templates\e[0m\n";
+echo ' > '.implode("\n > ", array_keys($templates))."\n";
diff --git a/bin/create-project b/bin/create-project
index 0269166b..cd7b5515 100755
--- a/bin/create-project
+++ b/bin/create-project
@@ -6,31 +6,30 @@ if [[ $# -gt 1 ]] ; then
fi
DIR="${1:-var/test-project/}"
-COMPONENTS="symfony/var-dumper "
-BUNDLES=""
+PACKAGES="orm "
+DEV_PACKAGES="debug maker server "
-for PACKAGE in $(find src/*/composer.json -type f) ; do
+for PACKAGE in $(find src/*Bundle/composer.json -type f) ; do
PACKAGE_NAME=$(grep -E "^\s*\"name\"\s*:\s*\"msgphp\/([^\"]+)\"\s*,\s*$" "${PACKAGE}")
if [[ ! -z $PACKAGE_NAME ]] ; then
PACKAGE_NAME=$(echo "${PACKAGE_NAME}" | sed -e "s/^\s*\"name\":\s*\"msgphp\///" -e "s/\"\s*,\s*$//")
- if [[ $PACKAGE_NAME = *-bundle ]] ; then
- echo -en "\e[34mInstall \"msgphp/${PACKAGE_NAME}\"? [yN]\e[0m "
- read answer
- if [[ $answer =~ ^y|Y|yes|YES$ ]] ; then
- BUNDLES="${BUNDLES}msgphp/${PACKAGE_NAME} "
- fi
+ echo -en "\e[34mInstall \"msgphp/${PACKAGE_NAME}\"? [yN]\e[0m "
+ read answer
+ if [[ $answer =~ ^y|Y|yes|YES$ ]] ; then
+ PACKAGES="${PACKAGES}msgphp/${PACKAGE_NAME} "
fi
fi
done
-composer create-project --prefer-dist symfony/skeleton "${DIR}"
+composer create-project --prefer-dist --no-progress symfony/skeleton "${DIR}"
[[ $? -ne 0 ]] && exit 1
pushd "${DIR}" &> /dev/null
echo -e "\e[34mInstalling dependencies...\e[0m"
-composer config extra.symfony.allow-contrib true
-composer require --prefer-dist --no-suggest ${COMPONENTS} ${BUNDLES}
+composer config extra.symfony.allow-contrib true && \
+composer require --prefer-dist --no-suggest ${PACKAGES} && \
+composer require --prefer-dist --dev --no-suggest ${DEV_PACKAGES}
[[ $? -ne 0 ]] && exit 1
popd &> /dev/null
@@ -40,7 +39,7 @@ read answer
if [[ ${answer:-y} =~ ^y|Y|yes|YES$ ]] ; then
composer link -h &> /dev/null
if [[ $? != 0 ]] ; then
- composer require --dev --no-progress --prefer-dist --no-interaction --no-suggest ro0nl/link
+ composer update --prefer-dist --quiet
[[ $? -ne 0 ]] && exit 1
fi
diff --git a/bin/cs b/bin/cs
index 4b00ddcf..ca69ccab 100755
--- a/bin/cs
+++ b/bin/cs
@@ -14,8 +14,8 @@ BIN="vendor/bin/php-cs-fixer"
CMD="${BIN} fix --dry-run --verbose --diff ${@} ${DIR}"
if [[ ! -f "${BIN}" ]] ; then
- composer update --no-progress --prefer-dist --no-interaction --no-suggest
+ composer update --prefer-dist --quiet
[[ $? -ne 0 ]] && exit 1
fi
-${CMD}
+bash -xc "${CMD}" 2>&1
diff --git a/bin/phpunit-package b/bin/phpunit-package
index 1715d1dd..19909876 100755
--- a/bin/phpunit-package
+++ b/bin/phpunit-package
@@ -5,15 +5,17 @@ if [[ $# -lt 1 ]] ; then
exit 1
fi
+BASE_DIR="$(cd $(dirname $(dirname "$0")) && pwd)"
+
if [[ -d $1 ]] ; then
- PACKAGE_DIR="$1"
+ PACKAGE_DIR="$(cd "$1" && pwd)"
else
- PACKAGE_DIR="src/$(echo $1 | sed -r 's/(^|-)(\w)/\U\2/g')"
+ PACKAGE_DIR="${BASE_DIR}/src/$(echo "$1" | sed -r 's/(^|-)(\w)/\U\2/g')"
fi
shift;
BIN="vendor/bin/simple-phpunit"
-CMD="${BIN} ${@}"
+CMD="SYMFONY_PHPUNIT_VERSION=7.4 ${BIN} ${@}"
if [[ ! -f "${PACKAGE_DIR}/phpunit.xml.dist" ]] ; then
echo "No valid package found in \"${PACKAGE_DIR}\""
@@ -21,25 +23,24 @@ if [[ ! -f "${PACKAGE_DIR}/phpunit.xml.dist" ]] ; then
fi
if [[ ! -f "${PACKAGE_DIR}/${BIN}" ]] ; then
- composer update --no-progress --prefer-dist --no-interaction --no-suggest --working-dir="${PACKAGE_DIR}"
+ composer update --prefer-dist --quiet --working-dir="${PACKAGE_DIR}"
[[ $? -ne 0 ]] && exit 1
fi
-if [[ ! $TRAVIS ]] || ( [[ $DEPS = high && $TRAVIS_BRANCH = master && $TRAVIS_PULL_REQUEST = false ]] ) ; then
- WORKING_DIR="$(dirname $(dirname "$0"))"
+if [[ ! $TRAVIS ]] ; then
composer link -h &> /dev/null
- if [[ $? != 0 ]] ; then
- composer update --no-progress --prefer-dist --no-interaction --no-suggest --working-dir="${WORKING_DIR}"
+ if [[ $? -ne 0 ]] ; then
+ composer update --prefer-dist --quiet --working-dir="${BASE_DIR}"
[[ $? -ne 0 ]] && exit 1
fi
- composer link --working-dir="${WORKING_DIR}" "$(cd "${PACKAGE_DIR}" && pwd)"
+ composer link "${PACKAGE_DIR}" --working-dir="${BASE_DIR}"
[[ $? -ne 0 ]] && exit 1
fi
pushd "${PACKAGE_DIR}" &> /dev/null
-echo -e "\e[34mTesting package\e[0m"
-${CMD}
+bash -xc "${CMD}" 2>&1
+[[ $? -ne 0 ]] && exit 1
popd &> /dev/null
diff --git a/bin/pr b/bin/pr
index 36bb6b13..1495bd74 100755
--- a/bin/pr
+++ b/bin/pr
@@ -22,55 +22,50 @@ if [[ ! -z $GITHUB_TOKEN ]] ; then
URL="$URL?access_token=${GITHUB_TOKEN}"
fi
-if [[ ! $(git branch --list ${BRANCH}) ]] ; then
- BODY=$(curl -sS -w ">>>%{http_code}" "${URL}")
+if [[ $(git branch --list ${BRANCH}) ]] ; then
+ echo "Branch \"${BRANCH}\" already exists"
+ exit 1
+fi
+
+BODY=$(curl -sS -w ">>>%{http_code}" "${URL}")
- if [[ ! $BODY =~ \>\>\>200\s*$ ]] ; then
- echo "Error fetching pull request #${PR}..."
- echo "${BODY}"
- exit 1
- fi
+if [[ ! $BODY =~ \>\>\>200\s*$ ]] ; then
+ echo "Error fetching pull request #${PR}..."
+ echo "${BODY}"
+ exit 1
+fi
- BODY=$(echo "${BODY}" | sed -e "s/>>>200$//" -e "s/\\\/\\\\\\\/g")
+BODY=$(echo "${BODY}" | sed -e "s/>>>200$//" -e "s/\\\/\\\\\\\/g")
- UPSTREAM_SSH_URL=$(cat < /dev/null
- git remote add "${BRANCH}" "${UPSTREAM_SSH_URL}" --fetch --track "${UPSTREAM_BRANCH}"
- if [[ $? -eq 1 ]] ; then
- git remote remove "${BRANCH}"
- echo "Pull request closed. Aborted."
- exit 1
- fi
- git branch --set-upstream-to "${BRANCH}/${UPSTREAM_BRANCH}"
-fi
-
-if [[ ! -z $(git status --porcelain) ]] ; then
- echo "Current working directory must be clean to continue..."
+echo "Adding pull request remote \"${UPSTREAM_SSH_URL}\"..."
+git remote remove "${BRANCH}" &> /dev/null
+git remote add "${BRANCH}" "${UPSTREAM_SSH_URL}" --fetch --track "${UPSTREAM_BRANCH}"
+if [[ $? -eq 1 ]] ; then
+ git remote remove "${BRANCH}"
+ echo "Pull request closed. Aborted."
exit 1
fi
-
-git checkout "${BRANCH}"
-git pull
+git checkout -b "${BRANCH}" "${BRANCH}/${UPSTREAM_BRANCH}"
diff --git a/bin/sa b/bin/sa
index 78fe34f3..38b90b2b 100755
--- a/bin/sa
+++ b/bin/sa
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
- DIR="src/"
+ DIR="src/ tests/fixtures/sa"
elif [[ -d $1 ]] ; then
DIR="$1"
shift;
@@ -11,11 +11,11 @@ else
fi
BIN="vendor/bin/phpstan"
-CMD="${BIN} analyse --configuration phpstan.neon --level max ${@} ${DIR}"
+CMD="${BIN} analyse ${@} ${DIR}"
if [[ ! -f $BIN ]] ; then
- composer update --no-progress --prefer-dist --no-interaction --no-suggest
+ composer update --prefer-dist --quiet
[[ $? -ne 0 ]] && exit 1
fi
-${CMD}
+bash -xc "${CMD}" 2>&1
diff --git a/bin/smoke-test b/bin/smoke-test
index 28c6fcd1..18ef50ea 100755
--- a/bin/smoke-test
+++ b/bin/smoke-test
@@ -1,7 +1,12 @@
#!/usr/bin/env bash
+if [[ ! $# -eq 0 ]] ; then
+ echo "Usage: $0"
+ exit 1
+fi
+
echo -e "\e[34mUpdating dependencies...\e[0m"
-bin/composer update --no-suggest --no-progress && composer update --no-suggest --no-progress
+bin/composer update --prefer-dist --quiet && composer update --prefer-dist --quiet
[[ $? -ne 0 ]] && exit 1
echo -e "\e[34mCode Style / Static Analysis\e[0m"
diff --git a/composer.json b/composer.json
index 01e64363..592f43e7 100644
--- a/composer.json
+++ b/composer.json
@@ -2,11 +2,13 @@
"description": "Composer configuration for bulk CI testing of packages",
"require-dev": {
"wikimedia/composer-merge-plugin": "^1.4",
- "friendsofphp/php-cs-fixer": "^2.10",
- "phpstan/phpstan": "^0.9",
- "phpstan/phpstan-phpunit": "^0.9",
- "phpunit/phpunit": "^7.0",
- "ro0nl/link": "^1.0"
+ "friendsofphp/php-cs-fixer": "^2.13",
+ "phpstan/phpstan": "^0.10",
+ "phpstan/phpstan-phpunit": "^0.10",
+ "phpunit/phpunit": "^7.3",
+ "ro0nl/link": "^1.0",
+ "symfony/messenger": "@dev",
+ "twig/twig": "^2.5"
},
"autoload-dev": {
"psr-4": {
diff --git a/docs/.refs/available-bundles.php b/docs/.refs/available-bundles.php
new file mode 100644
index 00000000..5a12c61f
--- /dev/null
+++ b/docs/.refs/available-bundles.php
@@ -0,0 +1,9 @@
+name][$class::NAME] = $class;
+ }
+
+ ksort($types[$domain['package']->name]);
+}
+
+$template = <<<'MD'
+{% for package, info in types %}
+## `{{ package }}`
+
+Type name | Type class
+--- | ---
+{% for name, class in info %}
+`{{ name }}` | `{{ class }}`
+{% endfor %}
+
+{% endfor %}
+MD;
+
+return [$template, 'types' => $types];
diff --git a/docs/.refs/domain-events.php b/docs/.refs/domain-events.php
new file mode 100644
index 00000000..cbd537cd
--- /dev/null
+++ b/docs/.refs/domain-events.php
@@ -0,0 +1,14 @@
+
+- `msgphp/eav-bundle`: Basic entity-attribute-value management (the `EAV` domain)
+- `msgphp/user-bundle`: Basic user management (the `User` domain)
+
-- `msgphp/eav-bundle`: Provides basic entity-attribute-value management (the EAV domain)
-- `msgphp/user-bundle`: Provides basic user management (the user domain)
-
-## Installing using [Composer]
+## Installation
```bash
composer require msgphp/-bundle
```
-### With Symfony Flex
-
-When [Symfony Flex] is used to mange your Symfony application the minimal bundle configuration is created for you
-automatically based on [MsgPHP recipes].
+!!! info
+ When [Symfony Flex] is used to mange your Symfony application the minimal bundle configuration is created for you
+ automatically based on [MsgPHP recipes]
## Configuration
-By default a bundle provides the following minimal configuration:
-
-```yaml
-msgphp_:
- class_mapping: []
- id_type_mapping: []
- default_id_type: integer
- commands: []
-```
-
-Depending on your personal preference you can also write the configuration in any other supported format. See the
-[demo configuration] for a more advanced example.
+A bundle provides the following configuration nodes by default:
### `class_mapping`
-Configures the bundle with a class mapping to to well which classes of yours should be used for a known class of ours.
+Configures the bundle with a class mapping to tell which classes of yours should be used for a known class of ours.
```yaml
msgphp_:
@@ -48,7 +34,7 @@ msgphp_:
MsgPhp\SomeClass: App\SomeClass
```
-The class mapping applies when working with an [object factory](../ddd/factory/object.md#).
+The class mapping applies when working with an [object factory](../ddd/factory/object.md).
Depending on the bundle a specific class mapping entry might enable one of the bundle its features which is otherwise
disabled by default.
@@ -63,24 +49,26 @@ It ensures a default class mapping entry is added which maps the identifier to a
```yaml
msgphp_:
id_type_mapping:
- MsgPhp\SomeDomainIdInterface: some_type_name
+ MsgPhp\SomeDomain\SomeIdInterface: some_type_name
```
-By convention any [Doctrine DBAL type] can be used. Additionally the following UUID types are detected as well:
+!!! note
+ See the [reference](../reference/identifiers.md) page for all available identifiers provided per domain
+
+By convention any [Doctrine DBAL type] can be used for a type name. Additionally the following UUID types are detected
+as well:
- `uuid`
- `uuid_binary`
- `uuid_binary_ordered_time`
-See also the [reference](../reference/identifiers.md) page for all available identifiers provided per domain.
-
### `default_id_type`
-Configures a default type name to use for all known domain identifiers provided by the bundle.
+Configures a default type name to use for all known domain identifiers provided by the bundle. See also [`id_type_mapping`](#id_type_mapping).
```yaml
msgphp_:
- default_id_type: uuid
+ default_id_type: integer
```
### `commands`
@@ -96,38 +84,40 @@ can be explicitly enabled or disabled by command.
```yaml
msgphp_:
commands:
- MsgPhp\SomeCommand: true
- MsgPhp\SomeOtherCommand: false
+ MsgPhp\SomeDomain\Command\SomeCommand: true
+ MsgPhp\SomeDomain\Command\SomeOtherCommand: false
```
-## Basic configuration example
+## Basic Configuration Example
Given a bundle provides the following domain identifiers:
-- `MsgPhp\FooIdInterface`
-- `MsgPhp\BarIdInterface`
-- `MsgPhp\BazIdInterface`
+- `MsgPhp\SomeDomain\FooIdInterface`
+- `MsgPhp\SomeDomain\BarIdInterface`
+- `MsgPhp\SomeDomain\BazIdInterface`
```yaml
msgphp_:
default_id_type: uuid
id_type_mapping:
- MsgPhp\FooIdInterface: integer
+ MsgPhp\SomeDomain\FooIdInterface: integer
# implied:
- # MsgPhp\BarIdInterface: uuid
- # MsgPhp\BazIdInterface: uuid
+ # MsgPhp\SomeDomain\BarIdInterface: uuid
+ # MsgPhp\SomeDomain\BazIdInterface: uuid
class_mapping:
- MsgPhp\BarIdInterface: App\MyBarUuid
+ MsgPhp\SomeDomain\BarIdInterface: App\MyBarUuid
# implied:
- # MsgPhp\FooIdInterface: MsgPhp\Domain\DomainId
- # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid\DomainId
+ # MsgPhp\SomeDomain\FooIdInterface: MsgPhp\SomeDomain\FooId
+ # MsgPhp\SomeDomain\BazIdInterface: MsgPhp\SomeDomain\Infra\Uuid\BazId
```
+!!! info
+ See also the [demo application configuration]
+
[Symfony Framework]: https://symfony.com/
[dependency injection]: https://symfony.com/doc/current/components/dependency_injection.html
-[Composer]: https://getcomposer.org/
[Symfony Flex]: https://symfony.com/doc/current/setup/flex.html
[MsgPHP recipes]: https://github.com/symfony/recipes-contrib/tree/master/msgphp
[autowiring]: https://symfony.com/doc/current/service_container/autowiring.html
-[demo configuration]: https://github.com/msgphp/symfony-demo-app/blob/master/config/packages/msgphp.php
+[demo application configuration]: https://github.com/msgphp/symfony-demo-app/blob/master/config/packages/msgphp.php
[Doctrine DBAL type]: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html
diff --git a/docs/cookbook/configuring-a-message-bus.md b/docs/cookbook/configuring-a-message-bus.md
new file mode 100644
index 00000000..d3b00d25
--- /dev/null
+++ b/docs/cookbook/configuring-a-message-bus.md
@@ -0,0 +1,110 @@
+# Configuring A Message Bus
+
+To be able to dispatch the default [messages](../reference/messages.md) a [message bus](../message-driven/message-bus.md)
+must be configured.
+
+## Using [Symfony Messenger](../infrastructure/symfony-messenger.md)
+
+### Installation
+
+```bash
+composer install symfony/messenger
+
+# with Symfony Flex
+composer install messenger
+```
+
+### Configuration
+
+See the Messenger [recipe configuration] for the minimal configuration to put in `config/packages/messenger.yaml`.
+
+!!! info
+ The configuration is automatically added with Symfony Flex
+
+Configure a command- and event bus:
+
+```yaml
+# config/packages/messenger.yaml
+
+framework:
+ messenger:
+ # ...
+
+ default_bus: command_bus
+ buses:
+ command_bus:
+ middleware:
+ - msgphp.messenger.console_message_receiver
+ event_bus:
+ middleware:
+ - msgphp.messenger.console_message_receiver
+ - allow_no_handler
+```
+
+By default MsgPHP uses the bus configured under `framework.messenger.default_bus`. You can override its alias services
+to use any other bus:
+
+```yaml
+# config/services.yaml
+
+services:
+ # ...
+
+ msgphp.messenger.command_bus: '@command_bus'
+ msgphp.messenger.event_bus: '@event_bus'
+```
+
+## Using [SimpleBus](../infrastructure/simple-bus.md)
+
+### Installation
+
+```bash
+composer install simple-bus/symfony-bridge
+```
+
+### Configuration
+
+Enable the bundles:
+
+```php
+ ['all' => true],
+ SimpleBus\SymfonyBridge\SimpleBusEventBusBundle::class => ['all' => true],
+];
+```
+
+By default MsgPHP uses the `simple_bus.command_bus` and `simple_bus.event_bus` services. You can override its alias
+services to use any other bus:
+
+```yaml
+# config/services.yaml
+
+services:
+ # ...
+
+ msgphp.simple_bus.command_bus: '@simple_bus.command_bus'
+ msgphp.simple_bus.event_bus: '@simple_bus.event_bus'
+```
+
+## Using A Custom Bus
+
+To use a custom [message bus](../message-driven/message-bus.md) implementation you can override its main alias service:
+
+```yaml
+# config/services.yaml
+
+services:
+ # ...
+
+ MsgPhp\Domain\Message\DomainMessageBusInterface: '@my_bus'
+```
+
+!!! info
+ The implementation must be capable to dispatch both command- and event messages
+
+[recipe configuration]: https://github.com/symfony/recipes/blob/master/symfony/messenger/4.1/config/packages/messenger.yaml
diff --git a/docs/cookbook/configuring-doctrine-orm.md b/docs/cookbook/configuring-doctrine-orm.md
new file mode 100644
index 00000000..ead236d4
--- /dev/null
+++ b/docs/cookbook/configuring-doctrine-orm.md
@@ -0,0 +1,229 @@
+# Configuring Doctrine ORM
+
+After a [bundle](bundle-installation.md) is installed we can configure [Doctrine ORM](../infrastructure/doctrine-orm.md).
+
+## Installation
+
+```bash
+composer install doctrine/orm doctrine/doctrine-bundle
+
+# with Symfony Flex
+composer install orm
+```
+
+## Configuration
+
+See the Doctrine Bundle [recipe configuration] for the minimal configuration to put in `config/packages/doctrine.yaml`.
+
+Although the examples use annotation based mappings, you are not required to do so. [Read more][doctrine-bundle-mapping-config].
+
+!!! info
+ The configuration is automatically added with Symfony Flex
+
+By default MsgPHP uses the `doctrine.orm.entity_manager` service. You can override its alias service to use any other
+entity manager:
+
+```yaml
+# config/services.yaml
+
+services:
+ # ...
+
+ msgphp.doctrine.entity_manager: '@doctrine.orm.some_other_entity_manager'
+```
+
+## Mapping Entities
+
+A MsgPHP bundle provides base entity models in the form of [mapped superclasses]. An actual entity object must be
+defined by your application:
+
+```php
+:
+ # ...
+
+ class_mapping:
+ MsgPhp\SomeDomain\Entity\SomeEntity: App\Entity\SomeEntity
+```
+
+!!! note
+ See the [reference](../reference/entities.md) page for all available entities provided per domain. When using a
+ entity field / feature trait its default ORM mapping is configured automatically.
+
+!!! info
+ With Symfony Flex the required entities of a MsgPHP bundle are automatically created, including ORM mapping
+ and configuration
+
+## Mapping Identifiers
+
+When an entity object is identified using a [domain identifier](../ddd/identifiers.md) we must configure it accordingly:
+
+```php
+:
+ # ...
+
+ class_mapping:
+ MsgPhp\SomeDomain\SomeIdInterface: App\SomeId
+ id_type_mapping:
+ MsgPhp\SomeDomain\SomeIdInterface: bigint
+```
+
+### Without Nullability
+
+```php
+id = $id;
+ }
+
+ public function getId(): SomeIdInterface
+ {
+ return $this->id;
+ }
+}
+```
+
+Or coupled with a known identifier class:
+
+```php
+id = new SomeId();
+ }
+
+ // ...
+}
+```
+
+### Without Automatic Hydration
+
+To hydrate the primitive identifier value instead of a value object, the type can be configured regularly. It requires
+to couple with a known identifier class.
+
+```php
+id = $id->isEmpty() ? null : (int) $id->toString();
+ }
+
+ public function getId(): SomeIdInterface
+ {
+ return SomeId::fromValue($this->id);
+ }
+}
+```
+
+!!! info
+ [Read more](../infrastructure/doctrine-orm.md#domain-identifier-hydration) about Doctrine identifier hydration
+
+## Database Setup
+
+Configure the database URL:
+
+```bash
+# .env
+
+# sqlite
+DATABASE_URL=sqlite:///%kernel.project_dir%/var/db.sqlite
+
+# mysql / mariadb
+DATABASE_URL="mysql://user:pass@host:3306/db_name?charset=utf8mb4&serverVersion=5.7"
+```
+
+Create the database schema:
+
+```bash
+bin/console doctrine:database:create
+bin/console doctrine:schema:update --force
+```
+
+[recipe configuration]: https://github.com/symfony/recipes/blob/master/doctrine/doctrine-bundle/1.6/config/packages/doctrine.yaml
+[doctrine-bundle-mapping-config]: https://symfony.com/doc/master/bundles/DoctrineBundle/configuration.html#mapping-configuration
+[mapped superclasses]: https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
diff --git a/docs/ddd/collections.md b/docs/ddd/collections.md
index 0a675d0c..cee6e36d 100644
--- a/docs/ddd/collections.md
+++ b/docs/ddd/collections.md
@@ -71,6 +71,48 @@ Keys are preserved.
Returns a map with each collection element as returned by `$mapper`.
+## Pagination API
+
+A collection that is part of a paginated result set is bound to `MsgPhp\Domain\PaginatedDomainCollectionInterface`. Its
+purpose is to expose the current pagination.
+
+### Extends
+
+- [`DomainCollectionInterface`](#collections)
+
+---
+
+### `getOffset(): float`
+
+Get the current page offset.
+
+---
+
+### `getLimit(): float`
+
+Get the current page limit (e.g. items per page).
+
+---
+
+### `getCurrentPage(): float`
+
+Get the current page number.
+
+---
+
+### `getLastPage(): float`
+
+Get the last page number.
+
+---
+
+### `getTotalCount(): float`
+
+Get the total no. of items in the full result set.
+
+!!! note
+ `count()` should return the no. of items on the current page
+
## Implementations
### `MsgPhp\Domain\DomainCollection`
@@ -82,7 +124,7 @@ type `\Traversable`. Meaning the minimal no. of elements are traversed, i.e. unt
- `__construct(iterable $elements)`
- `$elements`: The elements this collection contains
-#### Basic example
+#### Basic Example
```php
map(function (int $value): int {
}); // [2, 4]
```
+### `MsgPhp\Domain\PaginatedDomainCollection`
+
+A first class citizen paginated domain collection to transform any collection into a paginated collection.
+
+- `__construct(iterable $elements, float $offset = .0, float $limit = .0, float $count = null, float $totalCount = null)`
+ - `$elements`: The elements this collection contains
+
### `MsgPhp\Domain\Infra\Doctrine\DomainCollection`
A Doctrine tailored domain collection.
diff --git a/docs/ddd/entities.md b/docs/ddd/entities.md
index cad7ed2e..cf968629 100644
--- a/docs/ddd/entities.md
+++ b/docs/ddd/entities.md
@@ -7,50 +7,39 @@ features represent a read/write-operation.
They are defined in a dedicated namespace for discovery, respectively `Msgphp\Domain\Entity\Fields\` and
`MsgPhp\Domain\Entity\Features\`. Additionally more specific fields and features can be provided per domain layer.
-See also the [reference](../reference/entities.md) page for all available entities provided per domain.
+!!! note
+ See the [reference](../reference/entities.md) page for all available entities provided per domain
-## Entity Fields
+## Basic Example
-### `CreatedAtField`
+```php
+createdAt = new \DateTimeImmutable();
+ }
+}
-- `isEnabled(): bool`
+// --- USAGE ---
-### `LastUpdatedAtField`
+$entity = new MyEntity();
+$createdAt = $entity->getCreatedAt();
-A datetime value representing an entity was last updated at. Requires `$lastUpdatedAt` to be set initially.
-
-- `getLastUpdatedAt(): \DateTimeInterface`
-
-## Entity Features
-
-### `CanBeConfirmed`
-
-Provides ability to confirm an entity. Requires `$confirmationToken` to be set initially (usually a random token).
-
-- `getConfirmationToken(): ?string`
-- `getConfirmedAt(): ?\DateTimeInterface`
-- `isConfirmed(): bool`
-- `confirm(): void`
- - Resets `$confirmationToken`
- - Sets `$confirmatedAt` to the current datetime
-
-### `CanBeEnabled`
-
-Provides ability to toggle an entity its availability state.
-
-- Inherits from `EnabledField`
-- `enable(): void`
- - Sets `$enabled` to `true`
-- `disable(): void`
- - Sets `$enabled` to `false`
+if (!$entity->isEnabled()) {
+ $entity->enable();
+}
+```
[POPO]: https://stackoverflow.com/questions/41188002/what-does-the-term-plain-old-php-object-popo-exactly-mean
[traits]: https://secure.php.net/traits
diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md
index 36707804..70cba70f 100644
--- a/docs/ddd/factory/entity-aware.md
+++ b/docs/ddd/factory/entity-aware.md
@@ -20,20 +20,20 @@ of [identity](../identities.md) value can be passed as `$id`.
### `identify(string $class, $value): DomainIdInterface`
-Returns a factorized [identifier](../identifiers.md) for the given entity class from a known primitive value.
+Returns a factorized [domain identifier](../identifiers.md) for the given entity class from a known primitive value.
---
### `nextIdentifier(string $class): DomainIdInterface`
-Returns the nex [identifier](../identifiers.md) for the given entity class. Depending on the implementation its value
-might be considered empty if it's not capable to calculate one upfront.
+Returns the next [domain identifier](../identifiers.md) for the given entity class. Depending on the implementation its
+value might be considered empty if it's not capable to calculate one upfront.
## Implementations
### `MsgPhp\Domain\Factory\EntityAwareFactory`
-A generic entity factory. It decorates any object factory and is based on a known [identity mapping](../identity-mapping.md)
+A generic entity factory. It decorates any object factory and uses the [domain identity mapping](../identity-mapping.md)
as well as a known entity to identifier class mapping.
- `__construct(DomainObjectFactoryInterface $factory, DomainIdentityMappingInterface $identityMapping, array $identifierMapping = [])`
diff --git a/docs/ddd/factory/static.md b/docs/ddd/factory/static.md
index 0ccb0dbb..10b4398a 100644
--- a/docs/ddd/factory/static.md
+++ b/docs/ddd/factory/static.md
@@ -1,13 +1,12 @@
# Static Factory
-A static factory is a utility class, it cannot be initialized as a new instance using `new ...();`. Its purpose is to
-factorize a known implementation for a given class.
+A static factory is a utility class. Its purpose is to ease factorizing some known implementation in a static way.
## Implementations
### `MsgPhp\Domain\Factory\DomainIdFactory`
-Factorizes an [identifier](../identifiers.md).
+Factorizes an [domain identifier](../identifiers.md).
- `static create($value): DomainIdInterface`
- `$value`: Any (primitive) identifier value
@@ -32,16 +31,19 @@ $id = DomainIdFactory::create('cf3d2f85-6c86-44d1-8634-af51c91a9a74');
### `MsgPhp\Domain\Factory\DomainCollectionFactory`
-Factorizes a [collection](../collections.md).
+Factorizes a [domain collection](../collections.md).
- `static create(?iterable $value): DomainCollectionInterface`
- - `$value`: Any (primitive) iterable value or `null` to imply an empty collection
+ - `$value`: Any iterable value or `null` to imply an empty collection
+- `static createFromCallable(callable $value): DomainCollectionInterface`
+ - `$value`: A callable returning any iterable value
-#### Basic example
+#### Basic Example
```php
Event()` methods. It's designed to support default [events](events.md#implementations)
-out-of-the-box.
+`handleEvent()` methods.
#### Basic example
```php
value === $event->newValue) {
return false;
@@ -66,7 +66,7 @@ class MyEntity implements DomainEventHandlerInterface
$entity = new MyEntity();
-if ($entity->handleEvent(new MyEvent('new value'))) {
+if ($entity->handleEvent(new MyDomainEvent('new value'))) {
// do something
}
```
diff --git a/docs/event-sourcing/events.md b/docs/event-sourcing/events.md
index 5f082d1f..b38de89d 100644
--- a/docs/event-sourcing/events.md
+++ b/docs/event-sourcing/events.md
@@ -1,7 +1,10 @@
# Events
-A domain event is bound to `MsgPhp\Domain\Event\DomainEventInterface`. Its purpose is to identify concrete domain events
-which represent something that happens. When handled it might lead to an application state change.
+A domain event is bound to `MsgPhp\Domain\Event\DomainEventInterface`. Its purpose is to represent any action that can
+ _happen_ regarding the domain. When handled it might lead to an application state change.
+
+!!! note
+ See the [reference](../reference/domain-events.md) page for all available events provided per domain
## Implementations
diff --git a/docs/index.md b/docs/index.md
index fde0f148..6c2ca571 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,16 +1,3 @@
# MsgPHP Documentation
> [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated.
-
----
-
-# News
-
-- **`2018-03-24`** Added initial _projection_ chapter
-- **`2018-03-18`** Added initial _cookbook_ and _reference_ chapters
-- **`2018-02-18`** Added initial _infrastructure_ chapter
-- **`2018-02-12`** Added [domain identities](ddd/identities.md) chapter
-- **`2018-01-27`** Added initial _message driven_ chapter
-- **`2018-01-26`** Added initial _event sourcing_ chapter
-- **`2018-01-25`** Added initial project [code conventions](code-conventions.md)
-- **`2018-01-18`** Initial documentation setup. Work in progress as of today.
diff --git a/docs/infrastructure/api-platform.md b/docs/infrastructure/api-platform.md
index df8ed43f..83ce84cc 100644
--- a/docs/infrastructure/api-platform.md
+++ b/docs/infrastructure/api-platform.md
@@ -4,21 +4,31 @@ An overview of available infrastructural code when using [API Platform].
- Requires [api-platform/core]
-## Domain Projection Data Provider
+## Projection Data Provider
-When working with [projections](../projection/models.md) an [API Data Provider] is provided by `MsgPhp\Domain\Infra\ApiPlatform\DomainProjectionDataProvider`.
+When working with [projections](../projection/models.md) an [API Data Provider] is provided by `MsgPhp\Domain\Infra\ApiPlatform\ProjectionDataProvider`.
It uses any [projection repository](../projection/repositories.md) in an effort to provide API resources.
### Minimal configuration
-See also [API Platform Configuration].
-
```yaml
api_platform:
+ # ...
+
resource_class_directories:
- '%kernel.project_dir%/src/Api/Projection'
+
+services:
+ # ..
+.
+ MsgPhp\Domain\Infra\ApiPlatform\ProjectionDataProvider:
+ tags: [api_platform.collection_data_provider]
+ autowire: true
```
+!!! note
+ See also [API Platform Configuration] documentation
+
### Basic example
```php
@@ -28,19 +38,19 @@ namespace App\Api\Projection;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
-use MsgPhp\Domain\Projection\DomainProjectionInterface;
+use MsgPhp\Domain\Projection\ProjectionInterface;
/**
* @ApiResource(shortName="Some")
*/
-class SomeProjection implements DomainProjectionInterface
+class SomeProjection implements ProjectionInterface
{
/**
* @ApiProperty(identifier=true)
*/
public $id;
- public static function fromDocument(array $document): DomainProjectionInterface
+ public static function fromDocument(array $document): ProjectionInterface
{
$projection = new self();
$projection->id = $document['id'] ?? null;
diff --git a/docs/infrastructure/doctrine-collections.md b/docs/infrastructure/doctrine-collections.md
index 18f116e1..d45a6693 100644
--- a/docs/infrastructure/doctrine-collections.md
+++ b/docs/infrastructure/doctrine-collections.md
@@ -7,10 +7,10 @@ An overview of available infrastructural code when using Doctrine's [Collections
## Domain Collection
A Doctrine tailored [domain collection](../ddd/collections.md) is provided by `MsgPhp\Domain\Infra\Doctrine\DomainCollection`.
-It leverages type `Doctrine\Common\Collections\Collection` as underlying data type.
+It decorates any `Doctrine\Common\Collections\Collection` type.
- `__construct(Collection $collection)`
- - `$collection`: The underlying collection
+ - `$collection`: The decorated collection
### Basic example
diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md
index fea38226..90ff1e8f 100644
--- a/docs/infrastructure/doctrine-dbal.md
+++ b/docs/infrastructure/doctrine-dbal.md
@@ -6,7 +6,7 @@ An overview of available infrastructural code when using Doctrine's [Database Ab
## Domain Identifier Type
-A translation between the database type and a [identifier](../ddd/identifiers.md) type in PHP is provided by
+A translation between the database type and a [domain identifier](../ddd/identifiers.md) type in PHP is provided by
`MsgPhp\Domain\Infra\Doctrine\DomainIdType`. Its purpose is to abstract the underlying data type of the identifier
value.
@@ -22,8 +22,9 @@ default [`Type`][api-type] implementation and can be used either generic or as a
- `$type`: A Doctrine type name to use as underlying data type. If not set `Type::INTEGER` is used.
- `static getDataType(): string`
-See also the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per
-domain.
+!!! note
+ See the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per
+ domain
### Basic example
diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md
index b840fa4c..fdc46383 100644
--- a/docs/infrastructure/doctrine-orm.md
+++ b/docs/infrastructure/doctrine-orm.md
@@ -31,7 +31,7 @@ $mapping = new DomainIdentityMapping($em);
## Domain Repository
-A Doctrine tailored [repository trait](../ddd/repositories.md) is provided by
+A Doctrine tailored [domain repository trait](../ddd/repositories.md) is provided by
`MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait`. It uses Doctrine's [`EntityManagerInterface`][api-em] as
underlying persistence layer.
@@ -163,14 +163,15 @@ $otherEntity = $factory->create(MyEntity::class, [
]);
```
-## Hydration
+## Domain Identifier Hydration
-When working with [identifiers](../ddd/identifiers.md) and the corresponding [type](doctrine-dbal.md#domain-identifier-type)
-a problem might occur when hydrating scalar values, e.g. using [`Query::getScalarResult()`][api-query-getscalarresult];
-it would return instances of `MsgPhp\Domain\DomainIdInterface` that can only be casted to string as its (true) scalar
-value (due to `__toString()`). In case the underlying data type is e.g. `integer` we'll lose it.
+When working with [domain identifiers](../ddd/identifiers.md) and its corresponding [type](doctrine-dbal.md#domain-identifier-type)
+a problem might occur when hydrating scalar values, e.g. using [`Query::getScalarResult()`][api-query-getscalarresult].
-To overcome, two hydration modes are available in order to hydrate the primitive identifier value instead.
+It would use instances of `MsgPhp\Domain\DomainIdInterface` that can only be casted to string as its (true) scalar
+value (due to `__toString()`). In case the underlying data type is e.g. `integer` it'll be lost.
+
+To overcome, two hydration modes are available to hydrate the primitive identifier value instead.
### Basic example
@@ -182,7 +183,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping as ORM;
use MsgPhp\Domain\DomainId;
use MsgPhp\Domain\Infra\Doctrine\DomainIdType;
-use MsgPhp\Domain\Infra\Doctrine\Hydration\{ScalarHydrator, SingleScalarHydrator}
+use MsgPhp\Domain\Infra\Doctrine\Hydration\{ScalarHydrator, SingleScalarHydrator};
// --- SETUP ---
@@ -193,6 +194,8 @@ class MyEntity
public $id;
}
+DomainIdType::setClass(DomainId::class);
+DomainIdType::setDataType(Type::INTEGER);
Type::addType(DomainIdType::NAME, DomainIdType::class);
/** @var EntityManagerInterface $em */
@@ -204,7 +207,7 @@ $config->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator
// --- USAGE ---
-$query = $em->createQuery('SELECT entity.id FORM MyEntity entity');
+$query = $em->createQuery('SELECT entity.id FROM MyEntity entity');
$query->getScalarResult()[0]['id']; // "1"
$query->getResult(ScalarHydrator::NAME)[0]['id']; // int(1)
diff --git a/docs/infrastructure/elasticsearch.md b/docs/infrastructure/elasticsearch.md
index 54824937..8e369fc4 100644
--- a/docs/infrastructure/elasticsearch.md
+++ b/docs/infrastructure/elasticsearch.md
@@ -4,9 +4,9 @@ An overview of available infrastructural code when using Elasticsearch's [PHP Ap
- Requires [elasticsearch/elasticsearch]
-## Domain Projection Type Registry
+## Projection Type Registry
-An Elasticsearch tailored [domain projection type registry](../projection/type-registry.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionTypeRegistry`.
+An Elasticsearch tailored [projection type registry](../projection/type-registry.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\ProjectionTypeRegistry`.
It works directly with any [`Client`][api-client] and a known configuration of type information.
- `__construct(Client $client, string $index, array $mappings, array $settings = [], LoggerInterface $logger = null)`
@@ -21,35 +21,79 @@ It works directly with any [`Client`][api-client] and a known configuration of t
someField = $document['some_field'] ?? null;
+ $projection->otherField = $document['other_field'] ?? null;
+
+ return $projection;
}
}
/** @var Client $client */
$client = ...;
-$typeRegistry = new DomainProjectionTypeRegistry($client, 'some_index', [
+$typeRegistry = new ProjectionTypeRegistry($client, 'some_index', [
MyProjection::class => [
- 'some_field' => null, // defaults to ['type' => 'text']
- 'other_field' => 'some', // defaults to ['type' => 'some']
- 'other_field2' => [
+ 'some_field' => 'some_type', // defaults to ['type' => 'some_type']
+ 'other_field' => [ // defaults to ['type' => 'text', ...]
// ...
],
],
]);
```
-## Domain Projection Repository
+### Advanced mapping example
+
+```php
+ [
+ 'some_field' => 'some_type',
+ 'other_field' => [
+ // ...
+ ],
+ ];
+ }
+}
+
+/** @var Client $client */
+$client = ...;
+$typeRegistry = new ProjectionTypeRegistry($client, 'some_index', [
+ MyProjection::class,
+]);
+```
+
+## Projection Repository
-An Elasticsearch tailored [domain projection repository](../projection/repositories.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionRepository`.
+An Elasticsearch tailored [projection repository](../projection/repositories.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\ProjectionRepository`.
It works directly with any [`Client`][api-client].
- `__construct(Client $client, string $index)`
@@ -62,13 +106,13 @@ It works directly with any [`Client`][api-client].
transform(new MyEntity());
-```
-
-### Manual container example
-
-In practice you often get a `$container` from configuration, e.g. when working with Symfony's [Service Locators]. This
-example shows how to manually create a container using PHP7 anonymous classes.
-
-```php
-has($id)) {
- throw new class('Entry not found') extends \RuntimeException implements NotFoundExceptionInterface {
- };
- }
-
- return function (MyEntity $object) {
- $document = DomainProjectionDocument::create(MyProjection::class, null, [
- 'some_field' => $object->someField,
- ]);
- $document->source = $object;
-
- return $document;
- };
- }
-};
-$transformer = new DomainProjectionDocumentTransformer($container);
-```
-
-[container-project]: https://www.php-fig.org/psr/psr-11/
-[psr/container]: https://packagist.org/packages/psr/container
-[api-container]: https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface
-[Service Locators]: https://symfony.com/doc/current/service_container/service_locators.html
-[anonymous classes]: https://secure.php.net/manual/en/language.oop5.anonymous.php
diff --git a/docs/infrastructure/simple-bus.md b/docs/infrastructure/simple-bus.md
new file mode 100644
index 00000000..f232e600
--- /dev/null
+++ b/docs/infrastructure/simple-bus.md
@@ -0,0 +1,35 @@
+# SimpleBus
+
+An overview of available infrastructural code when using [SimpleBus].
+
+- Requires [simple-bus/message-bus]
+
+## Domain Message Bus
+
+A SimpleBus tailored [domain message bus](../message-driven/message-bus.md) is provided by `MsgPhp\Domain\Infra\SimpleBus\DomainMessageBus`.
+It decorates any `SimpleBus\Message\Bus\MessageBus` type.
+
+- `__construct(MessageBus $bus)`
+ - `$bus`: The decorated bus
+
+### Basic example
+
+```php
+dispatch(new SomeMessage());
+```
+
+[SimpleBus]: http://docs.simplebus.io
+[simple-bus/message-bus]: https://packagist.org/packages/simple-bus/message-bus
diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md
index 1b4efe99..64579f5b 100644
--- a/docs/infrastructure/symfony-console.md
+++ b/docs/infrastructure/symfony-console.md
@@ -6,24 +6,24 @@ An overview of available infrastructural code when using [Symfony Console][conso
## Commands
-Various standard [console commands] are provided depending on used domain infrastructure. They are defined in the
-`MsgPhp\Domain\Infra\Console\Command\` namespace.
+Various standard [console commands] are available and can be used depending on implemented domain infrastructure. They
+are defined in the `MsgPhp\Domain\Infra\Console\Command\` namespace.
-### `InitializeDomainProjectionTypesCommand`
+### `InitializeProjectionTypesCommand`
-Initializes a [projection type registry](../projection/type-registry.md).
+Initializes the [projection type registry](../projection/type-registry.md).
```bash
-bin/console domain:projection:initialize-types [--force]
+bin/console projection:initialize-types [--force]
```
-### `SynchronizeDomainProjectionsCommand`
+### `SynchronizeProjectionsCommand`
Synchronizes domain objects and their [projections](../projection/models.md) using the [projection synchronization](../projection/synchronization.md)
utility service.
```bash
-bin/console domain:projection:synchronize
+bin/console projection:synchronize
```
## Context Factory
diff --git a/docs/infrastructure/symfony-messenger.md b/docs/infrastructure/symfony-messenger.md
new file mode 100644
index 00000000..23232db7
--- /dev/null
+++ b/docs/infrastructure/symfony-messenger.md
@@ -0,0 +1,35 @@
+# Symfony Messenger
+
+An overview of available infrastructural code when using [Symfony Messenger][messenger-project].
+
+- Requires [symfony/messenger]
+
+## Domain Message Bus
+
+A Symfony Messenger tailored [domain message bus](../message-driven/message-bus.md) is provided by `MsgPhp\Domain\Infra\Messenger\DomainMessageBus`.
+It decorates any `Symfony\Component\Messenger\MessageBusInterface` type.
+
+- `__construct(MessageBusInterface $bus)`
+ - `$bus`: The decorated bus
+
+### Basic example
+
+```php
+dispatch(new SomeMessage());
+```
+
+[messenger-project]: https://symfony.com/doc/current/components/messenger.html
+[symfony/messenger]: https://packagist.org/packages/symfony/messenger
diff --git a/docs/infrastructure/uuid.md b/docs/infrastructure/uuid.md
index 51aedf0e..5c66072e 100644
--- a/docs/infrastructure/uuid.md
+++ b/docs/infrastructure/uuid.md
@@ -13,7 +13,8 @@ leverages type `Ramsey\Uuid\UuidInterface` as underlying data type.
- `$uuid`: The underlying UUID. In case of `null` a UUID version 4 value is generated upfront. Meaning the
identifier will never be considered empty.
-See also the [reference](../reference/identifiers.md) page for all available UUID identifiers provided per domain.
+!!! note
+ See the [reference](../reference/identifiers.md) page for all available UUID identifiers provided per domain
### Basic example
diff --git a/docs/message-driven/cqrs.md b/docs/message-driven/cqrs.md
index 5cd06089..344f36d3 100644
--- a/docs/message-driven/cqrs.md
+++ b/docs/message-driven/cqrs.md
@@ -31,6 +31,24 @@ class MyCommand
{
}
+class MyDomainEvent implements DomainEventInterface
+{
+}
+
+class MyEntity implements DomainEventHandlerInterface
+{
+ public function handleEvent(DomainEventInterface $event): bool
+ {
+ if ($event instanceof MyDomainEvent) {
+ // do something
+
+ return true;
+ }
+
+ return false;
+ }
+}
+
class MyCommandHandler
{
use EventSourcingCommandHandlerTrait;
@@ -53,24 +71,6 @@ class MyCommandHandler
}
}
-class MyDomainEvent implements DomainEventInterface
-{
-}
-
-class MyEntity implements DomainEventHandlerInterface
-{
- public function handleEvent(DomainEventInterface $event): bool
- {
- if ($event instanceof MyDomainEvent) {
- // do something
-
- return true;
- }
-
- return false;
- }
-}
-
// --- USAGE ---
/** @var DomainMessageBusInterface $bus */
diff --git a/docs/message-driven/message-bus.md b/docs/message-driven/message-bus.md
index e164ecfb..6e911578 100644
--- a/docs/message-driven/message-bus.md
+++ b/docs/message-driven/message-bus.md
@@ -3,13 +3,25 @@
A domain message bus is bound to `MsgPhp\Domain\Message\DomainMessageBusInterface`. Its purpose is to dispatch any type
of message object and helps you to use [CQRS](cqrs.md) and [event sourcing](../event-sourcing/event-handlers.md).
+!!! note
+ See the [reference](../reference/messages.md) page for all available messages provided per domain
+
## API
-### `dispatch(object $message): mixed`
+### `dispatch(object $message): void`
-Dispatches the given message object. The bus can return a value coming from handlers, but is not required to do so.
+Dispatches the given message object.
## Implementations
-- `MsgPhp\Domain\Infra\SimpleBus\DomainMessageBus`
- - Requires [`simple-bus/message-bus`](https://packagist.org/packages/simple-bus/message-bus)
+### `MsgPhp\Domain\Infra\Messenger\DomainMessageBus`
+
+A Symfony Messenger tailored domain message bus.
+
+- [Read more](../infrastructure/symfony-messenger.md#domain-message-bus)
+
+### `MsgPhp\Domain\Infra\SimpleBus\DomainMessageBus`
+
+A SimpleBus tailored domain message bus.
+
+- [Read more](../infrastructure/simple-bus.md#domain-message-bus)
diff --git a/docs/message-driven/message-dispatcher.md b/docs/message-driven/message-dispatcher.md
index 67084e94..03187675 100644
--- a/docs/message-driven/message-dispatcher.md
+++ b/docs/message-driven/message-dispatcher.md
@@ -5,10 +5,9 @@ The domain message dispatcher is a utility trait. Its purpose is to dispatch a f
## API
-### `dispatch(string $class, array $context = []): mixed`
+### `dispatch(string $class, array $context = []): void`
-Dispatches a message object factorized from `$class` and `$context`. The dispatcher can return a value coming from
-handlers, but is not required to do so.
+Dispatches a message object factorized from `$class` and `$context`.
## Basic example
diff --git a/docs/projection/document-transformers.md b/docs/projection/document-transformers.md
index 537bc3be..904e72e3 100644
--- a/docs/projection/document-transformers.md
+++ b/docs/projection/document-transformers.md
@@ -1,18 +1,69 @@
# Projection Document Transformers
-A projection document transformer is bound to `MsgPhp\Domain\Projection\DomainProjectionDocumentTransformerInterface`.
-Its purpose is to transform domain objects into [projection documents](documents.md).
+A projection document transformer is bound to `MsgPhp\Domain\Projection\ProjectionDocumentTransformerInterface`. Its
+purpose is to transform arbitrary objects into [projection documents](documents.md).
## API
-### `transform(object $object): DomainProjectionDocument`
+### `transform(object $object): ProjectionDocument`
Transforms the domain object into a projection document.
-## Implementations
+## Basic example
-### `MsgPhp\Domain\Infra\Psr\DomainProjectionDocumentTransformer`
+```php
+id = $id;
+ }
+}
+
+class MyProjection implements ProjectionInterface
+{
+ public $id;
+ public $someField;
+
+ public static function fromDocument(array $document): ProjectionInterface
+ {
+ $projection = new static();
+ $projection->id = $document['id'] ?? null;
+ $projection->someField = $document['some_field'] ?? null;
+
+ return $projection;
+ }
+}
+
+class MyTransformer implements ProjectionDocumentTransformerInterface
+{
+ public function transform($object): ProjectionDocument
+ {
+ if ($object instanceof MyEntity) {
+ return new ProjectionDocument(MyProjection::class, $object->id, [
+ 'id' => $object->id,
+ 'some_field' => $object->someField,
+ ]);
+ }
+
+ throw new \LogicException();
+ }
+}
+
+$transformer = new MyTransformer();
+
+// --- USAGE ---
+
+$entity = new MyEntity(1);
+$document = $transformer->transform($entity);
+$projection = $document->toProjection();
+```
diff --git a/docs/projection/documents.md b/docs/projection/documents.md
index a8e6098a..4f445a01 100644
--- a/docs/projection/documents.md
+++ b/docs/projection/documents.md
@@ -1,21 +1,21 @@
# Projection Documents
-A projection document is a domain value object of type `MsgPhp\Domain\Projection\DomainProjectionDocument`. Its purpose
-is to hold a projection document its data and current state.
+A projection document is a value object of type `MsgPhp\Domain\Projection\ProjectionDocument`. Its purpose is to hold a
+projection document its data and current state.
## API
### Properties
- `int $status`: The current document status. See also [default statuses][api-statuses].
-- `?\Exception $error`: An occurred error, if any
-- `?object $source`: The origin object source, if any
+- `?\Throwable $error`: An occurred error, if any
+- `?object $source`: The source object, if any
---
### `getType(): string`
-Gets the projection type. Always must refer to a [projection](models.md) class name.
+Gets the projection type and refers to a [projection](models.md) class name.
---
@@ -27,11 +27,11 @@ Gets the document ID, if any. Otherwise an auto-generated value is implied.
### `getBody(): array`
-Gets the document body.
+Gets the document data.
---
-### `toProjection(): DomainProjectionInterface`
+### `toProjection(): ProjectionInterface`
Transforms the document into its projection model.
@@ -40,20 +40,19 @@ Transforms the document into its projection model.
```php
'value',
]);
@@ -61,4 +60,4 @@ $document = new DomainProjectionDocument(MyProjection::class, null, [
$projection = $document->toProjection();
```
-[api-statuses]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionDocument.html#page-content
+[api-statuses]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/ProjectionDocument.html#page-content
diff --git a/docs/projection/models.md b/docs/projection/models.md
index 3b630291..16a3f6ce 100644
--- a/docs/projection/models.md
+++ b/docs/projection/models.md
@@ -1,19 +1,20 @@
# Projections
-A domain projection is a model object and bound to `MsgPhp\Domain\Projection\DomainProjectionInterface`. Its purpose is
-to convert raw model data (a document) into a projection, enabling to be stored separate.
+A projection is a model object and bound to `MsgPhp\Domain\Projection\ProjectionInterface`. Its purpose is to
+convert raw model data (a document) into a projection.
The document is usually a transformation from a domain object (e.g. an entity) and therefor projections should be
considered read-only and disposable, as they can be re-created / synchronized at any time from a source of truth.
-A practical use case for domain projections are APIs, where each API resource is a so called projection from a
-corresponding entity. It enables decoupling and thus optimized API responses.
+A practical use case for projections are APIs, where each API resource is a so called projection from a corresponding
+entity. It enables decoupling and thus optimized API responses.
-For integration with [API Platform] see also the [domain projection data provider](../infrastructure/api-platform.md#domain-projection-data-provider).
+!!! info
+ For integration with [API Platform] see the [projection data provider](../infrastructure/api-platform.md#projection-data-provider)
## API
-### `static fromDocument(array $document): DomainProjectionInterface`
+### `static fromDocument(array $document): ProjectionInterface`
Creates a projection from raw document data.
@@ -22,17 +23,17 @@ Creates a projection from raw document data.
```php
someField = $document['some_field'] ?? null;
return $projection;
diff --git a/docs/projection/repositories.md b/docs/projection/repositories.md
index c4dd4c4c..8f3cb1ba 100644
--- a/docs/projection/repositories.md
+++ b/docs/projection/repositories.md
@@ -1,19 +1,19 @@
# Projection Repositories
-A domain projection repository is bound to `MsgPhp\Domain\Projection\DomainProjectionRepositoryInterface`. Its purpose
-is to store and query [projection documents](documents.md).
+A projection repository is bound to `MsgPhp\Domain\Projection\ProjectionRepositoryInterface`. Its purpose is to store
+and query [projection documents](documents.md).
## API
-### `findAll(string $type, int $offset = 0, int $limit = 0): iterable`
+### `findAll(string $type, int $offset = 0, int $limit = 0): ProjectionDocument[]`
-Finds all [projections](models.md) by type.
+Finds all projection documents by type.
---
-### `find(string $type, string $id): ?DomainProjectionInterface`
+### `find(string $type, string $id): ?ProjectionDocument`
-Finds a single [projection](models.md) by type and ID. In case its document cannot be found `null` should be returned.
+Finds a single projection document by type and ID. In case its document cannot be found `null` should be returned.
---
@@ -23,53 +23,51 @@ Deletes all projection documents by type.
---
-### `save(DomainProjectionDocument $document): void`
+### `save(ProjectionDocument $document): void`
Saves a projection document. The document will be available on any subsequent query.
---
-### `delete(DomainProjectionDocument $document): void`
+### `delete(string $type, string $id): void`
-Deletes a projection document. The document will be unavailable on any subsequent query.
+Deletes a projection document by type and ID. The document will be unavailable on any subsequent query.
## Implementations
-### `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionRepository`
+### `MsgPhp\Domain\Infra\Elasticsearch\ProjectionRepository`
An Elasticsearch tailored projection repository.
-- [Read more](../infrastructure/elasticsearch.md#domain-projection-repository)
+- [Read more](../infrastructure/elasticsearch.md#projection-repository)
## Basic example
```php
find(MyProjection::class, $id);
+$document = $repository->find(MyProjection::class, $id);
if (null === $projection) {
- $document = DomainProjectionDocument::create(MyProjection::class, $id, [
+ $document = ProjectionDocument::create(MyProjection::class, $id, [
'some_field' => 'value',
]);
$repository->save($document);
diff --git a/docs/projection/synchronization.md b/docs/projection/synchronization.md
index 05eca350..528f93db 100644
--- a/docs/projection/synchronization.md
+++ b/docs/projection/synchronization.md
@@ -1,30 +1,20 @@
# Projection Synchronization
-`MsgPhp\Domain\Projection\DomainProjectionSynchronization` is a utility domain service. Its purpose is to ease
-synchronizing provided [projections](models.md).
-
-So called _providers_ actually provide domain objects, which are in turn transformed to [documents](documents.md) using
-any [transformer](document-transformers.md). A document is then stored using any [repository](repositories.md).
+`MsgPhp\Domain\Projection\ProjectionSynchronization` is a utility domain service. Its purpose is to ease synchronizing
+[projection documents](documents.md) from source objects.
## API
### `synchronize(): iterable`
-Yields a projection document for each provided domain object regarding its state. The actual document status can be
-read from [`ProjectionDocument::$status`][api-projection-document-status].
-
-[api-projection-document-status]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionDocument.html#property_status
+Yields all projection documents attempted to be synchronized. The actual document status can be read from [`ProjectionDocument::$status`][api-projection-document-status].
## Basic example
```php
synchronize() as $document) {
- if (DomainProjectionDocument::STATUS_VALID === $document->status) {
+ if (ProjectionDocument::STATUS_SYNCHRONIZED === $document->status) {
echo 'Synchronized projection for '.get_class($document->source).' with ID '.$document->source->id.PHP_EOL;
continue;
}
@@ -72,4 +63,6 @@ foreach ($synchronization->synchronize() as $document) {
A synchronization can be ran using the CLI when working with Symfony Console.
-- [Read more](../infrastructure/symfony-console.md#synchronizedomainprojectionscommand)
+- [Read more](../infrastructure/symfony-console.md#synchronizeprojectionscommand)
+
+[api-projection-document-status]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/ProjectionDocument.html#property_status
diff --git a/docs/projection/type-registry.md b/docs/projection/type-registry.md
index 406430e8..a68ce32e 100644
--- a/docs/projection/type-registry.md
+++ b/docs/projection/type-registry.md
@@ -1,11 +1,11 @@
# Projection Type Registry
-A projection type registry is bound to `MsgPhp\Domain\Projection\DomainProjectionTypeRegistryInterface`.
-Its purpose is to manage all available [projection](models.md) type information.
+A projection type registry is bound to `MsgPhp\Domain\Projection\ProjectionTypeRegistryInterface`. Its purpose is to
+manage all available [projection](models.md) type information.
## API
-### `all(): array`
+### `all(): string[]`
Returns all available projection types for this registry.
@@ -24,22 +24,22 @@ Destroys the type registry and thus requires to be re-initialized after.
## Implementations
-### `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionTypeRegistry`
+### `MsgPhp\Domain\Infra\Elasticsearch\ProjectionTypeRegistry`
An Elasticsearch tailored projection type registry.
-- [Read more](../infrastructure/elasticsearch.md#domain-projection-type-registry)
+- [Read more](../infrastructure/elasticsearch.md#projection-type-registry)
## Basic example
```php
all());
The type registry can be initialized using the CLI when working with Symfony Console.
-- [Read more](../infrastructure/symfony-console.md#initializedomainprojectiontypescommand)
+- [Read more](../infrastructure/symfony-console.md#initializeprojectiontypescommand)
diff --git a/docs/reference/doctrine-identifier-types.md b/docs/reference/doctrine-identifier-types.md
index 581c7823..fb712afd 100644
--- a/docs/reference/doctrine-identifier-types.md
+++ b/docs/reference/doctrine-identifier-types.md
@@ -1,11 +1,26 @@
# Doctrine Identifier Types
-Reference of available [Doctrine identifier types](../infrastructure/doctrine-dbal.md#domain-identifier-type) across all
-domains.
+Reference of available [Doctrine identifier types](../infrastructure/doctrine-dbal.md#domain-identifier-type) per
+domain.
+
+
+## `msgphp/domain`
+
+Type name | Type class
+--- | ---
+`msgphp_domain_id` | `MsgPhp\Domain\Infra\Doctrine\DomainIdType`
+
+## `msgphp/eav`
Type name | Type class
--- | ---
-`msgphp_domain_id` | `MsgPhp\Domain\Infra\Doctrine\Type\DomainIdType`
`msgphp_attribute_id` | `MsgPhp\Eav\Infra\Doctrine\Type\AttributeIdType`
`msgphp_attribute_value_id` | `MsgPhp\Eav\Infra\Doctrine\Type\AttributeValueIdType`
+
+## `msgphp/user`
+
+Type name | Type class
+--- | ---
`msgphp_user_id` | `MsgPhp\User\Infra\Doctrine\Type\UserIdType`
+
+
diff --git a/docs/reference/domain-events.md b/docs/reference/domain-events.md
new file mode 100644
index 00000000..6f548025
--- /dev/null
+++ b/docs/reference/domain-events.md
@@ -0,0 +1,17 @@
+# Domain Events
+
+Reference of available [domain events](../event-sourcing/events.md) per domain.
+
+
+## `msgphp/domain`
+
+- `MsgPhp\Domain\Event\ConfirmEvent`
+- `MsgPhp\Domain\Event\DisableEvent`
+- `MsgPhp\Domain\Event\EnableEvent`
+
+## `msgphp/user`
+
+- `MsgPhp\User\Event\Domain\ChangeCredentialEvent`
+- `MsgPhp\User\Event\Domain\RequestPasswordEvent`
+
+
diff --git a/docs/reference/entities.md b/docs/reference/entities.md
index a24fc177..eb618685 100644
--- a/docs/reference/entities.md
+++ b/docs/reference/entities.md
@@ -1,52 +1,48 @@
-# Domain Entities
+# Entities
Reference of available [entities](../ddd/entities.md) per domain.
-## Base Domain
+
+## `msgphp/domain`
-### Entity Fields
+### Fields
- `MsgPhp\Domain\Entity\Fields\CreatedAtField`
- `MsgPhp\Domain\Entity\Fields\EnabledField`
- `MsgPhp\Domain\Entity\Fields\LastUpdatedAtField`
-### Entity Features
+### Features
- `MsgPhp\Domain\Entity\Features\CanBeConfirmed`
- `MsgPhp\Domain\Entity\Features\CanBeEnabled`
----
-
-## EAV Domain
+## `msgphp/eav`
### Entities
-Class | Abstract | Required
---- | --- | ---
-`MsgPhp\Eav\Entity\Attribute` | ✔ | ✔
-`MsgPhp\Eav\Entity\AttributeValue` | ✔ | ✔
+- `MsgPhp\Eav\Entity\Attribute`
+- `MsgPhp\Eav\Entity\AttributeValue`
-### Entity Fields
+### Fields
- `MsgPhp\Eav\Entity\Fields\AttributesField`
-- `MsgPhp\Eav\Entity\Fields\AttributeValueField`
----
+### Features
+
+- `MsgPhp\Eav\Entity\Features\EntityAttributeValue`
-## User Domain
+## `msgphp/user`
### Entities
-Class | Abstract | Required
---- | --- | ---
-`MsgPhp\User\Entity\Role` | ✔ | ✗
-`MsgPhp\User\Entity\User` | ✔ | ✔
-`MsgPhp\User\Entity\UserAttributeValue` | ✔ | ✗
-`MsgPhp\User\Entity\UserEmail` | ✔ | ✗
-`MsgPhp\User\Entity\Username` | ✗ | ✗
-`MsgPhp\User\Entity\UserRole` | ✔ | ✗
+- `MsgPhp\User\Entity\Role`
+- `MsgPhp\User\Entity\User`
+- `MsgPhp\User\Entity\UserAttributeValue`
+- `MsgPhp\User\Entity\UserEmail`
+- `MsgPhp\User\Entity\UserRole`
+- `MsgPhp\User\Entity\Username`
-### Entity Fields
+### Fields
- `MsgPhp\User\Entity\Fields\AttributeValuesField`
- `MsgPhp\User\Entity\Fields\EmailsField`
@@ -54,11 +50,7 @@ Class | Abstract | Required
- `MsgPhp\User\Entity\Fields\RolesField`
- `MsgPhp\User\Entity\Fields\UserField`
-### Entity Features
-
-- `MsgPhp\User\Entity\Features\ResettablePassword`
-
-#### Credential Types
+### Features
- `MsgPhp\User\Entity\Features\EmailCredential`
- `MsgPhp\User\Entity\Features\EmailPasswordCredential`
@@ -66,4 +58,7 @@ Class | Abstract | Required
- `MsgPhp\User\Entity\Features\NicknameCredential`
- `MsgPhp\User\Entity\Features\NicknamePasswordCredential`
- `MsgPhp\User\Entity\Features\NicknameSaltedPasswordCredential`
+- `MsgPhp\User\Entity\Features\ResettablePassword`
- `MsgPhp\User\Entity\Features\TokenCredential`
+
+
diff --git a/docs/reference/identifiers.md b/docs/reference/identifiers.md
index df3e3b53..97cfedf0 100644
--- a/docs/reference/identifiers.md
+++ b/docs/reference/identifiers.md
@@ -1,28 +1,40 @@
-# Domain Identifiers
+# Identifiers
Reference of available [identifiers](../ddd/identifiers.md) per domain.
-## Base Domain
+
+## `msgphp/domain`
-- `MsgPhp\Domain\DomainIdInterface`
- - Scalar types: `MsgPhp\Domain\DomainId`
- - UUID types: `MsgPhp\Domain\Infra\Uuid\DomainId`
+### `MsgPhp\Domain\DomainIdInterface`
----
+Primitive type | Implementation
+--- | ---
+Scalar | `MsgPhp\Domain\DomainId`
+UUID | `MsgPhp\Domain\Infra\Uuid\DomainId`
-## EAV Domain
+## `msgphp/eav`
-- `MsgPhp\Eav\AttributeIdInterface`
- - Scalar types: `MsgPhp\Eav\AttributeId`
- - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeId`
-- `MsgPhp\Eav\AttributeValueIdInterface`
- - Scalar types: `MsgPhp\Eav\AttributeValueId`
- - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeValueId`
+### `MsgPhp\Eav\AttributeIdInterface`
----
+Primitive type | Implementation
+--- | ---
+Scalar | `MsgPhp\Eav\AttributeId`
+UUID | `MsgPhp\Eav\Infra\Uuid\AttributeId`
-## User Domain
+### `MsgPhp\Eav\AttributeValueIdInterface`
-- `MsgPhp\User\UserIdInterface`
- - Scalar types: `MsgPhp\User\UserId`
- - UUID types: `MsgPhp\User\Infra\Uuid\UserId`
+Primitive type | Implementation
+--- | ---
+Scalar | `MsgPhp\Eav\AttributeValueId`
+UUID | `MsgPhp\Eav\Infra\Uuid\AttributeValueId`
+
+## `msgphp/user`
+
+### `MsgPhp\User\UserIdInterface`
+
+Primitive type | Implementation
+--- | ---
+Scalar | `MsgPhp\User\UserId`
+UUID | `MsgPhp\User\Infra\Uuid\UserId`
+
+
diff --git a/docs/reference/messages.md b/docs/reference/messages.md
new file mode 100644
index 00000000..f23d3b61
--- /dev/null
+++ b/docs/reference/messages.md
@@ -0,0 +1,72 @@
+# Messages
+
+Reference of available messages per domain that can be dispatched using a [message bus](../message-driven/message-bus.md).
+
+
+## `msgphp/domain`
+
+### Commands
+
+- `MsgPhp\Domain\Command\DeleteProjectionDocumentCommand`
+- `MsgPhp\Domain\Command\SaveProjectionDocumentCommand`
+
+### Events
+
+- `MsgPhp\Domain\Event\ProjectionDocumentDeletedEvent`
+- `MsgPhp\Domain\Event\ProjectionDocumentSavedEvent`
+
+## `msgphp/eav`
+
+### Commands
+
+- `MsgPhp\Eav\Command\CreateAttributeCommand`
+- `MsgPhp\Eav\Command\DeleteAttributeCommand`
+
+### Events
+
+- `MsgPhp\Eav\Event\AttributeCreatedEvent`
+- `MsgPhp\Eav\Event\AttributeDeletedEvent`
+
+## `msgphp/user`
+
+### Commands
+
+- `MsgPhp\User\Command\AddUserAttributeValueCommand`
+- `MsgPhp\User\Command\AddUserEmailCommand`
+- `MsgPhp\User\Command\AddUserRoleCommand`
+- `MsgPhp\User\Command\ChangeUserAttributeValueCommand`
+- `MsgPhp\User\Command\ChangeUserCredentialCommand`
+- `MsgPhp\User\Command\ConfirmUserCommand`
+- `MsgPhp\User\Command\ConfirmUserEmailCommand`
+- `MsgPhp\User\Command\CreateRoleCommand`
+- `MsgPhp\User\Command\CreateUserCommand`
+- `MsgPhp\User\Command\DeleteRoleCommand`
+- `MsgPhp\User\Command\DeleteUserAttributeValueCommand`
+- `MsgPhp\User\Command\DeleteUserCommand`
+- `MsgPhp\User\Command\DeleteUserEmailCommand`
+- `MsgPhp\User\Command\DeleteUserRoleCommand`
+- `MsgPhp\User\Command\DisableUserCommand`
+- `MsgPhp\User\Command\EnableUserCommand`
+- `MsgPhp\User\Command\RequestUserPasswordCommand`
+
+### Events
+
+- `MsgPhp\User\Event\RoleCreatedEvent`
+- `MsgPhp\User\Event\RoleDeletedEvent`
+- `MsgPhp\User\Event\UserAttributeValueAddedEvent`
+- `MsgPhp\User\Event\UserAttributeValueChangedEvent`
+- `MsgPhp\User\Event\UserAttributeValueDeletedEvent`
+- `MsgPhp\User\Event\UserConfirmedEvent`
+- `MsgPhp\User\Event\UserCreatedEvent`
+- `MsgPhp\User\Event\UserCredentialChangedEvent`
+- `MsgPhp\User\Event\UserDeletedEvent`
+- `MsgPhp\User\Event\UserDisabledEvent`
+- `MsgPhp\User\Event\UserEmailAddedEvent`
+- `MsgPhp\User\Event\UserEmailConfirmedEvent`
+- `MsgPhp\User\Event\UserEmailDeletedEvent`
+- `MsgPhp\User\Event\UserEnabledEvent`
+- `MsgPhp\User\Event\UserPasswordRequestedEvent`
+- `MsgPhp\User\Event\UserRoleAddedEvent`
+- `MsgPhp\User\Event\UserRoleDeletedEvent`
+
+
diff --git a/docs/reference/repositories.md b/docs/reference/repositories.md
new file mode 100644
index 00000000..a57917fb
--- /dev/null
+++ b/docs/reference/repositories.md
@@ -0,0 +1,19 @@
+# Repositories
+
+Reference of available [repositories](../ddd/repositories.md) per domain.
+
+
+## `msgphp/eav`
+
+- `MsgPhp\Eav\Repository\AttributeRepositoryInterface`
+
+## `msgphp/user`
+
+- `MsgPhp\User\Repository\RoleRepositoryInterface`
+- `MsgPhp\User\Repository\UserAttributeValueRepositoryInterface`
+- `MsgPhp\User\Repository\UserEmailRepositoryInterface`
+- `MsgPhp\User\Repository\UserRepositoryInterface`
+- `MsgPhp\User\Repository\UserRoleRepositoryInterface`
+- `MsgPhp\User\Repository\UsernameRepositoryInterface`
+
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 62435617..ed2b7a69 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,9 +8,9 @@ theme:
name: material
custom_dir: docs/.theme
extra_css: [style.css]
-
markdown_extensions:
- - codehilite
+ - codehilite # https://squidfunk.github.io/mkdocs-material/extensions/codehilite/
+ - admonition # https://squidfunk.github.io/mkdocs-material/extensions/admonition/
pages:
- Home: index.md
@@ -24,8 +24,8 @@ pages:
- Object Factory: ddd/factory/object.md
- Entity Aware Factory: ddd/factory/entity-aware.md
- Static Factory: ddd/factory/static.md
- - Repositories: ddd/repositories.md
- Entities: ddd/entities.md
+ - Repositories: ddd/repositories.md
- Event-Sourcing:
- Events: event-sourcing/events.md
- Event Handlers: event-sourcing/event-handlers.md
@@ -46,12 +46,18 @@ pages:
- Doctrine DBAL: infrastructure/doctrine-dbal.md
- Doctrine ORM: infrastructure/doctrine-orm.md
- Elasticsearch: infrastructure/elasticsearch.md
- - PSR Container: infrastructure/psr-container.md
+ - SimpleBus: infrastructure/simple-bus.md
- Symfony Console: infrastructure/symfony-console.md
+ - Symfony Messenger: infrastructure/symfony-messenger.md
- UUID: infrastructure/uuid.md
- Cookbook:
- Bundle Installation: cookbook/bundle-installation.md
+ - Configuring Doctrine ORM: cookbook/configuring-doctrine-orm.md
+ - Configuring A Message Bus: cookbook/configuring-a-message-bus.md
- Reference:
- - Domain Identifiers: reference/identifiers.md
- - Domain Entities: reference/entities.md
+ - Identifiers: reference/identifiers.md
+ - Entities: reference/entities.md
+ - Repositories: reference/repositories.md
+ - Messages: reference/messages.md
+ - Domain Events: reference/domain-events.md
- Doctrine Identifier Types: reference/doctrine-identifier-types.md
diff --git a/phpstan.neon b/phpstan.neon
index 7a6e23ab..c4dad375 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,25 +1,33 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- - vendor/phpstan/phpstan-phpunit/strictRules.neon
parameters:
+ level: max
+
+ autoload_directories:
+ - tests/fixtures/sa
+
ignoreErrors:
- '#Constructor of class MsgPhp\\Domain\\Tests\\Factory\\KnownTestObject has an unused parameter \$arg\.#'
- '#Parameter \$arg of method MsgPhp\\Domain\\Tests\\Factory\\KnownTestObject::__construct\(\) has invalid typehint type MsgPhp\\Domain\\Tests\\Factory\\UnknownTestObject\.#'
- '#Static property MsgPhp\\Domain\\Tests\\Infra\\InMemory\\DomainEntityRepositoryTraitTest::\$memory \(MsgPhp\\Domain\\Infra\\InMemory\\GlobalObjectMemory\) does not accept null\.#'
+ - '#Static property MsgPhp\\Domain\\Tests\\Infra\\Doctrine\\.+Test::\$em \(Doctrine\\ORM\\EntityManager\) does not accept null\.#'
- '#Class MsgPhp\\Domain\\Tests\\Factory\\WrongCase referenced with incorrect case: MsgPhp\\Domain\\Tests\\Factory\\wrongcase\.#'
- # see https://github.com/symfony/symfony/pull/26297
- - '#Parameter \#1 \$name of method Symfony\\Component\\Config\\Definition\\Builder\\NodeBuilder::node\(\) expects string, null given\.#'
+ # phpstan
+ - '#Cannot call method getName\(\) on ReflectionClass\|null\.#'
- # forward compatibility
- - '#Class MsgPhp\\Domain\\Infra\\Config\\ClassMappingNode constructor invoked with 3 parameters, 1\-2 required\.#'
- - '#Access to an undefined property MsgPhp\\Domain\\Infra\\Config\\ClassMappingNodeDefinition::\$pathSeparator#'
+ # doctrine
+ - '#Parameter \#1 \$hydrationMode of method Doctrine\\ORM\\AbstractQuery::getResult\(\) expects int, string given\.#'
+ - '#Parameter \#1 \$hydrationMode of method Doctrine\\ORM\\AbstractQuery::getSingleResult\(\) expects int\|null, string given\.#'
- # symfony fluent config
- - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::classMappingNode\(\)\.#'
- - '#Calling method classMappingNode\(\) on possibly null value of type Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\|null\.#'
+ # msgphp
+ - '#Method MsgPhp\\.+\\Infra\\Doctrine\\Repository\\.+Repository::find.*\(\) should return MsgPhp\\.+\\Entity\\.+ but returns object\.#'
+ - '#Parameter \#1 \$command of method MsgPhp\\.+\\Command\\Handler\\.+Handler::getDomainEvent(Handler)?\(\) expects MsgPhp\\.+\\Command\\.+Command, object given\.#'
+ - '#Cannot call method classMappingNode\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\|null\.#'
excludes_analyse:
- '*/vendor/*'
+ - src/UserBundle/Resources/skeleton/
+ - src/UserBundle/Maker/UserMaker.php
diff --git a/src/Domain/Command/DeleteProjectionDocumentCommand.php b/src/Domain/Command/DeleteProjectionDocumentCommand.php
new file mode 100644
index 00000000..39b71d58
--- /dev/null
+++ b/src/Domain/Command/DeleteProjectionDocumentCommand.php
@@ -0,0 +1,20 @@
+
+ */
+class DeleteProjectionDocumentCommand
+{
+ public $type;
+ public $id;
+
+ final public function __construct(string $type, string $id)
+ {
+ $this->type = $type;
+ $this->id = $id;
+ }
+}
diff --git a/src/Domain/Command/EventSourcingCommandHandlerTrait.php b/src/Domain/Command/EventSourcingCommandHandlerTrait.php
index d524d0a5..fd004618 100644
--- a/src/Domain/Command/EventSourcingCommandHandlerTrait.php
+++ b/src/Domain/Command/EventSourcingCommandHandlerTrait.php
@@ -20,7 +20,7 @@ private function handle($command, callable $onHandled = null): void
$handler = $this->getDomainEventHandler($command);
if (!$handler instanceof DomainEventHandlerInterface) {
- throw new \LogicException(sprintf('Object "%s" is unable to handle domain event "%s". Did you forgot to implement DomainEventHandlerInterface?', get_class($handler), get_class($event)));
+ throw new \LogicException(sprintf('Object "%s" is unable to handle domain event "%s". Did you forgot to implement DomainEventHandlerInterface?', \get_class($handler), \get_class($event)));
}
if ($handler->handleEvent($event) && null !== $onHandled) {
diff --git a/src/Domain/Command/Handler/DeleteProjectionDocumentHandler.php b/src/Domain/Command/Handler/DeleteProjectionDocumentHandler.php
new file mode 100644
index 00000000..900897f4
--- /dev/null
+++ b/src/Domain/Command/Handler/DeleteProjectionDocumentHandler.php
@@ -0,0 +1,38 @@
+
+ */
+final class DeleteProjectionDocumentHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus, ProjectionRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(DeleteProjectionDocumentCommand $command): void
+ {
+ if (null === $document = $this->repository->find($command->type, $command->id)) {
+ return;
+ }
+
+ $this->repository->delete($document->getType(), $document->getId());
+ $this->dispatch(ProjectionDocumentDeletedEvent::class, [$document]);
+ }
+}
diff --git a/src/Domain/Command/Handler/SaveProjectionDocumentHandler.php b/src/Domain/Command/Handler/SaveProjectionDocumentHandler.php
new file mode 100644
index 00000000..969b5cb7
--- /dev/null
+++ b/src/Domain/Command/Handler/SaveProjectionDocumentHandler.php
@@ -0,0 +1,36 @@
+
+ */
+final class SaveProjectionDocumentHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus, ProjectionRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(SaveProjectionDocumentCommand $command): void
+ {
+ $document = new ProjectionDocument($command->type, $command->id, $command->body);
+
+ $this->repository->save($document);
+ $this->dispatch(ProjectionDocumentSavedEvent::class, [$document]);
+ }
+}
diff --git a/src/Domain/Command/SaveProjectionDocumentCommand.php b/src/Domain/Command/SaveProjectionDocumentCommand.php
new file mode 100644
index 00000000..61291094
--- /dev/null
+++ b/src/Domain/Command/SaveProjectionDocumentCommand.php
@@ -0,0 +1,24 @@
+
+ */
+class SaveProjectionDocumentCommand
+{
+ public $type;
+ public $id;
+ public $body;
+
+ final public function __construct(ProjectionDocument $document)
+ {
+ $this->type = $document->getType();
+ $this->id = $document->getId();
+ $this->body = $document->getBody();
+ }
+}
diff --git a/src/Domain/DomainCollection.php b/src/Domain/DomainCollection.php
index 1de02b1f..b894837c 100644
--- a/src/Domain/DomainCollection.php
+++ b/src/Domain/DomainCollection.php
@@ -11,9 +11,6 @@ final class DomainCollection implements DomainCollectionInterface
{
private $elements;
- /**
- * @return $this|self
- */
public static function fromValue(?iterable $value): DomainCollectionInterface
{
return new self($value ?? []);
@@ -62,7 +59,7 @@ public function contains($element): bool
return false;
}
- return in_array($element, $this->elements, true);
+ return \in_array($element, $this->elements, true);
}
public function containsKey($key): bool
@@ -157,7 +154,7 @@ public function slice(int $offset, int $limit = 0): DomainCollectionInterface
return new self($elements);
}
- return new self(array_slice($this->elements, $offset, $limit ?: null, true));
+ return new self(\array_slice($this->elements, $offset, $limit ?: null, true));
}
public function map(callable $mapper): array
@@ -176,6 +173,6 @@ public function map(callable $mapper): array
public function count(): int
{
- return $this->elements instanceof \Traversable ? iterator_count($this->elements) : count($this->elements);
+ return $this->elements instanceof \Traversable ? iterator_count($this->elements) : \count($this->elements);
}
}
diff --git a/src/Domain/DomainId.php b/src/Domain/DomainId.php
index 8796a76e..7a718601 100644
--- a/src/Domain/DomainId.php
+++ b/src/Domain/DomainId.php
@@ -12,11 +12,11 @@ class DomainId implements DomainIdInterface
private $id;
/**
- * @return $this|self
+ * @return static
*/
final public static function fromValue($value): DomainIdInterface
{
- if (null !== $value && !is_string($value)) {
+ if (null !== $value && !\is_string($value)) {
$value = (string) $value;
}
@@ -35,7 +35,7 @@ final public function isEmpty(): bool
final public function equals(DomainIdInterface $id): bool
{
- return $id === $this ? true : (null !== $this->id && $id instanceof self && static::class === get_class($id) ? $this->id === $id->id : false);
+ return $id === $this ? true : (null !== $this->id && $id instanceof self && static::class === \get_class($id) ? $this->id === $id->id : false);
}
final public function toString(): string
diff --git a/src/Domain/DomainIdentityHelper.php b/src/Domain/DomainIdentityHelper.php
index 421adc8a..eeb0d7fd 100644
--- a/src/Domain/DomainIdentityHelper.php
+++ b/src/Domain/DomainIdentityHelper.php
@@ -24,9 +24,9 @@ public function isIdentifier($value): bool
return true;
}
- if (is_object($value)) {
+ if (\is_object($value)) {
try {
- $this->mapping->getIdentifierFieldNames(get_class($value));
+ $this->mapping->getIdentifierFieldNames(\get_class($value));
return true;
} catch (InvalidClassException $e) {
@@ -46,14 +46,14 @@ public function isEmptyIdentifier($value): bool
return $value->isEmpty();
}
- if (is_object($value)) {
+ if (\is_object($value)) {
try {
$identity = $this->mapping->getIdentity($value);
} catch (InvalidClassException $e) {
return false;
}
- return !$this->isIdentity(get_class($value), $identity);
+ return !$this->isIdentity(\get_class($value), $identity);
}
return false;
@@ -65,7 +65,7 @@ public function normalizeIdentifier($value)
return $value->isEmpty() ? null : $value->toString();
}
- if (is_object($value)) {
+ if (\is_object($value)) {
try {
$identity = $this->mapping->getIdentity($value);
} catch (InvalidClassException $e) {
@@ -76,7 +76,7 @@ public function normalizeIdentifier($value)
return $this->normalizeIdentifier($id);
}, $identity);
- return 1 === count($this->mapping->getIdentifierFieldNames(get_class($value))) ? reset($identity) : $identity;
+ return 1 === \count($this->mapping->getIdentifierFieldNames(\get_class($value))) ? reset($identity) : $identity;
}
return $value;
@@ -105,10 +105,10 @@ public function isIdentity(string $class, $value): bool
}
$fields = $this->mapping->getIdentifierFieldNames($class);
- $count = count($fields);
+ $count = \count($fields);
- if (is_array($value)) {
- if (count($value) !== $count) {
+ if (\is_array($value)) {
+ if (\count($value) !== $count) {
return false;
}
@@ -126,7 +126,7 @@ public function isIdentity(string $class, $value): bool
public function toIdentity(string $class, $value): array
{
- if (is_array($value)) {
+ if (\is_array($value)) {
return $value;
}
diff --git a/src/Domain/Event/DomainEventHandlerTrait.php b/src/Domain/Event/DomainEventHandlerTrait.php
index 51777ba7..edaf7abc 100644
--- a/src/Domain/Event/DomainEventHandlerTrait.php
+++ b/src/Domain/Event/DomainEventHandlerTrait.php
@@ -13,7 +13,7 @@ trait DomainEventHandlerTrait
{
public function handleEvent(DomainEventInterface $event): bool
{
- $method = 'handle'.(false === ($pos = strrpos($class = get_class($event), '\\')) ? $class : substr($class, $pos + 1));
+ $method = 'handle'.(false === ($pos = strrpos($class = \get_class($event), '\\')) ? $class : substr($class, $pos + 1));
if ('Event' !== substr($method, -5)) {
$method .= 'Event';
}
diff --git a/src/Domain/Event/ProjectionDocumentDeletedEvent.php b/src/Domain/Event/ProjectionDocumentDeletedEvent.php
new file mode 100644
index 00000000..06ee4bfa
--- /dev/null
+++ b/src/Domain/Event/ProjectionDocumentDeletedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class ProjectionDocumentDeletedEvent
+{
+ public $document;
+
+ final public function __construct(ProjectionDocument $document)
+ {
+ $this->document = $document;
+ }
+}
diff --git a/src/Domain/Event/ProjectionDocumentSavedEvent.php b/src/Domain/Event/ProjectionDocumentSavedEvent.php
new file mode 100644
index 00000000..a7913210
--- /dev/null
+++ b/src/Domain/Event/ProjectionDocumentSavedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class ProjectionDocumentSavedEvent
+{
+ public $document;
+
+ final public function __construct(ProjectionDocument $document)
+ {
+ $this->document = $document;
+ }
+}
diff --git a/src/Domain/Exception/DuplicateEntityException.php b/src/Domain/Exception/DuplicateEntityException.php
index 7a291617..834ec44f 100644
--- a/src/Domain/Exception/DuplicateEntityException.php
+++ b/src/Domain/Exception/DuplicateEntityException.php
@@ -11,6 +11,6 @@ final class DuplicateEntityException extends \RuntimeException implements Domain
{
public static function createForId(string $class, $id): self
{
- return new self(sprintf('Entity "%s" with identifier %s cannot be duplicated.', $class, json_encode($id)));
+ return new self(sprintf('Entity "%s" with identifier %s cannot be duplicated.', $class, (string) json_encode($id)));
}
}
diff --git a/src/Domain/Exception/EntityNotFoundException.php b/src/Domain/Exception/EntityNotFoundException.php
index 4fa0d44a..1caba702 100644
--- a/src/Domain/Exception/EntityNotFoundException.php
+++ b/src/Domain/Exception/EntityNotFoundException.php
@@ -11,11 +11,11 @@ final class EntityNotFoundException extends \RuntimeException implements DomainE
{
public static function createForId(string $class, $id): self
{
- return new self(sprintf('Entity "%s" with identifier %s cannot be found.', $class, json_encode($id)));
+ return new self(sprintf('Entity "%s" with identifier %s cannot be found.', $class, (string) json_encode($id)));
}
public static function createForFields(string $class, array $fields): self
{
- return new self(sprintf('Entity "%s" with fields matching %s cannot be found.', $class, json_encode($fields)));
+ return new self(sprintf('Entity "%s" with fields matching %s cannot be found.', $class, (string) json_encode($fields)));
}
}
diff --git a/src/Domain/Exception/UnknownDomainEventException.php b/src/Domain/Exception/UnknownDomainEventException.php
index 01ca19d1..8cc218ad 100644
--- a/src/Domain/Exception/UnknownDomainEventException.php
+++ b/src/Domain/Exception/UnknownDomainEventException.php
@@ -13,6 +13,6 @@ final class UnknownDomainEventException extends \RuntimeException implements Dom
{
public static function createForHandler(DomainEventHandlerInterface $handler, DomainEventInterface $event): self
{
- return new self(sprintf('Domain event "%s" cannot be handled by "%s".', get_class($event), get_class($handler)));
+ return new self(sprintf('Domain event "%s" cannot be handled by "%s".', \get_class($event), \get_class($handler)));
}
}
diff --git a/src/Domain/Factory/ClassMethodResolver.php b/src/Domain/Factory/ClassMethodResolver.php
index fbedb0c1..b7409aeb 100644
--- a/src/Domain/Factory/ClassMethodResolver.php
+++ b/src/Domain/Factory/ClassMethodResolver.php
@@ -62,7 +62,7 @@ public static function resolve(string $class, string $method): array
return [
'name' => $name = $param->getName(),
- 'key' => strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)),
+ 'key' => strtolower((string) preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)),
'required' => $required,
'default' => $default,
'type' => $type,
diff --git a/src/Domain/Factory/DomainCollectionFactory.php b/src/Domain/Factory/DomainCollectionFactory.php
index ad330a88..877b7db1 100644
--- a/src/Domain/Factory/DomainCollectionFactory.php
+++ b/src/Domain/Factory/DomainCollectionFactory.php
@@ -15,6 +15,10 @@ final class DomainCollectionFactory
{
public static function create(?iterable $value): DomainCollectionInterface
{
+ if ($value instanceof DomainCollectionInterface) {
+ return $value;
+ }
+
if ($value instanceof Collection) {
return new DoctrineInfra\DomainCollection($value);
}
@@ -22,6 +26,27 @@ public static function create(?iterable $value): DomainCollectionInterface
return DomainCollection::fromValue($value);
}
+ public static function createFromCallable(callable $value): DomainCollectionInterface
+ {
+ $iterator = new class($value) implements \IteratorAggregate {
+ private $callback;
+
+ public function __construct(callable $value)
+ {
+ $this->callback = $value;
+ }
+
+ public function getIterator(): \Traversable
+ {
+ $iterable = ($this->callback)();
+
+ return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
+ }
+ };
+
+ return self::create($iterator);
+ }
+
private function __construct()
{
}
diff --git a/src/Domain/Factory/DomainIdFactory.php b/src/Domain/Factory/DomainIdFactory.php
index a72299cf..a0ebb84d 100644
--- a/src/Domain/Factory/DomainIdFactory.php
+++ b/src/Domain/Factory/DomainIdFactory.php
@@ -16,6 +16,10 @@ final class DomainIdFactory
{
public static function create($value): DomainIdInterface
{
+ if ($value instanceof DomainIdInterface) {
+ return $value;
+ }
+
if ($value instanceof UuidInterface) {
return new UuidInfra\DomainId($value);
}
diff --git a/src/Domain/Factory/DomainObjectFactory.php b/src/Domain/Factory/DomainObjectFactory.php
index be28adad..dfe94206 100644
--- a/src/Domain/Factory/DomainObjectFactory.php
+++ b/src/Domain/Factory/DomainObjectFactory.php
@@ -51,7 +51,7 @@ private function resolveArguments(string $class, string $method, array $context)
throw new \LogicException(sprintf('No value available for argument $%s in class method "%s::%s()".', $name, $class, $method));
}
- if ($given && isset($argument['type']) && (interface_exists($argument['type']) || class_exists($argument['type'])) && !is_object($value)) {
+ if ($given && isset($argument['type']) && (interface_exists($argument['type']) || class_exists($argument['type'])) && !\is_object($value)) {
try {
$arguments[] = ($this->factory ?? $this)->create($argument['type'], (array) $value);
diff --git a/src/Domain/Factory/EntityAwareFactory.php b/src/Domain/Factory/EntityAwareFactory.php
index 09189208..ad30816d 100644
--- a/src/Domain/Factory/EntityAwareFactory.php
+++ b/src/Domain/Factory/EntityAwareFactory.php
@@ -32,7 +32,7 @@ public function reference(string $class, $id)
{
$idFields = $this->identityMapping->getIdentifierFieldNames($class);
- if (!is_array($id)) {
+ if (!\is_array($id)) {
$id = [array_shift($idFields) => $id];
}
diff --git a/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php b/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php
deleted file mode 100644
index 061f43ec..00000000
--- a/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php
+++ /dev/null
@@ -1,43 +0,0 @@
-
- */
-final class DomainProjectionDataProvider implements CollectionDataProviderInterface, ItemDataProviderInterface, RestrictedDataProviderInterface
-{
- private $typeRegistry;
- private $repository;
-
- public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry, DomainProjectionRepositoryInterface $repository)
- {
- $this->typeRegistry = $typeRegistry;
- $this->repository = $repository;
- }
-
- public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
- {
- return in_array($resourceClass, $this->typeRegistry->all(), true);
- }
-
- /**
- * @return DomainProjectionInterface[]
- */
- public function getCollection(string $resourceClass, string $operationName = null): iterable
- {
- return $this->repository->findAll($resourceClass);
- }
-
- public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?DomainProjectionInterface
- {
- return $this->repository->find($resourceClass, $id);
- }
-}
diff --git a/src/Domain/Infra/ApiPlatform/Paginator.php b/src/Domain/Infra/ApiPlatform/Paginator.php
new file mode 100644
index 00000000..8c811a8a
--- /dev/null
+++ b/src/Domain/Infra/ApiPlatform/Paginator.php
@@ -0,0 +1,51 @@
+
+ */
+final class Paginator implements \IteratorAggregate, PaginatorInterface
+{
+ private $collection;
+
+ public function __construct(PaginatedDomainCollectionInterface $collection)
+ {
+ $this->collection = $collection;
+ }
+
+ public function getIterator(): \Traversable
+ {
+ return $this->collection->getIterator();
+ }
+
+ public function count(): int
+ {
+ return \count($this->collection);
+ }
+
+ public function getLastPage(): float
+ {
+ return $this->collection->getLastPage();
+ }
+
+ public function getTotalItems(): float
+ {
+ return $this->collection->getTotalCount();
+ }
+
+ public function getCurrentPage(): float
+ {
+ return $this->collection->getCurrentPage();
+ }
+
+ public function getItemsPerPage(): float
+ {
+ return $this->collection->getLimit();
+ }
+}
diff --git a/src/Domain/Infra/ApiPlatform/ProjectionDataProvider.php b/src/Domain/Infra/ApiPlatform/ProjectionDataProvider.php
new file mode 100644
index 00000000..d685de20
--- /dev/null
+++ b/src/Domain/Infra/ApiPlatform/ProjectionDataProvider.php
@@ -0,0 +1,52 @@
+
+ */
+final class ProjectionDataProvider implements CollectionDataProviderInterface, ItemDataProviderInterface, RestrictedDataProviderInterface
+{
+ private $typeRegistry;
+ private $repository;
+
+ public function __construct(ProjectionTypeRegistryInterface $typeRegistry, ProjectionRepositoryInterface $repository)
+ {
+ $this->typeRegistry = $typeRegistry;
+ $this->repository = $repository;
+ }
+
+ public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
+ {
+ return \in_array($resourceClass, $this->typeRegistry->all(), true);
+ }
+
+ /**
+ * @return ProjectionInterface[]
+ */
+ public function getCollection(string $resourceClass, string $operationName = null): iterable
+ {
+ $collection = $this->repository->findAll($resourceClass);
+
+ return new Paginator(new PaginatedDomainCollection((function () use ($collection): iterable {
+ foreach ($collection as $document) {
+ yield $document->toProjection();
+ }
+ })(), $collection->getOffset(), $collection->getLimit(), (float) \count($collection), $collection->getTotalCount()));
+ }
+
+ public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?ProjectionInterface
+ {
+ $document = $this->repository->find($resourceClass, $id);
+
+ return null === $document ? null : $document->toProjection();
+ }
+}
diff --git a/src/Domain/Infra/Config/ClassMappingNode.php b/src/Domain/Infra/Config/ClassMappingNode.php
index 9c2dbd25..9561310d 100644
--- a/src/Domain/Infra/Config/ClassMappingNode.php
+++ b/src/Domain/Infra/Config/ClassMappingNode.php
@@ -21,8 +21,8 @@ protected function validateType($value): void
{
parent::validateType($value);
- if (!is_array($value)) {
- throw new \UnexpectedValueException(sprintf('Expected configuration value to be type array, got "%s".', gettype($value)));
+ if (!\is_array($value)) {
+ throw new \UnexpectedValueException(sprintf('Expected configuration value to be type array, got "%s".', \gettype($value)));
}
foreach ($value as $k => $v) {
diff --git a/src/Domain/Infra/Config/ClassMappingNodeDefinition.php b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php
index 4cb3f6e1..50bd3ac8 100644
--- a/src/Domain/Infra/Config/ClassMappingNodeDefinition.php
+++ b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php
@@ -81,8 +81,8 @@ public function subClassValues(): self
{
$this->validate()->always(function (array $value): array {
foreach ($value as $class => $mappedClass) {
- if (!is_string($mappedClass)) {
- throw new \LogicException(sprintf('Class "%s" must be configured to a mapped sub class value, got type "%s".', $class, gettype($mappedClass)));
+ if (!\is_string($mappedClass)) {
+ throw new \LogicException(sprintf('Class "%s" must be configured to a mapped sub class value, got type "%s".', $class, \gettype($mappedClass)));
}
if (!is_subclass_of($mappedClass, $class)) {
throw new \LogicException(sprintf('Class "%s" must be configured to a mapped sub class value, got "%s".', $class, $mappedClass));
@@ -166,7 +166,7 @@ protected function createNode(): NodeInterface
$prototypedNode = $prototype->getNode();
if (!$prototypedNode instanceof PrototypeNodeInterface) {
- throw new \LogicException(sprintf('Prototyped node must be an instance of "%s", got "%s".', PrototypeNodeInterface::class, get_class($prototypedNode)));
+ throw new \LogicException(sprintf('Prototyped node must be an instance of "%s", got "%s".', PrototypeNodeInterface::class, \get_class($prototypedNode)));
}
$node->setPrototype($prototypedNode);
diff --git a/src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php b/src/Domain/Infra/Console/Command/InitializeProjectionTypesCommand.php
similarity index 80%
rename from src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php
rename to src/Domain/Infra/Console/Command/InitializeProjectionTypesCommand.php
index 58348b18..6b2e8c2d 100644
--- a/src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php
+++ b/src/Domain/Infra/Console/Command/InitializeProjectionTypesCommand.php
@@ -4,7 +4,7 @@
namespace MsgPhp\Domain\Infra\Console\Command;
-use MsgPhp\Domain\Projection\DomainProjectionTypeRegistryInterface;
+use MsgPhp\Domain\Projection\ProjectionTypeRegistryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -14,13 +14,13 @@
/**
* @author Roland Franssen
*/
-final class InitializeDomainProjectionTypesCommand extends Command
+final class InitializeProjectionTypesCommand extends Command
{
- protected static $defaultName = 'domain:projection:initialize-types';
+ protected static $defaultName = 'projection:initialize-types';
private $typeRegistry;
- public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry)
+ public function __construct(ProjectionTypeRegistryInterface $typeRegistry)
{
$this->typeRegistry = $typeRegistry;
diff --git a/src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php b/src/Domain/Infra/Console/Command/SynchronizeProjectionsCommand.php
similarity index 78%
rename from src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php
rename to src/Domain/Infra/Console/Command/SynchronizeProjectionsCommand.php
index f6d57ee7..8d99648b 100644
--- a/src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php
+++ b/src/Domain/Infra/Console/Command/SynchronizeProjectionsCommand.php
@@ -4,7 +4,7 @@
namespace MsgPhp\Domain\Infra\Console\Command;
-use MsgPhp\Domain\Projection\{DomainProjectionDocument, DomainProjectionSynchronization};
+use MsgPhp\Domain\Projection\{ProjectionDocument, ProjectionSynchronization};
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -14,14 +14,14 @@
/**
* @author Roland Franssen
*/
-final class SynchronizeDomainProjectionsCommand extends Command
+final class SynchronizeProjectionsCommand extends Command
{
- protected static $defaultName = 'domain:projection:synchronize';
+ protected static $defaultName = 'projection:synchronize';
private $synchronization;
private $logger;
- public function __construct(DomainProjectionSynchronization $synchronization, LoggerInterface $logger = null)
+ public function __construct(ProjectionSynchronization $synchronization, LoggerInterface $logger = null)
{
$this->synchronization = $synchronization;
$this->logger = $logger;
@@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$succeed = $failed = 0;
foreach ($this->synchronization->synchronize() as $document) {
- if (DomainProjectionDocument::STATUS_VALID === $document->status) {
+ if (ProjectionDocument::STATUS_SYNCHRONIZED === $document->status) {
++$succeed;
} else {
++$failed;
diff --git a/src/Domain/Infra/Console/Context/ClassContextElementFactory.php b/src/Domain/Infra/Console/Context/ClassContextElementFactory.php
index 949e12dc..160dcbda 100644
--- a/src/Domain/Infra/Console/Context/ClassContextElementFactory.php
+++ b/src/Domain/Infra/Console/Context/ClassContextElementFactory.php
@@ -11,6 +11,6 @@ final class ClassContextElementFactory implements ClassContextElementFactoryInte
{
public function getElement(string $class, string $method, string $argument): ContextElement
{
- return new ContextElement(ucfirst(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument)));
+ return new ContextElement(ucfirst((string) preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument)));
}
}
diff --git a/src/Domain/Infra/Console/Context/ClassContextFactory.php b/src/Domain/Infra/Console/Context/ClassContextFactory.php
index 2cb8b13b..9f0d5f73 100644
--- a/src/Domain/Infra/Console/Context/ClassContextFactory.php
+++ b/src/Domain/Infra/Console/Context/ClassContextFactory.php
@@ -119,7 +119,7 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val
$element = $argument['element'];
$required = $argument['required'] && !($this->flags & self::ALWAYS_OPTIONAL);
- if (is_array($value) && self::isObject($type = $argument['type']) && ($required || $given)) {
+ if (\is_array($value) && self::isObject($type = $argument['type']) && ($required || $given)) {
$method = is_subclass_of($type, DomainCollectionInterface::class) || is_subclass_of($type, DomainIdInterface::class) ? 'fromValue' : '__construct';
$context[$key] = $this->getContext($input, $io, [], array_map(function (array $argument, int $i) use ($type, $method, $value, $element): array {
if (array_key_exists($argument['name'], $value)) {
@@ -145,18 +145,17 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val
continue;
}
- if ($element->generate($io, $generated)) {
- $this->generatedValues[] = [$element->label, json_encode($generated)];
- $context[$key] = $element->normalize($generated);
- continue;
- }
-
- if ($required) {
+ if ($required || $given) {
if (!$interactive) {
throw new \LogicException(sprintf('No value provided for "%s".', $field));
}
- $context[$key] = $this->askRequiredValue($io, $element, $value);
+ if ($element->generate($io, $generated)) {
+ $this->generatedValues[] = [$element->label, json_encode($generated)];
+ $context[$key] = $element->normalize($generated);
+ } else {
+ $context[$key] = $this->askRequiredValue($io, $element, $value);
+ }
continue;
}
diff --git a/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php
index fbf853a5..9d5a7349 100644
--- a/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php
+++ b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php
@@ -64,10 +64,10 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val
} elseif (false !== $found = array_search($value, $metadata->discriminatorMap, true)) {
$context[$key] = $found;
} else {
- throw new \LogicException(sprintf('Invalid entity discriminator "%s" provided.', $value));
+ throw new \LogicException(sprintf('Invalid entity discriminator %s provided.', json_encode($value)));
}
- // @todo add feature to ask additional context values, required by the provided discriminator class
+ // @todo ask additional context values, required by the provided discriminator class
}
return $context + $this->factory->getContext($input, $io, $values);
diff --git a/src/Domain/Infra/Console/MessageReceiver.php b/src/Domain/Infra/Console/MessageReceiver.php
new file mode 100644
index 00000000..24a83e05
--- /dev/null
+++ b/src/Domain/Infra/Console/MessageReceiver.php
@@ -0,0 +1,36 @@
+
+ */
+final class MessageReceiver
+{
+ /** @var MessageReceivingInterface|null */
+ private $receiver;
+
+ public function receive($message): void
+ {
+ if (null === $this->receiver) {
+ return;
+ }
+
+ $this->receiver->onMessageReceived($message);
+ }
+
+ public function onCommand(ConsoleCommandEvent $event): void
+ {
+ $this->receiver = ($command = $event->getCommand()) instanceof MessageReceivingInterface ? $command : null;
+ }
+
+ public function onTerminate(): void
+ {
+ $this->receiver = null;
+ }
+}
diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php
index 3629d5ce..357d79f9 100644
--- a/src/Domain/Infra/DependencyInjection/BundleHelper.php
+++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php
@@ -4,14 +4,17 @@
namespace MsgPhp\Domain\Infra\DependencyInjection;
-use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\ORM\Events as DoctrineOrmEvents;
-use Doctrine\ORM\Version as DoctrineOrmVersion;
-use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra};
+use MsgPhp\Domain\{DomainIdentityHelper, DomainIdentityMappingInterface};
+use MsgPhp\Domain\Factory\{ClassMappingObjectFactory, DomainObjectFactory, DomainObjectFactoryInterface, EntityAwareFactory, EntityAwareFactoryInterface};
+use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, InMemory as InMemoryInfra, Messenger as MessengerInfra, SimpleBus as SimpleBusInfra};
+use MsgPhp\Domain\Message\{DomainMessageBus, DomainMessageBusInterface};
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\DependencyInjection\Alias;
-use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
/**
* @author Roland Franssen
@@ -22,61 +25,29 @@ final class BundleHelper
{
private static $initialized = [];
- public static function initDomain(ContainerBuilder $container): void
+ public static function build(ContainerBuilder $container): void
{
if ($initialized = &self::getInitialized($container, __FUNCTION__)) {
return;
}
- if (class_exists(ConsoleEvents::class)) {
- $container->register(ConsoleInfra\Context\ClassContextFactory::class)
- ->setPublic(false)
- ->setAbstract(true)
- ->setAutowired(true)
- ->setArgument('$method', '__construct');
-
- $container->register(ConsoleInfra\Context\ClassContextElementFactory::class)
- ->setPublic(false);
+ self::initIdentityMapping($container);
+ self::initObjectFactory($container);
+ self::initMessageBus($container);
- $container->setAlias(ConsoleInfra\Context\ClassContextElementFactoryInterface::class, new Alias(ConsoleInfra\Context\ClassContextElementFactory::class, false));
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ self::initDoctrineOrm($container);
}
-
- if (class_exists(DoctrineOrmVersion::class)) {
- $container->addCompilerPass(new Compiler\DoctrineObjectFieldMappingPass());
-
- $container->setParameter('msgphp.doctrine.mapping_cache_dirname', 'msgphp/doctrine-mapping');
-
- $container->register(DoctrineInfra\Event\ObjectFieldMappingListener::class)
- ->setPublic(false)
- ->setArgument('$mapping', [])
- ->addTag('doctrine.event_listener', ['event' => DoctrineOrmEvents::loadClassMetadata]);
-
- if (ContainerHelper::hasBundle($container, DoctrineBundle::class)) {
- @mkdir($mappingDir = $container->getParameterBag()->resolveValue('%kernel.cache_dir%/%msgphp.doctrine.mapping_cache_dirname%'), 0777, true);
-
- $container->prependExtensionConfig('doctrine', ['orm' => [
- 'hydrators' => [
- DoctrineInfra\Hydration\ScalarHydrator::NAME => DoctrineInfra\Hydration\ScalarHydrator::class,
- DoctrineInfra\Hydration\SingleScalarHydrator::NAME => DoctrineInfra\Hydration\SingleScalarHydrator::class,
- ],
- 'mappings' => [
- 'msgphp' => [
- 'dir' => $mappingDir,
- 'type' => 'xml',
- 'prefix' => 'MsgPhp',
- 'is_bundle' => false,
- ],
- ],
- ]]);
- }
+ if (FeatureDetection::isConsoleAvailable($container)) {
+ self::initConsole($container);
}
- $container->addCompilerPass(new Compiler\ResolveDomainPass());
+ $container->addCompilerPass(new Compiler\ResolveDomainPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
$initialized = true;
}
- public static function initDoctrineTypes(Container $container): void
+ public static function boot(ContainerInterface $container): void
{
if ($initialized = &self::getInitialized($container, __FUNCTION__)) {
return;
@@ -92,7 +63,180 @@ public static function initDoctrineTypes(Container $container): void
$initialized = true;
}
- private static function &getInitialized(Container $container, string $key)
+ private static function initIdentityMapping(ContainerBuilder $container): void
+ {
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ $container->register(DoctrineInfra\DomainIdentityMapping::class)
+ ->setPublic(false)
+ ->setArgument('$em', new Reference('msgphp.doctrine.entity_manager'))
+ ->setArgument('$classMapping', '%msgphp.domain.class_mapping%');
+
+ $container->setAlias(DomainIdentityMappingInterface::class, new Alias(DoctrineInfra\DomainIdentityMapping::class, false));
+ } else {
+ $container->register(InMemoryInfra\DomainIdentityMapping::class)
+ ->setPublic(false)
+ ->setArgument('$mapping', '%msgphp.domain.identity_mapping%')
+ ->setArgument('$accessor', $container->autowire(InMemoryInfra\ObjectFieldAccessor::class));
+
+ $container->setAlias(DomainIdentityMappingInterface::class, new Alias(InMemoryInfra\DomainIdentityMapping::class, false));
+ }
+
+ $container->autowire(DomainIdentityHelper::class)
+ ->setPublic(false);
+ }
+
+ private static function initObjectFactory(ContainerBuilder $container): void
+ {
+ $container->register(DomainObjectFactory::class)
+ ->setPublic(false)
+ ->addMethodCall('setNestedFactory', [new Reference(DomainObjectFactoryInterface::class)]);
+
+ $container->setAlias(DomainObjectFactoryInterface::class, new Alias(DomainObjectFactory::class, false));
+
+ $container->register(ClassMappingObjectFactory::class)
+ ->setPublic(false)
+ ->setDecoratedService(DomainObjectFactory::class)
+ ->setArgument('$factory', new Reference(ClassMappingObjectFactory::class.'.inner'))
+ ->setArgument('$mapping', '%msgphp.domain.class_mapping%');
+
+ $container->autowire(EntityAwareFactory::class)
+ ->setPublic(false)
+ ->setDecoratedService(DomainObjectFactory::class)
+ ->setArgument('$factory', new Reference(EntityAwareFactory::class.'.inner'))
+ ->setArgument('$identifierMapping', '%msgphp.domain.id_class_mapping%');
+
+ $container->setAlias(EntityAwareFactoryInterface::class, new Alias(DomainObjectFactoryInterface::class, false));
+
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ $container->register(DoctrineInfra\EntityAwareFactory::class)
+ ->setPublic(false)
+ ->setDecoratedService(EntityAwareFactory::class)
+ ->setArgument('$factory', new Reference(DoctrineInfra\EntityAwareFactory::class.'.inner'))
+ ->setArgument('$em', new Reference('msgphp.doctrine.entity_manager'))
+ ->setArgument('$classMapping', '%msgphp.domain.class_mapping%');
+ }
+ }
+
+ private static function initMessageBus(ContainerBuilder $container): void
+ {
+ if (FeatureDetection::isMessengerAvailable($container)) {
+ $type = 'messenger';
+ $container->setAlias('msgphp.messenger.command_bus', new Alias('message_bus', false));
+ $commandBus = ContainerHelper::registerAnonymous($container, MessengerInfra\DomainMessageBus::class, false, $commandBusId);
+ $commandBus->setArgument('$bus', new Reference('msgphp.messenger.command_bus'));
+
+ $container->setAlias('msgphp.messenger.event_bus', new Alias('message_bus', false));
+ $eventBus = ContainerHelper::registerAnonymous($container, MessengerInfra\DomainMessageBus::class);
+ $eventBus->setArgument('$bus', new Reference('msgphp.messenger.event_bus'));
+
+ if (FeatureDetection::isConsoleAvailable($container)) {
+ $container->autowire('msgphp.messenger.console_message_receiver', MessengerInfra\Middleware\ConsoleMessageReceiverMiddleware::class)
+ ->setPublic(false);
+ }
+ } elseif (FeatureDetection::hasSimpleBusCommandBusBundle($container)) {
+ $type = 'simple_bus';
+ $container->setAlias('msgphp.simple_bus.command_bus', new Alias('simple_bus.command_bus', false));
+ $commandBus = ContainerHelper::registerAnonymous($container, SimpleBusInfra\DomainMessageBus::class, false, $commandBusId);
+ $commandBus->setArgument('$bus', new Reference('msgphp.simple_bus.command_bus'));
+
+ if (FeatureDetection::hasSimpleBusEventBusBundle($container)) {
+ $container->setAlias('msgphp.simple_bus.event_bus', new Alias('simple_bus.event_bus', false));
+ $eventBus = ContainerHelper::registerAnonymous($container, SimpleBusInfra\DomainMessageBus::class);
+ $eventBus->setArgument('$bus', new Reference('msgphp.simple_bus.event_bus'));
+ } else {
+ $eventBus = null;
+ }
+
+ if (FeatureDetection::isConsoleAvailable($container)) {
+ $consoleReceiver = $container->autowire(SimpleBusInfra\Middleware\ConsoleMessageReceiverMiddleware::class)
+ ->setPublic(false)
+ ->addTag('command_bus_middleware');
+ if (null !== $eventBus) {
+ $consoleReceiver->addTag('event_bus_middleware');
+ }
+ }
+ } else {
+ return;
+ }
+
+ $container->setAlias('msgphp.command_bus', new Alias('msgphp.'.$type.'.command_bus', false));
+
+ if (null === $eventBus) {
+ $container->setAlias(DomainMessageBusInterface::class, new Alias($commandBusId, false));
+
+ return;
+ }
+
+ $container->register(DomainMessageBus::class)
+ ->setPublic(false)
+ ->setArgument('$commandBus', $commandBus)
+ ->setArgument('$eventBus', $eventBus)
+ ->setArgument('$eventClasses', '%msgphp.domain.events%');
+
+ $container->setAlias(DomainMessageBusInterface::class, new Alias(DomainMessageBus::class, false));
+ }
+
+ private static function initDoctrineOrm(ContainerBuilder $container): void
+ {
+ @mkdir($mappingDir = $container->getParameterBag()->resolveValue('%kernel.cache_dir%/msgphp/doctrine-mapping'), 0777, true);
+
+ $container->addCompilerPass(new Compiler\DoctrineObjectFieldMappingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
+
+ $container->prependExtensionConfig('doctrine', ['orm' => [
+ 'hydrators' => [
+ DoctrineInfra\Hydration\ScalarHydrator::NAME => DoctrineInfra\Hydration\ScalarHydrator::class,
+ DoctrineInfra\Hydration\SingleScalarHydrator::NAME => DoctrineInfra\Hydration\SingleScalarHydrator::class,
+ ],
+ 'mappings' => [
+ 'msgphp' => [
+ 'dir' => $mappingDir,
+ 'type' => 'xml',
+ 'prefix' => 'MsgPhp',
+ 'is_bundle' => false,
+ ],
+ ],
+ ]]);
+
+ $container->setAlias('msgphp.doctrine.entity_manager', new Alias('doctrine.orm.entity_manager', false));
+
+ $container->register(DoctrineInfra\ObjectFieldMappings::class)
+ ->setPublic(false)
+ ->addTag('msgphp.doctrine.object_field_mappings', ['priority' => -100]);
+
+ $container->register(DoctrineInfra\Event\ObjectFieldMappingListener::class)
+ ->setPublic(false)
+ ->addTag('doctrine.event_listener', ['event' => DoctrineOrmEvents::loadClassMetadata])
+ ->addTag('msgphp.domain.process_class_mapping', ['argument' => '$mappings']);
+
+ if (FeatureDetection::hasFrameworkBundle($container)) {
+ $container->register(DoctrineInfra\MappingCacheWarmer::class)
+ ->setPublic(false)
+ ->setArgument('$dirName', 'msgphp/doctrine-mapping')
+ ->setArgument('$mappingFiles', '%msgphp.doctrine.mapping_files%')
+ ->addTag('kernel.cache_warmer', ['priority' => 100]);
+ }
+ }
+
+ private static function initConsole(ContainerBuilder $container): void
+ {
+ $container->autowire(ConsoleInfra\Context\ClassContextFactory::class)
+ ->setPublic(false)
+ ->setAbstract(true)
+ ->setArgument('$method', '__construct')
+ ->setArgument('$classMapping', '%msgphp.domain.class_mapping%');
+
+ $container->register(ConsoleInfra\Context\ClassContextElementFactory::class)
+ ->setPublic(false);
+
+ $container->setAlias(ConsoleInfra\Context\ClassContextElementFactoryInterface::class, new Alias(ConsoleInfra\Context\ClassContextElementFactory::class, false));
+
+ $container->register(ConsoleInfra\MessageReceiver::class)
+ ->setPublic(false)
+ ->addTag('kernel.event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => 'onCommand'])
+ ->addTag('kernel.event_listener', ['event' => ConsoleEvents::TERMINATE, 'method' => 'onTerminate']);
+ }
+
+ private static function &getInitialized(ContainerInterface $container, string $key)
{
if (!isset(self::$initialized[$hash = spl_object_hash($container)."\0".$key])) {
self::$initialized[$hash] = false;
diff --git a/src/Domain/Infra/DependencyInjection/Compiler/DoctrineObjectFieldMappingPass.php b/src/Domain/Infra/DependencyInjection/Compiler/DoctrineObjectFieldMappingPass.php
index 8d2a9ba3..4139684b 100644
--- a/src/Domain/Infra/DependencyInjection/Compiler/DoctrineObjectFieldMappingPass.php
+++ b/src/Domain/Infra/DependencyInjection/Compiler/DoctrineObjectFieldMappingPass.php
@@ -4,14 +4,12 @@
namespace MsgPhp\Domain\Infra\DependencyInjection\Compiler;
-use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
-use MsgPhp\Domain\Infra\Doctrine\{EntityFieldsMapping, ObjectFieldMappingProviderInterface};
+use MsgPhp\Domain\Infra\Doctrine\ObjectFieldMappingsProviderInterface;
use MsgPhp\Domain\Infra\Doctrine\Event\ObjectFieldMappingListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
-use Symfony\Component\DependencyInjection\Reference;
/**
* @author Roland Franssen
@@ -22,33 +20,37 @@ final class DoctrineObjectFieldMappingPass implements CompilerPassInterface
private $tagName;
private $listenerId;
- private $defaultProviders;
- public function __construct(string $tagName = 'msgphp.doctrine.object_field_mapping', string $listenerId = ObjectFieldMappingListener::class, array $defaultProviders = [EntityFieldsMapping::class])
+ public function __construct(string $tagName = 'msgphp.doctrine.object_field_mappings', string $listenerId = ObjectFieldMappingListener::class)
{
$this->tagName = $tagName;
$this->listenerId = $listenerId;
- $this->defaultProviders = $defaultProviders;
}
public function process(ContainerBuilder $container): void
{
- $mapping = [];
- $providers = array_merge($this->defaultProviders, array_map(function (Reference $provider) use ($container): ?string {
- return $container->findDefinition((string) $provider)->getClass();
- }, $this->findAndSortTaggedServices($this->tagName, $container)));
-
- foreach ($providers as $provider) {
- if (!ContainerHelper::getClassReflection($container, $provider)->implementsInterface(ObjectFieldMappingProviderInterface::class)) {
- throw new InvalidArgumentException(sprintf('Provider "%s" must implement "%s".', $provider, ObjectFieldMappingProviderInterface::class));
+ $mappings = [];
+
+ foreach ($this->findAndSortTaggedServices($this->tagName, $container) as $providerId) {
+ $providerId = (string) $providerId;
+ $providerClass = $container->findDefinition($providerId)->getClass() ?? $providerId;
+
+ if (!is_subclass_of($providerClass, ObjectFieldMappingsProviderInterface::class)) {
+ throw new InvalidArgumentException(sprintf('Provider "%s" must implement "%s".', $providerId, ObjectFieldMappingsProviderInterface::class));
}
- $mapping = array_replace_recursive($mapping, $provider::getObjectFieldMapping());
+ foreach ($providerClass::provideObjectFieldMappings() as $class => $mapping) {
+ if (isset($mappings[$class])) {
+ continue;
+ }
+
+ $mappings[$class] = $mapping;
+ }
}
- if ($mapping) {
+ if ($mappings) {
$container->getDefinition($this->listenerId)
- ->setArgument('$mapping', $mapping);
+ ->setArgument('$mappings', $mappings);
} else {
$container->removeDefinition($this->listenerId);
}
diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php
index c4638978..295b48fb 100644
--- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php
+++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php
@@ -4,15 +4,11 @@
namespace MsgPhp\Domain\Infra\DependencyInjection\Compiler;
-use Doctrine\ORM\EntityManagerInterface as DoctrineEntityManager;
-use MsgPhp\Domain\{DomainIdentityHelper, DomainIdentityMappingInterface, Factory, Message};
-use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, InMemory as InMemoryInfra, SimpleBus as SimpleBusInfra};
-use Symfony\Component\DependencyInjection\Alias;
+use MsgPhp\Domain\Event\DomainEventInterface;
+use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
+use MsgPhp\Domain\Message\DomainMessageBusInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Definition;
-use Symfony\Component\DependencyInjection\Reference;
-use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
/**
* @author Roland Franssen
@@ -23,119 +19,72 @@ final class ResolveDomainPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
- $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping'));
- $idClassMapping = array_merge(...$container->getParameter('msgphp.domain.id_class_mapping'));
- $identityMapping = array_merge(...$container->getParameter('msgphp.domain.identity_mapping'));
-
- $this->registerIdentityMapping($container, $classMapping, $identityMapping);
- $this->registerEntityFactory($container, $classMapping, $idClassMapping);
- $this->registerMessageBus($container);
-
- if ($container->hasDefinition(ConsoleInfra\Context\ClassContextFactory::class)) {
- $container->getDefinition(ConsoleInfra\Context\ClassContextFactory::class)
- ->setArgument('$classMapping', $classMapping);
+ $defaultEvents = array_values(array_filter(array_map(function (string $file): string {
+ return 'MsgPhp\\Domain\\Event\\'.basename($file, '.php');
+ }, glob(\dirname(__DIR__, 3).'/Event/*Event.php')), function (string $class): bool {
+ return !is_subclass_of($class, DomainEventInterface::class);
+ }));
+ $container->setParameter($param = 'msgphp.domain.events', $container->hasParameter($param) ? array_merge($container->getParameter($param), $defaultEvents) : $defaultEvents);
+
+ if ($container->has(DomainMessageBusInterface::class)) {
+ $commandBus = $container->findDefinition('msgphp.command_bus');
+ $commandBusId = null;
+ $container->removeAlias('msgphp.command_bus');
+
+ foreach ($container->getDefinitions() as $id => $definition) {
+ if ($definition === $commandBus) {
+ $commandBusId = $id;
+ break;
+ }
+ }
+
+ foreach ($container->findTaggedServiceIds('msgphp.domain.command_handler') as $id => $attr) {
+ ContainerHelper::tagCommandHandler($container, $id, explode(',', $attr[0]['handles']), (string) $commandBusId);
+ }
+ } else {
+ foreach ($container->findTaggedServiceIds('msgphp.domain.message_aware') as $id => $attr) {
+ $container->removeDefinition($id);
+ }
}
- if (interface_exists(CacheWarmerInterface::class) && $container->hasParameter('msgphp.doctrine.mapping_files')) {
- $mappingFiles = array_merge(...$container->getParameter('msgphp.doctrine.mapping_files'));
+ $classMapping = $container->getParameter('msgphp.domain.class_mapping');
+ foreach ($container->findTaggedServiceIds('msgphp.domain.process_class_mapping') as $id => $attr) {
+ $definition = $container->getDefinition($id);
- self::register($container, DoctrineInfra\MappingCacheWarmer::class)
- ->setArgument('$dirname', '%msgphp.doctrine.mapping_cache_dirname%')
- ->setArgument('$mappingFiles', $mappingFiles)
- ->addTag('kernel.cache_warmer', ['priority' => 100]);
- }
+ foreach ($attr as $attr) {
+ if (!isset($attr['argument'])) {
+ continue;
+ }
- if ($container->hasDefinition(DoctrineInfra\Event\ObjectFieldMappingListener::class)) {
- ($definition = $container->getDefinition(DoctrineInfra\Event\ObjectFieldMappingListener::class))
- ->setArgument('$mapping', self::processClassMapping($definition->getArgument('$mapping'), $classMapping));
- }
-
- $params = $container->getParameterBag();
- $params->remove('msgphp.domain.class_mapping');
- $params->remove('msgphp.domain.id_class_mapping');
- $params->remove('msgphp.domain.identity_mapping');
- $params->remove('msgphp.doctrine.mapping_files');
- }
+ $value = $definition->getArgument($attr['argument']);
+ $definition->setArgument($attr['argument'], self::processClassMapping($value, $classMapping, !empty($attr['array_keys'])));
+ }
- private static function register(ContainerBuilder $container, string $class, string $id = null): Definition
- {
- return $container->register($id ?? $class, $class)->setPublic(false);
- }
-
- private static function alias(ContainerBuilder $container, string $alias, string $id): void
- {
- $container->setAlias($alias, new Alias($id, false));
+ $definition->clearTag('msgphp.domain.process_class_mapping');
+ }
}
- private static function processClassMapping($value, array $classMapping)
+ private static function processClassMapping($value, array $classMapping, bool $arrayKeys = false)
{
- if (is_string($value) && isset($classMapping[$value])) {
+ if (\is_string($value) && isset($classMapping[$value])) {
return $classMapping[$value];
}
- if (is_array($value)) {
- array_walk_recursive($value, function (&$value) use ($classMapping): void {
- $value = self::processClassMapping($value, $classMapping);
- });
- }
-
- return $value;
- }
-
- private function registerIdentityMapping(ContainerBuilder $container, array $classMapping, array $identityMapping): void
- {
- if ($container->has(DoctrineEntityManager::class)) {
- self::register($container, $aliasId = DoctrineInfra\DomainIdentityMapping::class)
- ->setAutowired(true)
- ->setArgument('$classMapping', $classMapping);
- } else {
- self::register($container, $aliasId = InMemoryInfra\DomainIdentityMapping::class)
- ->setArgument('$mapping', $identityMapping)
- ->setArgument('$accessor', self::register($container, InMemoryInfra\ObjectFieldAccessor::class)
- ->setAutowired(true));
+ if (!\is_array($value)) {
+ return $value;
}
- self::alias($container, DomainIdentityMappingInterface::class, $aliasId);
+ $result = [];
- self::register($container, DomainIdentityHelper::class)
- ->setAutowired(true);
- }
+ foreach ($value as $k => $v) {
+ $v = self::processClassMapping($v, $classMapping, $arrayKeys);
+ if ($arrayKeys) {
+ $k = self::processClassMapping($k, $classMapping);
+ }
- private function registerEntityFactory(ContainerBuilder $container, array $classMapping, array $idClassMapping): void
- {
- self::register($container, $aliasId = Factory\DomainObjectFactory::class)
- ->addMethodCall('setNestedFactory', [new Reference(Factory\DomainObjectFactoryInterface::class)]);
-
- self::register($container, Factory\ClassMappingObjectFactory::class)
- ->setDecoratedService($aliasId)
- ->setArgument('$factory', new Reference(Factory\ClassMappingObjectFactory::class.'.inner'))
- ->setArgument('$mapping', $classMapping);
-
- self::register($container, $entityAliasId = Factory\EntityAwareFactory::class)
- ->setAutowired(true)
- ->setArgument('$identifierMapping', $idClassMapping);
-
- if ($container->has(DoctrineEntityManager::class)) {
- self::register($container, DoctrineInfra\EntityAwareFactory::class)
- ->setAutowired(true)
- ->setDecoratedService($entityAliasId)
- ->setArgument('$factory', new Reference(DoctrineInfra\EntityAwareFactory::class.'.inner'))
- ->setArgument('$classMapping', $classMapping);
+ $result[$k] = $v;
}
- self::alias($container, Factory\DomainObjectFactoryInterface::class, $aliasId);
- self::alias($container, Factory\EntityAwareFactoryInterface::class, $entityAliasId);
- }
-
- private function registerMessageBus(ContainerBuilder $container): void
- {
- if (!$container->has('simple_bus.command_bus')) {
- return;
- }
-
- self::register($container, $aliasId = SimpleBusInfra\DomainMessageBus::class)
- ->setArgument('$bus', new Reference('simple_bus.command_bus'));
-
- self::alias($container, Message\DomainMessageBusInterface::class, $aliasId);
+ return $result;
}
}
diff --git a/src/Domain/Infra/DependencyInjection/ConfigHelper.php b/src/Domain/Infra/DependencyInjection/ConfigHelper.php
index 62e9eaf8..5d263595 100644
--- a/src/Domain/Infra/DependencyInjection/ConfigHelper.php
+++ b/src/Domain/Infra/DependencyInjection/ConfigHelper.php
@@ -21,7 +21,7 @@ final class ConfigHelper
public static function defaultBundleConfig(array $defaultIdClassMapping, array $idClassMappingPerType): \Closure
{
return function (array $value) use ($defaultIdClassMapping, $idClassMappingPerType): array {
- $defaultType = $value['default_id_type'] ?? ConfigHelper::DEFAULT_ID_TYPE;
+ $defaultType = $value['default_id_type'] ?? self::DEFAULT_ID_TYPE;
unset($value['default_id_type']);
if (isset($value['id_type_mapping'])) {
@@ -31,7 +31,7 @@ public static function defaultBundleConfig(array $defaultIdClassMapping, array $
}
if (null === $mappedClass = $idClassMapping[$type][$class] ?? $defaultIdClassMapping[$class] ?? null) {
- $mappedClass = in_array($type, self::UUID_TYPES, true) ? UuidInfra\DomainId::class : DomainId::class;
+ $mappedClass = \in_array($type, self::UUID_TYPES, true) ? UuidInfra\DomainId::class : DomainId::class;
}
$value['class_mapping'][$class] = $mappedClass;
@@ -52,15 +52,15 @@ public static function defaultBundleConfig(array $defaultIdClassMapping, array $
public static function resolveCommandMappingConfig(array $commandMapping, array $classMapping, array &$config): void
{
- foreach ($commandMapping as $commandClass => $features) {
- $mappedClass = $classMapping[$commandClass] ?? $commandClass;
- $isEventHandler = is_subclass_of($mappedClass, DomainEventHandlerInterface::class);
-
- foreach ($features as $feature => $featureInfo) {
- if (!trait_exists($feature)) {
- $config[$feature] = $featureInfo;
- } elseif (self::uses($mappedClass, $feature)) {
- $config += array_fill_keys($featureInfo, $isEventHandler);
+ foreach ($commandMapping as $class => $features) {
+ $available = isset($classMapping[$class]);
+ $handlerAvailable = $available && is_subclass_of($classMapping[$class], DomainEventHandlerInterface::class);
+
+ foreach ($features as $feature => $info) {
+ if (!\is_array($info)) {
+ $config += [$info => $available];
+ } else {
+ $config += array_fill_keys($info, $available && self::uses($classMapping[$class], $feature) ? $handlerAvailable : false);
}
}
}
diff --git a/src/Domain/Infra/DependencyInjection/ContainerHelper.php b/src/Domain/Infra/DependencyInjection/ContainerHelper.php
index dc8c94b2..fb0c2218 100644
--- a/src/Domain/Infra/DependencyInjection/ContainerHelper.php
+++ b/src/Domain/Infra/DependencyInjection/ContainerHelper.php
@@ -4,19 +4,11 @@
namespace MsgPhp\Domain\Infra\DependencyInjection;
-use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
-use Doctrine\DBAL\Types\Type as DoctrineType;
-use Doctrine\ORM\Version as DoctrineOrmVersion;
-use Ramsey\Uuid\Doctrine as DoctrineUuid;
-use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle;
-use MsgPhp\Domain\Infra\{Console as ConsoleInfra, SimpleBus as SimpleBusInfra};
-use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\DependencyInjection\ChildDefinition;
-use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
-use Symfony\Component\DependencyInjection\Reference;
/**
* @author Roland Franssen
@@ -27,23 +19,16 @@ final class ContainerHelper
{
private static $counter = 0;
- public static function hasBundle(Container $container, string $class): bool
+ public static function hasBundle(ContainerInterface $container, string $class): bool
{
- return in_array($class, $container->getParameter('kernel.bundles'), true);
+ return \in_array($class, $container->getParameter('kernel.bundles'), true);
}
- public static function getBundles(Container $container): array
+ public static function getBundles(ContainerInterface $container): array
{
return array_flip($container->getParameter('kernel.bundles'));
}
- public static function getClassReflector(ContainerBuilder $container): \Closure
- {
- return function (string $class) use ($container): \ReflectionClass {
- return self::getClassReflection($container, $class);
- };
- }
-
public static function getClassReflection(ContainerBuilder $container, ?string $class): \ReflectionClass
{
if (!$class || !($reflection = $container->getReflectionClass($class))) {
@@ -53,9 +38,10 @@ public static function getClassReflection(ContainerBuilder $container, ?string $
return $reflection;
}
- public static function removeDefinitionWithAliases(ContainerBuilder $container, string $id): void
+ public static function removeId(ContainerBuilder $container, string $id): void
{
$container->removeDefinition($id);
+ $container->removeAlias($id);
foreach ($container->getAliases() as $aliasId => $alias) {
if ($id === (string) $alias) {
@@ -64,220 +50,39 @@ public static function removeDefinitionWithAliases(ContainerBuilder $container,
}
}
- public static function removeIf(ContainerBuilder $container, $condition, array $ids): void
+ public static function removeIf(ContainerBuilder $container, bool $condition, array $ids): void
{
if (!$condition) {
return;
}
foreach ($ids as $id) {
- self::removeDefinitionWithAliases($container, $id);
- $container->removeAlias($id);
+ self::removeId($container, $id);
}
}
- public static function registerAnonymous(ContainerBuilder $container, string $class, bool $child = false): Definition
+ public static function registerAnonymous(ContainerBuilder $container, string $class, bool $child = false, string &$id = null): Definition
{
$definition = $child ? new ChildDefinition($class) : new Definition($class);
$definition->setPublic(false);
- return $container->setDefinition($class.'.'.ContainerBuilder::hash(__METHOD__.++self::$counter), $definition);
- }
-
- public static function registerConsoleClassContextFactory(ContainerBuilder $container, string $class, int $flags = 0): Definition
- {
- $definition = self::registerAnonymous($container, ConsoleInfra\Context\ClassContextFactory::class, true)
- ->setArgument('$class', $class)
- ->setArgument('$flags', $flags);
-
- if (class_exists(DoctrineOrmVersion::class) && self::hasBundle($container, DoctrineBundle::class)) {
- $definition = self::registerAnonymous($container, ConsoleInfra\Context\DoctrineEntityContextFactory::class)
- ->setAutowired(true)
- ->setArgument('$factory', $definition)
- ->setArgument('$class', $class);
- }
-
- return $definition;
- }
-
- public static function configureIdentityMapping(ContainerBuilder $container, array $classMapping, array $identityMapping): void
- {
- foreach ($identityMapping as $class => $mapping) {
- if (isset($classMapping[$class]) && !isset($identityMapping[$classMapping[$class]])) {
- $identityMapping[$classMapping[$class]] = $mapping;
- }
- }
-
- $values = $container->hasParameter($param = 'msgphp.domain.identity_mapping') ? $container->getParameter($param) : [];
- $values[] = $identityMapping;
-
- $container->setParameter($param, $values);
- }
-
- public static function configureEntityFactory(ContainerBuilder $container, array $classMapping, array $idClassMapping): void
- {
- foreach ($idClassMapping as $class => $idClass) {
- if (isset($classMapping[$class]) && !isset($idClassMapping[$classMapping[$class]])) {
- $idClassMapping[$classMapping[$class]] = $idClass;
- }
- }
-
- $values = $container->hasParameter($param = 'msgphp.domain.class_mapping') ? $container->getParameter($param) : [];
- $values[] = $classMapping;
-
- $container->setParameter($param, $values);
-
- $values = $container->hasParameter($param = 'msgphp.domain.id_class_mapping') ? $container->getParameter($param) : [];
- $values[] = $idClassMapping;
-
- $container->setParameter($param, $values);
- }
-
- public static function configureDoctrineTypes(ContainerBuilder $container, array $classMapping, array $idTypeMapping, array $typeClassMapping): void
- {
- if (!class_exists(DoctrineType::class)) {
- return;
- }
-
- $dbalTypes = $mappingTypes = $typeConfig = [];
- $uuidMapping = [
- 'uuid' => DoctrineUuid\UuidType::class,
- 'uuid_binary' => DoctrineUuid\UuidBinaryType::class,
- 'uuid_binary_ordered_time' => DoctrineUuid\UuidBinaryOrderedTimeType::class,
- ];
-
- foreach ($typeClassMapping as $idClass => $typeClass) {
- $type = $idTypeMapping[$idClass] ?? DoctrineType::INTEGER;
-
- if (isset($uuidMapping[$type])) {
- if (!class_exists($uuidClass = $uuidMapping[$type])) {
- throw new \LogicException(sprintf('Type "%s" for identifier "%s" requires "ramsey/uuid-doctrine".', $type, $idClass));
- }
-
- $dbalTypes[$uuidClass::NAME] = $uuidClass;
-
- if ('uuid_binary' === $type || 'uuid_binary_ordered_time' === $type) {
- $mappingTypes[$type] = 'binary';
- }
- }
-
- if (!defined($typeClass.'::NAME')) {
- throw new \LogicException(sprintf('Type class "%s" for identifier "%s" requires a "NAME" constant.', $typeClass, $idClass));
- }
-
- $dbalTypes[$typeClass::NAME] = $typeClass;
- $typeConfig[$typeClass::NAME] = ['class' => $classMapping[$idClass] ?? $idClass, 'type' => $type, 'type_class' => $typeClass];
- }
-
- if ($dbalTypes || $mappingTypes) {
- if ($container->hasParameter($param = 'msgphp.doctrine.type_config')) {
- $typeConfig += $container->getParameter($param);
- }
-
- $container->setParameter($param, $typeConfig);
-
- if (self::hasBundle($container, DoctrineBundle::class)) {
- $container->prependExtensionConfig('doctrine', ['dbal' => ['types' => $dbalTypes, 'mapping_types' => $mappingTypes]]);
- }
- }
- }
-
- public static function configureDoctrineOrmMapping(ContainerBuilder $container, array $mappingFiles, array $objectFieldMappings = []): void
- {
- if (!class_exists(DoctrineOrmVersion::class)) {
- return;
- }
-
- $values = $container->hasParameter($param = 'msgphp.doctrine.mapping_files') ? $container->getParameter($param) : [];
- $values[] = $mappingFiles;
-
- $container->setParameter($param, $values);
-
- foreach ($objectFieldMappings as $class) {
- $container->register($class)
- ->setPublic(false)
- ->addTag('msgphp.doctrine.object_field_mapping', ['priority' => -100]);
- }
+ return $container->setDefinition($id = $class.'.'.ContainerBuilder::hash(__METHOD__.++self::$counter), $definition);
}
- public static function configureDoctrineOrmTargetEntities(ContainerBuilder $container, array $classMapping): void
+ public static function tagCommandHandler(ContainerBuilder $container, string $handlerId, array $handles, string $busId): void
{
- if (!class_exists(DoctrineOrmVersion::class) || !self::hasBundle($container, DoctrineBundle::class)) {
- return;
- }
+ $handler = $container->getDefinition($handlerId);
+ $messengerEnabled = FeatureDetection::isMessengerAvailable($container);
+ $simpleBusEnabled = FeatureDetection::hasSimpleBusCommandBusBundle($container);
- $container->prependExtensionConfig('doctrine', [
- 'orm' => [
- 'resolve_target_entities' => $classMapping,
- ],
- ]);
- }
-
- public static function configureDoctrineOrmRepositories(ContainerBuilder $container, array $classMapping, array $repositoryMapping): void
- {
- if (!class_exists(DoctrineOrmVersion::class)) {
- return;
- }
-
- foreach ($repositoryMapping as $repository => $class) {
- if (!isset($classMapping[$class])) {
- self::removeDefinitionWithAliases($container, $repository);
- continue;
+ foreach ($handles as $class) {
+ if ($messengerEnabled) {
+ $handler->addTag('messenger.message_handler', ['handles' => $class, 'bus' => $busId]);
}
-
- $container->getDefinition($repository)
- ->setArgument('$class', $classMapping[$class]);
- }
- }
-
- public static function configureCommandMessages(ContainerBuilder $container, array $classMapping, array $commands): void
- {
- if (!self::hasBundle($container, SimpleBusCommandBusBundle::class)) {
- return;
- }
-
- foreach ($container->findTaggedServiceIds($tag = 'command_handler') as $id => $attr) {
- foreach ($attr as $attr) {
- if (!isset($attr[$attrName = 'handles'])) {
- continue;
- }
-
- if ($commands[$command = $attr[$attrName]] ?? false) {
- if (isset($classMapping[$command])) {
- $container->getDefinition($id)
- ->addTag($tag, [$attrName => $classMapping[$command]]);
- }
-
- continue;
- }
-
- $container->removeDefinition($id);
- }
- }
- }
-
- public static function configureEventMessages(ContainerBuilder $container, array $classMapping, array $events): void
- {
- if (!self::hasBundle($container, SimpleBusCommandBusBundle::class)) {
- return;
- }
-
- $definition = self::registerAnonymous($container, SimpleBusInfra\EventMessageHandler::class);
- $definition
- ->setPublic(true)
- ->setArgument('$bus', new Reference('simple_bus.event_bus', ContainerBuilder::NULL_ON_INVALID_REFERENCE));
-
- if (class_exists(ConsoleEvents::class)) {
- $definition
- ->addTag('kernel.event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => 'onConsoleCommand'])
- ->addTag('kernel.event_listener', ['event' => ConsoleEvents::TERMINATE, 'method' => 'onConsoleTerminate']);
- }
-
- foreach ($events as $event) {
- $definition->addTag($tag = 'command_handler', [$attrName = 'handles' => $event]);
-
- if (isset($classMapping[$event])) {
- $definition->addTag($tag, [$attrName => $classMapping[$event]]);
+ if ($simpleBusEnabled) {
+ $handler
+ ->setPublic(true)
+ ->addTag('command_handler', ['handles' => $class]);
}
}
}
diff --git a/src/Domain/Infra/DependencyInjection/ExtensionHelper.php b/src/Domain/Infra/DependencyInjection/ExtensionHelper.php
new file mode 100644
index 00000000..64b42d72
--- /dev/null
+++ b/src/Domain/Infra/DependencyInjection/ExtensionHelper.php
@@ -0,0 +1,176 @@
+
+ *
+ * @internal
+ */
+final class ExtensionHelper
+{
+ public static function configureDomain(ContainerBuilder $container, array $classMapping, array $idClassMapping, array $identityMapping): void
+ {
+ foreach ($idClassMapping as $class => $idClass) {
+ if (isset($classMapping[$class]) && !isset($idClassMapping[$classMapping[$class]])) {
+ $idClassMapping[$classMapping[$class]] = $idClass;
+ }
+ }
+ foreach ($identityMapping as $class => $mapping) {
+ if (isset($classMapping[$class]) && !isset($identityMapping[$classMapping[$class]])) {
+ $identityMapping[$classMapping[$class]] = $mapping;
+ }
+ }
+
+ $container->setParameter($param = 'msgphp.domain.class_mapping', $container->hasParameter($param) ? $classMapping + $container->getParameter($param) : $classMapping);
+ $container->setParameter($param = 'msgphp.domain.id_class_mapping', $container->hasParameter($param) ? $idClassMapping + $container->getParameter($param) : $idClassMapping);
+ $container->setParameter($param = 'msgphp.domain.identity_mapping', $container->hasParameter($param) ? $identityMapping + $container->getParameter($param) : $identityMapping);
+ }
+
+ public static function configureDoctrineOrm(ContainerBuilder $container, array $classMapping, array $idTypeMapping, array $typeClassMapping, array $mappingFiles): void
+ {
+ $dbalTypes = $dbalMappingTypes = $typeConfig = [];
+ $uuidMapping = [
+ 'uuid' => DoctrineUuid\UuidType::class,
+ 'uuid_binary' => DoctrineUuid\UuidBinaryType::class,
+ 'uuid_binary_ordered_time' => DoctrineUuid\UuidBinaryOrderedTimeType::class,
+ ];
+
+ foreach ($typeClassMapping as $idClass => $typeClass) {
+ $type = $idTypeMapping[$idClass] ?? DoctrineType::INTEGER;
+
+ if (isset($uuidMapping[$type])) {
+ if (!class_exists($uuidClass = $uuidMapping[$type])) {
+ throw new \LogicException(sprintf('Type "%s" for identifier "%s" requires "ramsey/uuid-doctrine".', $type, $idClass));
+ }
+
+ $dbalTypes[$uuidClass::NAME] = $uuidClass;
+
+ if ('uuid_binary' === $type || 'uuid_binary_ordered_time' === $type) {
+ $dbalMappingTypes[$type] = 'binary';
+ }
+ }
+
+ if (!\defined($typeClass.'::NAME')) {
+ throw new \LogicException(sprintf('Type class "%s" for identifier "%s" requires a "NAME" constant.', $typeClass, $idClass));
+ }
+
+ $dbalTypes[$typeClass::NAME] = $typeClass;
+ $typeConfig[$typeClass::NAME] = ['class' => $classMapping[$idClass] ?? $idClass, 'type' => $type, 'type_class' => $typeClass];
+ }
+
+ $container->setParameter($param = 'msgphp.doctrine.type_config', $container->hasParameter($param) ? $typeConfig + $container->getParameter($param) : $typeConfig);
+ $container->setParameter($param = 'msgphp.doctrine.mapping_files', $container->hasParameter($param) ? array_merge($container->getParameter($param), $mappingFiles) : $mappingFiles);
+
+ $container->prependExtensionConfig('doctrine', [
+ 'dbal' => [
+ 'types' => $dbalTypes,
+ 'mapping_types' => $dbalMappingTypes,
+ ],
+ 'orm' => [
+ 'resolve_target_entities' => $classMapping,
+ ],
+ ]);
+ }
+
+ public static function finalizeCommandHandlers(ContainerBuilder $container, array $classMapping, array $commands, array $events): void
+ {
+ foreach ($container->findTaggedServiceIds($tag = 'msgphp.domain.command_handler') as $id => $attr) {
+ $definition = $container->getDefinition($id);
+ $definition->addTag('msgphp.domain.message_aware');
+
+ $command = (new \ReflectionMethod($definition->getClass() ?? (string) $id, '__invoke'))->getParameters()[0]->getClass()->getName();
+ if (empty($commands[$command])) {
+ $container->removeDefinition($id);
+ continue;
+ }
+
+ $handles = [$command];
+ if (isset($classMapping[$command])) {
+ $handles[] = $classMapping[$command];
+ }
+
+ $definition
+ ->clearTag($tag)
+ ->addTag($tag, $attr[0] + ['handles' => implode(',', $handles)]);
+ }
+
+ foreach ($events as $class) {
+ if (isset($classMapping[$class])) {
+ $events[] = $classMapping[$class];
+ }
+ }
+
+ $container->setParameter($param = 'msgphp.domain.events', $container->hasParameter($param) ? array_merge($container->getParameter($param), $events) : $events);
+ }
+
+ public static function finalizeDoctrineOrmRepositories(ContainerBuilder $container, array $classMapping, array $entityRepositoryMapping): void
+ {
+ foreach ($entityRepositoryMapping as $entity => $repository) {
+ if (!$container->hasDefinition($repository)) {
+ continue;
+ }
+
+ if (!isset($classMapping[$entity])) {
+ $container->removeDefinition($repository);
+ continue;
+ }
+
+ ($definition = $container->getDefinition($repository))
+ ->setArgument('$class', $classMapping[$entity]);
+
+ foreach (class_implements($definition->getClass() ?? $repository) as $interface) {
+ $container->setAlias($interface, new Alias($repository, false));
+ }
+ }
+ }
+
+ public static function finalizeConsoleCommands(ContainerBuilder $container, array $commands, array $consoleDomainCommandsMapping): void
+ {
+ foreach ($consoleDomainCommandsMapping as $domainCommand => $consoleCommands) {
+ foreach ($consoleCommands as $consoleCommand) {
+ if (!$container->hasDefinition($consoleCommand)) {
+ continue;
+ }
+
+ if (empty($commands[$domainCommand])) {
+ $container->removeDefinition($consoleCommand);
+ continue;
+ }
+
+ $container->getDefinition($consoleCommand)
+ ->addTag('msgphp.domain.message_aware');
+ }
+ }
+ }
+
+ public static function registerConsoleClassContextFactory(ContainerBuilder $container, string $class, int $flags = 0): Definition
+ {
+ $definition = ContainerHelper::registerAnonymous($container, ConsoleInfra\Context\ClassContextFactory::class, true)
+ ->setArgument('$class', $class)
+ ->setArgument('$flags', $flags);
+
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ $definition = ContainerHelper::registerAnonymous($container, ConsoleInfra\Context\DoctrineEntityContextFactory::class)
+ ->setArgument('$factory', $definition)
+ ->setArgument('$em', new Reference('msgphp.doctrine.entity_manager'))
+ ->setArgument('$class', $class);
+ }
+
+ return $definition;
+ }
+
+ private function __construct()
+ {
+ }
+}
diff --git a/src/Domain/Infra/DependencyInjection/FeatureDetection.php b/src/Domain/Infra/DependencyInjection/FeatureDetection.php
new file mode 100644
index 00000000..494e0719
--- /dev/null
+++ b/src/Domain/Infra/DependencyInjection/FeatureDetection.php
@@ -0,0 +1,98 @@
+
+ *
+ * @todo let user allow to opt-in / -out per feature pre-build
+ *
+ * @internal
+ */
+final class FeatureDetection
+{
+ public static function isSymfonyFullStack(): bool
+ {
+ return class_exists('Symfony\Bundle\FullStack');
+ }
+
+ public static function hasFrameworkBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, FrameworkBundle::class);
+ }
+
+ public static function hasSecurityBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, SecurityBundle::class);
+ }
+
+ public static function hasTwigBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, TwigBundle::class);
+ }
+
+ public static function hasDoctrineBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, DoctrineBundle::class);
+ }
+
+ public static function hasSimpleBusCommandBusBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, SimpleBusCommandBusBundle::class);
+ }
+
+ public static function hasSimpleBusEventBusBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, SimpleBusEventBusBundle::class);
+ }
+
+ public static function hasMsgPhpEavBundle(ContainerInterface $container): bool
+ {
+ return ContainerHelper::hasBundle($container, MsgPhpEavBundle::class);
+ }
+
+ public static function isFormAvailable(ContainerInterface $container): bool
+ {
+ return !self::isSymfonyFullStack() && self::hasFrameworkBundle($container) && interface_exists(FormInterface::class);
+ }
+
+ public static function isValidatorAvailable(ContainerInterface $container): bool
+ {
+ return !self::isSymfonyFullStack() && self::hasFrameworkBundle($container) && interface_exists(ValidatorInterface::class);
+ }
+
+ public static function isConsoleAvailable(ContainerInterface $container): bool
+ {
+ return !self::isSymfonyFullStack() && self::hasFrameworkBundle($container) && class_exists(ConsoleEvents::class);
+ }
+
+ public static function isMessengerAvailable(ContainerInterface $container): bool
+ {
+ return !self::isSymfonyFullStack() && self::hasFrameworkBundle($container) && interface_exists(MessageBusInterface::class);
+ }
+
+ public static function isDoctrineOrmAvailable(ContainerInterface $container): bool
+ {
+ return self::hasDoctrineBundle($container) && class_exists(DoctrineOrmVersion::class);
+ }
+
+ private function __construct()
+ {
+ }
+}
diff --git a/src/Domain/Infra/Doctrine/DomainCollection.php b/src/Domain/Infra/Doctrine/DomainCollection.php
index 48ef4de0..b248a48d 100644
--- a/src/Domain/Infra/Doctrine/DomainCollection.php
+++ b/src/Domain/Infra/Doctrine/DomainCollection.php
@@ -15,16 +15,17 @@ final class DomainCollection implements DomainCollectionInterface
{
private $collection;
- /**
- * @return $this|self
- */
public static function fromValue(?iterable $value): DomainCollectionInterface
{
if ($value instanceof Collection) {
return new self($value);
}
- return new self(new ArrayCollection($value instanceof \Traversable ? iterator_to_array($value) : $value ?? []));
+ if ($value instanceof \Traversable) {
+ $value = iterator_to_array($value);
+ }
+
+ return new self(new ArrayCollection($value ?? []));
}
public function __construct(Collection $collection)
diff --git a/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php b/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php
index 7162c687..0ce6824a 100644
--- a/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php
+++ b/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php
@@ -109,7 +109,7 @@ private function doExistsByFields(array $fields): bool
private function doSave($entity): void
{
if (!$entity instanceof $this->class) {
- throw InvalidClassException::create(get_class($entity));
+ throw InvalidClassException::create(\get_class($entity));
}
$this->em->persist($entity);
@@ -117,7 +117,7 @@ private function doSave($entity): void
try {
$this->em->flush();
} catch (UniqueConstraintViolationException $e) {
- throw DuplicateEntityException::createForId(get_class($entity), $this->identityHelper->getIdentity($entity));
+ throw DuplicateEntityException::createForId(\get_class($entity), $this->identityHelper->getIdentity($entity));
}
}
@@ -127,7 +127,7 @@ private function doSave($entity): void
private function doDelete($entity): void
{
if (!$entity instanceof $this->class) {
- throw InvalidClassException::create(get_class($entity));
+ throw InvalidClassException::create(\get_class($entity));
}
$this->em->remove($entity);
@@ -168,7 +168,7 @@ private function addFieldCriteria(QueryBuilder $qb, array $fields, bool $or = fa
$expr = $qb->expr();
$where = $or ? $expr->orX() : $expr->andX();
- $alias = $alias ?? $qb->getAllAliases()[0] ?? $this->alias;
+ $alias = $alias ?? $this->alias;
$idFields = array_flip($this->identityHelper->getIdentifierFieldNames($this->class));
$associations = $this->em->getClassMetadata($this->class)->getAssociationMappings();
@@ -192,7 +192,7 @@ private function addFieldCriteria(QueryBuilder $qb, array $fields, bool $or = fa
$param = $this->addFieldParameter($qb, $field, $value);
- if (is_array($value)) {
+ if (\is_array($value)) {
$where->add($expr->in($fieldAlias, $param));
} elseif (isset($associations[$field])) {
$where->add($expr->eq('IDENTITY('.$fieldAlias.')', $param));
diff --git a/src/Domain/Infra/Doctrine/DomainIdType.php b/src/Domain/Infra/Doctrine/DomainIdType.php
index e4e4aaa9..bc123c85 100644
--- a/src/Domain/Infra/Doctrine/DomainIdType.php
+++ b/src/Domain/Infra/Doctrine/DomainIdType.php
@@ -56,7 +56,7 @@ final public static function resetMapping(): void
final public static function resolveName($value): ?string
{
if ($value instanceof DomainIdInterface) {
- $class = get_class($value);
+ $class = \get_class($value);
foreach (self::$mapping as $type => $mapping) {
if ($class === $mapping['class']) {
@@ -76,7 +76,7 @@ final public static function resolveName($value): ?string
final public static function resolveValue($value, AbstractPlatform $platform)
{
if ($value instanceof DomainIdInterface) {
- $class = get_class($value);
+ $class = \get_class($value);
$type = Type::INTEGER;
foreach (self::$mapping as $type => $mapping) {
diff --git a/src/Domain/Infra/Doctrine/DomainIdentityMapping.php b/src/Domain/Infra/Doctrine/DomainIdentityMapping.php
index fefd9583..5b54a1f2 100644
--- a/src/Domain/Infra/Doctrine/DomainIdentityMapping.php
+++ b/src/Domain/Infra/Doctrine/DomainIdentityMapping.php
@@ -30,12 +30,12 @@ public function getIdentifierFieldNames(string $class): array
public function getIdentity($object): array
{
- return array_filter($this->getMetadata(get_class($object))->getIdentifierValues($object), function ($value) {
+ return array_filter($this->getMetadata(\get_class($object))->getIdentifierValues($object), function ($value) {
if ($value instanceof DomainIdInterface) {
return !$value->isEmpty();
}
- if (is_object($value)) {
+ if (\is_object($value)) {
return (bool) $this->getIdentity($value);
}
@@ -46,11 +46,12 @@ public function getIdentity($object): array
private function getMetadata(string $class): ClassMetadata
{
$class = $this->classMapping[$class] ?? $class;
+ $factory = $this->em->getMetadataFactory();
- if (!class_exists($class) || $this->em->getMetadataFactory()->isTransient($class)) {
+ if (!class_exists($class) || $factory->isTransient($class)) {
throw InvalidClassException::create($class);
}
- return $this->em->getClassMetadata($class);
+ return $factory->getMetadataFor($class);
}
}
diff --git a/src/Domain/Infra/Doctrine/EntityAwareFactory.php b/src/Domain/Infra/Doctrine/EntityAwareFactory.php
index afdeea8d..b6b1a33a 100644
--- a/src/Domain/Infra/Doctrine/EntityAwareFactory.php
+++ b/src/Domain/Infra/Doctrine/EntityAwareFactory.php
@@ -41,7 +41,7 @@ public function reference(string $class, $id)
if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) {
throw InvalidClassException::create($class);
}
- if (is_array($id)) {
+ if (\is_array($id)) {
$class = $this->getDiscriminatorClass($class, $id, true);
}
if (null === $ref = $this->em->getReference($class, $id)) {
diff --git a/src/Domain/Infra/Doctrine/EntityFieldsMapping.php b/src/Domain/Infra/Doctrine/EntityFieldsMapping.php
deleted file mode 100644
index bb98ad8e..00000000
--- a/src/Domain/Infra/Doctrine/EntityFieldsMapping.php
+++ /dev/null
@@ -1,47 +0,0 @@
-
- *
- * @internal
- */
-final class EntityFieldsMapping implements ObjectFieldMappingProviderInterface
-{
- public static function getObjectFieldMapping(): array
- {
- return [
- Features\CanBeConfirmed::class => [
- 'confirmationToken' => [
- 'type' => 'string',
- 'unique' => true,
- 'nullable' => true,
- ],
- 'confirmedAt' => [
- 'type' => 'datetime',
- 'nullable' => true,
- ],
- ],
- Fields\CreatedAtField::class => [
- 'createdAt' => [
- 'type' => 'datetime',
- ],
- ],
- Fields\EnabledField::class => [
- 'enabled' => [
- 'type' => 'boolean',
- ],
- ],
- Fields\LastUpdatedAtField::class => [
- 'lastUpdatedAt' => [
- 'type' => 'datetime',
- ],
- ],
- ];
- }
-}
diff --git a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php
index bd6f39bd..1f6aa411 100644
--- a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php
+++ b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php
@@ -13,19 +13,19 @@
*/
final class ObjectFieldMappingListener
{
- private $mapping;
+ private $mappings;
/** @var ClassMetadataFactory|null */
private $metadataFactory;
- public function __construct(array $mapping)
+ public function __construct(array $mappings)
{
- $this->mapping = $mapping;
+ $this->mappings = $mappings;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
{
- if (!$this->mapping) {
+ if (!$this->mappings) {
return;
}
@@ -40,8 +40,8 @@ private function processClassFields(ClassMetadataInfo $metadata, \ReflectionClas
{
$class = $class ?? $metadata->getReflectionClass();
- if (isset($this->mapping[$name = $class->getName()])) {
- $this->processFieldMapping($metadata, $this->mapping[$name]);
+ if (isset($this->mappings[$name = $class->getName()])) {
+ $this->processFieldMapping($metadata, $this->mappings[$name]);
}
foreach ($class->getTraits() as $trait) {
@@ -49,21 +49,21 @@ private function processClassFields(ClassMetadataInfo $metadata, \ReflectionClas
}
}
- private function processFieldMapping(ClassMetadataInfo $metadata, array $fields): void
+ private function processFieldMapping(ClassMetadataInfo $metadata, array $mapping): void
{
- foreach ($fields as $field => $mapping) {
+ foreach ($mapping as $field => $info) {
if ($metadata->hasField($field) || $metadata->hasAssociation($field)) {
continue;
}
- $mapping = ['fieldName' => $field] + $mapping;
+ $info = ['fieldName' => $field] + $info;
- if (isset($mapping['type']) && method_exists($metadata, $method = 'map'.ucfirst($mapping['type']))) {
- unset($mapping['type']);
- $metadata->$method($mapping);
+ if (isset($info['type']) && method_exists($metadata, $method = 'map'.ucfirst($info['type']))) {
+ unset($info['type']);
+ $metadata->$method($info);
if ('mapEmbedded' === $method) {
- $embeddableMetadata = $this->getMetadata($mapping['class']);
+ $embeddableMetadata = $this->getMetadata($info['class']);
if ($embeddableMetadata->isEmbeddedClass) {
$this->addNestedEmbeddedClasses($embeddableMetadata, $metadata, $field);
@@ -78,7 +78,7 @@ private function processFieldMapping(ClassMetadataInfo $metadata, array $fields)
$metadata->inlineEmbeddable($field, $embeddableMetadata);
}
} else {
- $metadata->mapField($mapping);
+ $metadata->mapField($info);
}
}
}
@@ -114,7 +114,7 @@ private function inheritIdGeneratorMapping(ClassMetadataInfo $class, ClassMetada
$class->setIdGeneratorType($parent->generatorType);
}
- if ($parent->idGenerator) {
+ if (!empty($parent->idGenerator)) {
$class->setIdGenerator($parent->idGenerator);
}
}
diff --git a/src/Domain/Infra/Doctrine/MappingCacheWarmer.php b/src/Domain/Infra/Doctrine/MappingCacheWarmer.php
index 59545f2e..e4dd06d3 100644
--- a/src/Domain/Infra/Doctrine/MappingCacheWarmer.php
+++ b/src/Domain/Infra/Doctrine/MappingCacheWarmer.php
@@ -14,12 +14,12 @@
*/
final class MappingCacheWarmer implements CacheWarmerInterface
{
- private $dirname;
+ private $dirName;
private $mappingFiles;
- public function __construct(string $dirname, array $mappingFiles)
+ public function __construct(string $dirName, array $mappingFiles)
{
- $this->dirname = $dirname;
+ $this->dirName = $dirName;
$this->mappingFiles = $mappingFiles;
}
@@ -31,7 +31,7 @@ public function isOptional(): bool
public function warmUp($cacheDir): void
{
$filesystem = new Filesystem();
- $filesystem->mkdir($target = $cacheDir.'/'.$this->dirname);
+ $filesystem->mkdir($target = $cacheDir.'/'.$this->dirName);
foreach ($this->mappingFiles as $file) {
$filesystem->copy($file, $target.'/'.basename($file));
diff --git a/src/Domain/Infra/Doctrine/ObjectFieldMappings.php b/src/Domain/Infra/Doctrine/ObjectFieldMappings.php
new file mode 100644
index 00000000..98dfa8f7
--- /dev/null
+++ b/src/Domain/Infra/Doctrine/ObjectFieldMappings.php
@@ -0,0 +1,45 @@
+
+ *
+ * @internal
+ */
+final class ObjectFieldMappings implements ObjectFieldMappingsProviderInterface
+{
+ public static function provideObjectFieldMappings(): iterable
+ {
+ yield Features\CanBeConfirmed::class => [
+ 'confirmationToken' => [
+ 'type' => 'string',
+ 'unique' => true,
+ 'nullable' => true,
+ ],
+ 'confirmedAt' => [
+ 'type' => 'datetime',
+ 'nullable' => true,
+ ],
+ ];
+ yield Fields\CreatedAtField::class => [
+ 'createdAt' => [
+ 'type' => 'datetime',
+ ],
+ ];
+ yield Fields\EnabledField::class => [
+ 'enabled' => [
+ 'type' => 'boolean',
+ ],
+ ];
+ yield Fields\LastUpdatedAtField::class => [
+ 'lastUpdatedAt' => [
+ 'type' => 'datetime',
+ ],
+ ];
+ }
+}
diff --git a/src/Domain/Infra/Doctrine/ObjectFieldMappingProviderInterface.php b/src/Domain/Infra/Doctrine/ObjectFieldMappingsProviderInterface.php
similarity index 77%
rename from src/Domain/Infra/Doctrine/ObjectFieldMappingProviderInterface.php
rename to src/Domain/Infra/Doctrine/ObjectFieldMappingsProviderInterface.php
index 338edcd3..ccaa0b10 100644
--- a/src/Domain/Infra/Doctrine/ObjectFieldMappingProviderInterface.php
+++ b/src/Domain/Infra/Doctrine/ObjectFieldMappingsProviderInterface.php
@@ -7,7 +7,7 @@
/**
* @author Roland Franssen
*/
-interface ObjectFieldMappingProviderInterface
+interface ObjectFieldMappingsProviderInterface
{
public const TYPE_EMBEDDED = 'embedded';
public const TYPE_MANY_TO_MANY = 'manyToMany';
@@ -15,5 +15,5 @@ interface ObjectFieldMappingProviderInterface
public const TYPE_ONE_TO_MANY = 'oneToMany';
public const TYPE_ONE_TO_ONE = 'oneToOne';
- public static function getObjectFieldMapping(): array;
+ public static function provideObjectFieldMappings(): iterable;
}
diff --git a/src/Domain/Infra/Elasticsearch/DocumentMappingProviderInterface.php b/src/Domain/Infra/Elasticsearch/DocumentMappingProviderInterface.php
new file mode 100644
index 00000000..eed11c16
--- /dev/null
+++ b/src/Domain/Infra/Elasticsearch/DocumentMappingProviderInterface.php
@@ -0,0 +1,13 @@
+
+ */
+interface DocumentMappingProviderInterface
+{
+ public static function provideDocumentMappings(): iterable;
+}
diff --git a/src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php b/src/Domain/Infra/Elasticsearch/ProjectionRepository.php
similarity index 56%
rename from src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php
rename to src/Domain/Infra/Elasticsearch/ProjectionRepository.php
index 280c9897..ce69b296 100644
--- a/src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php
+++ b/src/Domain/Infra/Elasticsearch/ProjectionRepository.php
@@ -6,12 +6,13 @@
use Elasticsearch\Client;
use Elasticsearch\Common\Exceptions\Missing404Exception;
-use MsgPhp\Domain\Projection\{DomainProjectionDocument, DomainProjectionInterface, DomainProjectionRepositoryInterface};
+use MsgPhp\Domain\{PaginatedDomainCollection, PaginatedDomainCollectionInterface};
+use MsgPhp\Domain\Projection\{ProjectionDocument, ProjectionRepositoryInterface};
/**
* @author Roland Franssen
*/
-final class DomainProjectionRepository implements DomainProjectionRepositoryInterface
+final class ProjectionRepository implements ProjectionRepositoryInterface
{
private $client;
private $index;
@@ -23,9 +24,9 @@ public function __construct(Client $client, string $index)
}
/**
- * @return DomainProjectionInterface[]
+ * @return PaginatedDomainCollectionInterface|ProjectionDocument[]
*/
- public function findAll(string $type, int $offset = 0, int $limit = 0): iterable
+ public function findAll(string $type, int $offset = 0, int $limit = 0): PaginatedDomainCollectionInterface
{
$params = [
'index' => $this->index,
@@ -40,14 +41,19 @@ public function findAll(string $type, int $offset = 0, int $limit = 0): iterable
$params['body']['size'] = $limit;
}
- $documents = $this->client->search($params);
+ $result = $this->client->search($params);
+ $documents = $result['hits']['hits'] ?? [];
+ $count = \count($documents);
+ $totalCount = $result['hits']['total'] ?? $count;
- foreach ($documents['hits']['hits'] ?? [] as $document) {
- yield $document['_type']::fromDocument($document['_source']);
- }
+ return new PaginatedDomainCollection((function () use ($documents) {
+ foreach ($documents as $document) {
+ yield $this->createDocument($document);
+ }
+ })(), (float) $offset, (float) $limit, (float) $count, (float) $totalCount);
}
- public function find(string $type, string $id): ?DomainProjectionInterface
+ public function find(string $type, string $id): ?ProjectionDocument
{
try {
$document = $this->client->get([
@@ -55,11 +61,11 @@ public function find(string $type, string $id): ?DomainProjectionInterface
'type' => $type,
'id' => $id,
]);
-
- return $document['_type']::fromDocument($document['_source']);
} catch (Missing404Exception $e) {
return null;
}
+
+ return $this->createDocument($document);
}
public function clear(string $type): void
@@ -73,7 +79,7 @@ public function clear(string $type): void
]);
}
- public function save(DomainProjectionDocument $document): void
+ public function save(ProjectionDocument $document): void
{
$params = ['index' => $this->index, 'type' => $document->getType(), 'body' => $document->getBody()];
if (null !== $id = $document->getId()) {
@@ -83,12 +89,17 @@ public function save(DomainProjectionDocument $document): void
$this->client->index($params);
}
- public function delete(DomainProjectionDocument $document): void
+ public function delete(string $type, string $id): void
{
$this->client->delete([
'index' => $this->index,
- 'type' => $document->getType(),
- 'id' => $document->getId(),
+ 'type' => $type,
+ 'id' => $id,
]);
}
+
+ private function createDocument(array $data): ProjectionDocument
+ {
+ return new ProjectionDocument($data['_type'] ?? null, $data['_id'] ?? null, $data['_source'] ?? []);
+ }
}
diff --git a/src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php b/src/Domain/Infra/Elasticsearch/ProjectionTypeRegistry.php
similarity index 50%
rename from src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php
rename to src/Domain/Infra/Elasticsearch/ProjectionTypeRegistry.php
index 289b2d44..2f325625 100644
--- a/src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php
+++ b/src/Domain/Infra/Elasticsearch/ProjectionTypeRegistry.php
@@ -5,37 +5,26 @@
namespace MsgPhp\Domain\Infra\Elasticsearch;
use Elasticsearch\Client;
-use MsgPhp\Domain\Projection\{DomainProjectionInterface, DomainProjectionTypeRegistryInterface};
+use MsgPhp\Domain\Projection\ProjectionTypeRegistryInterface;
use Psr\Log\LoggerInterface;
/**
* @author Roland Franssen
*/
-final class DomainProjectionTypeRegistry implements DomainProjectionTypeRegistryInterface
+final class ProjectionTypeRegistry implements ProjectionTypeRegistryInterface
{
+ private const DEFAULT_PROPERTY_TYPE = 'text';
+
private $client;
private $index;
private $mappings;
private $settings;
private $logger;
private $types;
+ private $indexParams;
public function __construct(Client $client, string $index, array $mappings, array $settings = [], LoggerInterface $logger = null)
{
- foreach ($mappings as $type => $mapping) {
- if (!isset($mapping['properties']) || !is_array($mapping['properties'])) {
- continue;
- }
-
- foreach ($mapping['properties'] as $property => $info) {
- if (!is_array($info)) {
- $info = ['type' => $info ?? 'text'];
- }
-
- $mappings[$type]['properties'][$property] = $info + ['type' => 'text'];
- }
- }
-
$this->client = $client;
$this->index = $index;
$this->mappings = $mappings;
@@ -48,16 +37,7 @@ public function __construct(Client $client, string $index, array $mappings, arra
*/
public function all(): array
{
- if (null === $this->types) {
- $this->types = [];
- foreach (array_keys($this->mappings) as $type) {
- if (is_subclass_of($type, DomainProjectionInterface::class)) {
- $this->types[] = $type;
- }
- }
- }
-
- return $this->types;
+ return $this->types ?? ($this->types = array_keys($this->getIndexParams()['body']['mappings'] ?? []));
}
public function initialize(): void
@@ -68,15 +48,7 @@ public function initialize(): void
return;
}
- if ($this->settings) {
- $params['body']['settings'] = $this->settings;
- }
-
- if ($this->mappings) {
- $params['body']['mappings'] = $this->mappings;
- }
-
- $indices->create($params);
+ $indices->create($params + $this->getIndexParams());
if (null !== $this->logger) {
$this->logger->info('Initialized Elasticsearch index "{index}".', ['index' => $this->index]);
@@ -97,4 +69,46 @@ public function destroy(): void
$this->logger->info('Destroyed Elasticsearch index "{index}".', ['index' => $this->index]);
}
}
+
+ private function getIndexParams(): array
+ {
+ if (null !== $this->indexParams) {
+ return $this->indexParams;
+ }
+
+ $params = [];
+
+ if ($this->settings) {
+ $params['body']['settings'] = $this->settings;
+ }
+
+ foreach ($this->provideMappings() as $type => $mapping) {
+ foreach ($mapping as $property => $propertyMapping) {
+ if (!\is_array($propertyMapping)) {
+ $propertyMapping = ['type' => $propertyMapping];
+ } elseif (!isset($propertyMapping['type'])) {
+ $propertyMapping['type'] = self::DEFAULT_PROPERTY_TYPE;
+ }
+
+ $params['body']['mappings'][$type]['properties'][$property] = $propertyMapping;
+ }
+ }
+
+ return $this->indexParams = $params;
+ }
+
+ private function provideMappings(): iterable
+ {
+ foreach ($this->mappings as $type => $mapping) {
+ if (\is_string($mapping)) {
+ if (!class_exists($mapping) || !is_subclass_of($mapping, DocumentMappingProviderInterface::class)) {
+ throw new \LogicException(sprintf('The class "%s" does not exists or is not a sub class of "%s".', $mapping, DocumentMappingProviderInterface::class));
+ }
+
+ yield from $mapping::provideDocumentMappings();
+ } else {
+ yield $type => $mapping;
+ }
+ }
+ }
}
diff --git a/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php b/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php
index b6f5d10b..32581b22 100644
--- a/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php
+++ b/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php
@@ -108,7 +108,7 @@ private function doExistsByFields(array $fields): bool
private function doSave($entity): void
{
if (!$entity instanceof $this->class) {
- throw InvalidClassException::create(get_class($entity));
+ throw InvalidClassException::create(\get_class($entity));
}
if ($this->memory->contains($entity)) {
@@ -116,7 +116,7 @@ private function doSave($entity): void
}
if (null !== ($id = $this->identityHelper->getIdentity($entity)) && $this->doExists($id)) {
- throw DuplicateEntityException::createForId(get_class($entity), $id);
+ throw DuplicateEntityException::createForId(\get_class($entity), $id);
}
$this->memory->persist($entity);
@@ -128,7 +128,7 @@ private function doSave($entity): void
private function doDelete($entity): void
{
if (!$entity instanceof $this->class) {
- throw InvalidClassException::create(get_class($entity));
+ throw InvalidClassException::create(\get_class($entity));
}
$this->memory->remove($entity);
@@ -137,7 +137,7 @@ private function doDelete($entity): void
private function createResultSet(array $entities, int $offset = 0, int $limit = 0): DomainCollectionInterface
{
if ($offset || $limit) {
- $entities = array_slice($entities, $offset, $limit ?: null);
+ $entities = \array_slice($entities, $offset, $limit ?: null);
}
return DomainCollectionFactory::create($entities);
@@ -148,7 +148,7 @@ private function createResultSet(array $entities, int $offset = 0, int $limit =
*/
private function matchesFields($entity, array $fields): bool
{
- $idFields = array_flip($this->identityHelper->getIdentifierFieldNames(get_class($entity)));
+ $idFields = array_flip($this->identityHelper->getIdentifierFieldNames(\get_class($entity)));
foreach ($fields as $field => $value) {
if (isset($idFields[$field]) && $this->identityHelper->isEmptyIdentifier($value)) {
return false;
@@ -156,8 +156,12 @@ private function matchesFields($entity, array $fields): bool
$knownValue = $this->identityHelper->normalizeIdentifier($this->accessor->getValue($entity, $field));
- if (is_array($value)) {
- if (!in_array($knownValue, array_map([$this->identityHelper, 'normalizeIdentifier'], $value)) || in_array(null, $value, true)) {
+ if (\is_array($value)) {
+ $value = array_map(function ($v) {
+ return $this->identityHelper->normalizeIdentifier($v);
+ }, $value);
+
+ if (null === $knownValue || !\in_array($knownValue, $value)) {
return false;
}
diff --git a/src/Domain/Infra/InMemory/DomainIdentityMapping.php b/src/Domain/Infra/InMemory/DomainIdentityMapping.php
index a39f7f67..bdfb8aa5 100644
--- a/src/Domain/Infra/InMemory/DomainIdentityMapping.php
+++ b/src/Domain/Infra/InMemory/DomainIdentityMapping.php
@@ -38,7 +38,7 @@ public function getIdentity($object): array
{
$ids = [];
- foreach ($this->getIdentifierFieldNames(get_class($object)) as $field) {
+ foreach ($this->getIdentifierFieldNames(\get_class($object)) as $field) {
if (null === ($value = $this->accessor->getValue($object, $field))) {
continue;
}
@@ -47,7 +47,7 @@ public function getIdentity($object): array
if ($value->isEmpty()) {
continue;
}
- } elseif (is_object($value) && !$this->getIdentity($value)) {
+ } elseif (\is_object($value) && !$this->getIdentity($value)) {
continue;
}
diff --git a/src/Domain/Infra/InMemory/GlobalObjectMemory.php b/src/Domain/Infra/InMemory/GlobalObjectMemory.php
index 99d119a1..39947817 100644
--- a/src/Domain/Infra/InMemory/GlobalObjectMemory.php
+++ b/src/Domain/Infra/InMemory/GlobalObjectMemory.php
@@ -42,7 +42,7 @@ public function contains($object): bool
*/
public function persist($object): void
{
- $this->storage->attach($object, get_class($object));
+ $this->storage->attach($object, \get_class($object));
}
/**
diff --git a/src/Domain/Infra/InMemory/ObjectFieldAccessor.php b/src/Domain/Infra/InMemory/ObjectFieldAccessor.php
index 06d446be..9fbaafe6 100644
--- a/src/Domain/Infra/InMemory/ObjectFieldAccessor.php
+++ b/src/Domain/Infra/InMemory/ObjectFieldAccessor.php
@@ -38,6 +38,6 @@ public function getValue($object, string $field)
return $object->$field;
}
- throw new \RuntimeException(sprintf('Unknown field name "%s" for object "%s".', $field, get_class($object)));
+ throw new \RuntimeException(sprintf('Unknown field name "%s" for object "%s".', $field, \get_class($object)));
}
}
diff --git a/src/Domain/Infra/Messenger/DomainMessageBus.php b/src/Domain/Infra/Messenger/DomainMessageBus.php
new file mode 100644
index 00000000..e5175599
--- /dev/null
+++ b/src/Domain/Infra/Messenger/DomainMessageBus.php
@@ -0,0 +1,26 @@
+
+ */
+final class DomainMessageBus implements DomainMessageBusInterface
+{
+ private $bus;
+
+ public function __construct(MessageBusInterface $bus)
+ {
+ $this->bus = $bus;
+ }
+
+ public function dispatch($message): void
+ {
+ $this->bus->dispatch($message);
+ }
+}
diff --git a/src/Domain/Infra/Messenger/Middleware/ConsoleMessageReceiverMiddleware.php b/src/Domain/Infra/Messenger/Middleware/ConsoleMessageReceiverMiddleware.php
new file mode 100644
index 00000000..39ce2677
--- /dev/null
+++ b/src/Domain/Infra/Messenger/Middleware/ConsoleMessageReceiverMiddleware.php
@@ -0,0 +1,30 @@
+
+ */
+final class ConsoleMessageReceiverMiddleware implements MiddlewareInterface
+{
+ private $receiver;
+
+ public function __construct(MessageReceiver $receiver)
+ {
+ $this->receiver = $receiver;
+ }
+
+ public function handle(Envelope $envelope, StackInterface $stack): Envelope
+ {
+ $this->receiver->receive($envelope->getMessage());
+
+ return $stack->next()->handle($envelope, $stack);
+ }
+}
diff --git a/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php b/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php
deleted file mode 100644
index e0aed3d0..00000000
--- a/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php
+++ /dev/null
@@ -1,31 +0,0 @@
-
- */
-final class DoctrineAwareClassContainer implements ContainerInterface
-{
- private $container;
-
- public function __construct(ContainerInterface $container)
- {
- $this->container = $container;
- }
-
- public function has($id)
- {
- return $this->container->has(ClassUtils::getRealClass($id));
- }
-
- public function get($id)
- {
- return $this->container->get(ClassUtils::getRealClass($id));
- }
-}
diff --git a/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php b/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php
deleted file mode 100644
index 2966be04..00000000
--- a/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- */
-final class DomainProjectionDocumentTransformer implements DomainProjectionDocumentTransformerInterface
-{
- private $transformers;
-
- public function __construct(ContainerInterface $transformers)
- {
- $this->transformers = $transformers;
- }
-
- public function transform($object): DomainProjectionDocument
- {
- if (!$this->transformers->has($class = get_class($object))) {
- throw new \LogicException(sprintf('No projection document transformer available for class "%s".', $class));
- }
-
- if (!is_callable($transformer = $this->transformers->get($class))) {
- throw new \LogicException(sprintf('Projection document transformer for class "%s" must be a callable, got "%s".', $class, gettype($transformer)));
- }
-
- $document = $transformer($object);
-
- if (!$document instanceof DomainProjectionDocument) {
- throw new \LogicException(sprintf('Projection document transformer for class "%s" must return an instance of "%s", got "%s".', $class, DomainProjectionDocument::class, is_object($document) ? get_class($document) : gettype($document)));
- }
-
- $document->source = $object;
-
- return $document;
- }
-}
diff --git a/src/Domain/Infra/SimpleBus/DomainMessageBus.php b/src/Domain/Infra/SimpleBus/DomainMessageBus.php
index f8de9e80..5d3662e5 100644
--- a/src/Domain/Infra/SimpleBus/DomainMessageBus.php
+++ b/src/Domain/Infra/SimpleBus/DomainMessageBus.php
@@ -19,10 +19,8 @@ public function __construct(MessageBus $bus)
$this->bus = $bus;
}
- public function dispatch($message)
+ public function dispatch($message): void
{
$this->bus->handle($message);
-
- return null;
}
}
diff --git a/src/Domain/Infra/SimpleBus/EventMessageHandler.php b/src/Domain/Infra/SimpleBus/EventMessageHandler.php
deleted file mode 100644
index cedce3ca..00000000
--- a/src/Domain/Infra/SimpleBus/EventMessageHandler.php
+++ /dev/null
@@ -1,49 +0,0 @@
-
- */
-final class EventMessageHandler
-{
- private $bus;
-
- /** @var MessageReceivingInterface|null */
- private $receiver;
-
- public function __construct(MessageBus $bus = null)
- {
- $this->bus = $bus;
- }
-
- /**
- * @param object $message
- */
- public function __invoke($message): void
- {
- if (null !== $this->bus) {
- $this->bus->handle($message);
- }
-
- if (null !== $this->receiver) {
- $this->receiver->onMessageReceived($message);
- }
- }
-
- public function onConsoleCommand(ConsoleCommandEvent $event): void
- {
- $this->receiver = ($command = $event->getCommand()) instanceof MessageReceivingInterface ? $command : null;
- }
-
- public function onConsoleTerminate(): void
- {
- $this->receiver = null;
- }
-}
diff --git a/src/Domain/Infra/SimpleBus/Middleware/ConsoleMessageReceiverMiddleware.php b/src/Domain/Infra/SimpleBus/Middleware/ConsoleMessageReceiverMiddleware.php
new file mode 100644
index 00000000..35dad54c
--- /dev/null
+++ b/src/Domain/Infra/SimpleBus/Middleware/ConsoleMessageReceiverMiddleware.php
@@ -0,0 +1,28 @@
+
+ */
+final class ConsoleMessageReceiverMiddleware implements MessageBusMiddleware
+{
+ private $receiver;
+
+ public function __construct(MessageReceiver $receiver)
+ {
+ $this->receiver = $receiver;
+ }
+
+ public function handle($message, callable $next): void
+ {
+ $this->receiver->receive($message);
+
+ $next($message);
+ }
+}
diff --git a/src/Domain/Infra/Uuid/DomainId.php b/src/Domain/Infra/Uuid/DomainId.php
index ae0979db..463a7f4b 100644
--- a/src/Domain/Infra/Uuid/DomainId.php
+++ b/src/Domain/Infra/Uuid/DomainId.php
@@ -16,7 +16,7 @@ class DomainId implements DomainIdInterface
private $uuid;
/**
- * @return $this|self
+ * @return static
*/
final public static function fromValue($value): DomainIdInterface
{
@@ -39,7 +39,7 @@ final public function isEmpty(): bool
final public function equals(DomainIdInterface $id): bool
{
- return $id === $this ? true : ($id instanceof self && static::class === get_class($id) ? $this->uuid->equals($id->uuid) : false);
+ return $id === $this ? true : ($id instanceof self && static::class === \get_class($id) ? $this->uuid->equals($id->uuid) : false);
}
final public function toString(): string
diff --git a/src/Domain/Message/DomainMessageBus.php b/src/Domain/Message/DomainMessageBus.php
new file mode 100644
index 00000000..af98c028
--- /dev/null
+++ b/src/Domain/Message/DomainMessageBus.php
@@ -0,0 +1,31 @@
+
+ */
+final class DomainMessageBus implements DomainMessageBusInterface
+{
+ private $commandBus;
+ private $eventBus;
+ private $eventClasses;
+
+ public function __construct(DomainMessageBusInterface $commandBus, DomainMessageBusInterface $eventBus, array $eventClasses)
+ {
+ $this->commandBus = $commandBus;
+ $this->eventBus = $eventBus;
+ $this->eventClasses = array_flip($eventClasses);
+ }
+
+ public function dispatch($message): void
+ {
+ if (isset($this->eventClasses[\get_class($message)])) {
+ $this->eventBus->dispatch($message);
+ } else {
+ $this->commandBus->dispatch($message);
+ }
+ }
+}
diff --git a/src/Domain/Message/DomainMessageBusInterface.php b/src/Domain/Message/DomainMessageBusInterface.php
index 63128a6c..73f1cf87 100644
--- a/src/Domain/Message/DomainMessageBusInterface.php
+++ b/src/Domain/Message/DomainMessageBusInterface.php
@@ -12,5 +12,5 @@ interface DomainMessageBusInterface
/**
* @param object $message
*/
- public function dispatch($message);
+ public function dispatch($message): void;
}
diff --git a/src/Domain/Message/MessageDispatchingTrait.php b/src/Domain/Message/MessageDispatchingTrait.php
index 257ee1c1..893174c0 100644
--- a/src/Domain/Message/MessageDispatchingTrait.php
+++ b/src/Domain/Message/MessageDispatchingTrait.php
@@ -20,8 +20,8 @@ public function __construct(DomainObjectFactoryInterface $factory, DomainMessage
$this->bus = $bus;
}
- private function dispatch(string $class, array $context = [])
+ private function dispatch(string $class, array $context = []): void
{
- return $this->bus->dispatch($this->factory->create($class, $context));
+ $this->bus->dispatch($this->factory->create($class, $context));
}
}
diff --git a/src/Domain/PaginatedDomainCollection.php b/src/Domain/PaginatedDomainCollection.php
new file mode 100644
index 00000000..b3bd5971
--- /dev/null
+++ b/src/Domain/PaginatedDomainCollection.php
@@ -0,0 +1,123 @@
+
+ */
+final class PaginatedDomainCollection implements PaginatedDomainCollectionInterface
+{
+ private $collection;
+ private $offset;
+ private $limit;
+ private $count;
+ private $totalCount;
+
+ public static function fromValue(?iterable $value): DomainCollectionInterface
+ {
+ return new self($value ?? []);
+ }
+
+ public function __construct(iterable $elements, float $offset = .0, float $limit = .0, float $count = null, float $totalCount = null)
+ {
+ if (null !== $count) {
+ $count = (int) $count;
+ }
+
+ $this->collection = $elements instanceof DomainCollectionInterface ? $elements : DomainCollection::fromValue($elements);
+ $this->offset = $offset;
+ $this->limit = $limit;
+ $this->count = $count;
+ $this->totalCount = $totalCount;
+ }
+
+ public function getIterator(): \Traversable
+ {
+ return $this->collection->getIterator();
+ }
+
+ public function isEmpty(): bool
+ {
+ return $this->collection->isEmpty();
+ }
+
+ public function contains($element): bool
+ {
+ return $this->collection->contains($element);
+ }
+
+ public function containsKey($key): bool
+ {
+ return $this->collection->containsKey($key);
+ }
+
+ public function first()
+ {
+ return $this->collection->first();
+ }
+
+ public function last()
+ {
+ return $this->collection->last();
+ }
+
+ public function get($key)
+ {
+ return $this->collection->get($key);
+ }
+
+ public function filter(callable $filter): DomainCollectionInterface
+ {
+ return $this->collection->filter($filter);
+ }
+
+ public function slice(int $offset, int $limit = 0): DomainCollectionInterface
+ {
+ return $this->collection->slice($offset, $limit);
+ }
+
+ public function map(callable $mapper): array
+ {
+ return $this->collection->map($mapper);
+ }
+
+ public function count(): int
+ {
+ return $this->count ?? $this->count = \count($this->collection);
+ }
+
+ public function getOffset(): float
+ {
+ return $this->offset;
+ }
+
+ public function getLimit(): float
+ {
+ return $this->limit;
+ }
+
+ public function getCurrentPage(): float
+ {
+ if (0 >= $this->limit) {
+ return 1.;
+ }
+
+ return floor($this->offset / $this->limit) + 1.;
+ }
+
+ public function getLastPage(): float
+ {
+ if (0 >= $this->limit) {
+ return 1.;
+ }
+
+ return ceil($this->getTotalCount() / $this->limit) ?: 1.;
+ }
+
+ public function getTotalCount(): float
+ {
+ return $this->totalCount ?? $this->totalCount = (float) \count($this);
+ }
+}
diff --git a/src/Domain/PaginatedDomainCollectionInterface.php b/src/Domain/PaginatedDomainCollectionInterface.php
new file mode 100644
index 00000000..0ad7b634
--- /dev/null
+++ b/src/Domain/PaginatedDomainCollectionInterface.php
@@ -0,0 +1,21 @@
+
+ */
+interface PaginatedDomainCollectionInterface extends DomainCollectionInterface
+{
+ public function getOffset(): float;
+
+ public function getLimit(): float;
+
+ public function getCurrentPage(): float;
+
+ public function getLastPage(): float;
+
+ public function getTotalCount(): float;
+}
diff --git a/src/Domain/Projection/DomainProjectionRepositoryInterface.php b/src/Domain/Projection/DomainProjectionRepositoryInterface.php
deleted file mode 100644
index e85aafdf..00000000
--- a/src/Domain/Projection/DomainProjectionRepositoryInterface.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-interface DomainProjectionRepositoryInterface
-{
- /**
- * @return DomainProjectionInterface[]
- */
- public function findAll(string $type, int $offset = 0, int $limit = 0): iterable;
-
- public function find(string $type, string $id): ?DomainProjectionInterface;
-
- public function clear(string $type): void;
-
- public function save(DomainProjectionDocument $document): void;
-
- public function delete(DomainProjectionDocument $document): void;
-}
diff --git a/src/Domain/Projection/DomainProjectionSynchronization.php b/src/Domain/Projection/DomainProjectionSynchronization.php
deleted file mode 100644
index 98386006..00000000
--- a/src/Domain/Projection/DomainProjectionSynchronization.php
+++ /dev/null
@@ -1,61 +0,0 @@
-
- */
-final class DomainProjectionSynchronization
-{
- private $typeRegistry;
- private $repository;
- private $providers;
- private $transformer;
-
- public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry, DomainProjectionRepositoryInterface $repository, DomainProjectionDocumentTransformerInterface $transformer, iterable $providers)
- {
- $this->typeRegistry = $typeRegistry;
- $this->repository = $repository;
- $this->providers = $providers;
- $this->transformer = $transformer;
- }
-
- /**
- * @return DomainProjectionDocument[]
- */
- public function synchronize(): iterable
- {
- foreach ($this->typeRegistry->all() as $type) {
- $this->repository->clear($type);
- }
-
- foreach ($this->providers as $provider) {
- foreach ($provider() as $object) {
- try {
- $document = $this->transformer->transform($object);
- } catch (\Exception $e) {
- $document = new DomainProjectionDocument();
- $document->status = DomainProjectionDocument::STATUS_FAILED_TRANSFORMATION;
- $document->source = $object;
- $document->error = $e;
-
- yield $document;
- continue;
- }
-
- try {
- $document->status = DomainProjectionDocument::STATUS_VALID;
-
- $this->repository->save($document);
- } catch (\Exception $e) {
- $document->status = DomainProjectionDocument::STATUS_FAILED_SAVING;
- $document->error = $e;
- } finally {
- yield $document;
- }
- }
- }
- }
-}
diff --git a/src/Domain/Projection/DomainProjectionDocument.php b/src/Domain/Projection/ProjectionDocument.php
similarity index 68%
rename from src/Domain/Projection/DomainProjectionDocument.php
rename to src/Domain/Projection/ProjectionDocument.php
index a2a01b59..c48174a1 100644
--- a/src/Domain/Projection/DomainProjectionDocument.php
+++ b/src/Domain/Projection/ProjectionDocument.php
@@ -7,10 +7,10 @@
/**
* @author Roland Franssen
*/
-final class DomainProjectionDocument
+final class ProjectionDocument
{
public const STATUS_UNKNOWN = 1;
- public const STATUS_VALID = 2;
+ public const STATUS_SYNCHRONIZED = 2;
public const STATUS_FAILED_TRANSFORMATION = 3;
public const STATUS_FAILED_SAVING = 4;
@@ -20,12 +20,12 @@ final class DomainProjectionDocument
/** @var object|null */
public $source;
- /** @var \Exception|null $error */
+ /** @var \Throwable|null $error */
public $error;
private $type;
private $id;
- private $body = [];
+ private $body;
public function __construct(string $type = null, string $id = null, array $body = [])
{
@@ -34,16 +34,8 @@ public function __construct(string $type = null, string $id = null, array $body
$this->body = $body;
}
- public function getType(): string
+ public function getType(): ?string
{
- if (null === $this->type) {
- throw new \LogicException('Document type not set.');
- }
-
- if (!is_subclass_of($this->type, DomainProjectionInterface::class)) {
- throw new \LogicException(sprintf('Document type must be a sub class of "%s", got "%s".', DomainProjectionInterface::class, $this->type));
- }
-
return $this->type;
}
@@ -57,8 +49,16 @@ public function getBody(): array
return $this->body;
}
- public function toProjection(): DomainProjectionInterface
+ public function toProjection(): ProjectionInterface
{
- return $this->getType()::fromDocument($this->body);
+ if (null === $this->type) {
+ throw new \LogicException('Document type not set.');
+ }
+
+ if (!is_subclass_of($this->type, ProjectionInterface::class)) {
+ throw new \LogicException(sprintf('Document type must be a sub class of "%s", got "%s".', ProjectionInterface::class, $this->type));
+ }
+
+ return $this->type::fromDocument($this->body);
}
}
diff --git a/src/Domain/Projection/ProjectionDocumentProvider.php b/src/Domain/Projection/ProjectionDocumentProvider.php
new file mode 100644
index 00000000..bed806fa
--- /dev/null
+++ b/src/Domain/Projection/ProjectionDocumentProvider.php
@@ -0,0 +1,41 @@
+
+ */
+final class ProjectionDocumentProvider implements \IteratorAggregate
+{
+ private $transformer;
+ private $dataProviders;
+
+ /**
+ * @param callable[] $dataProviders
+ */
+ public function __construct(ProjectionDocumentTransformerInterface $transformer, iterable $dataProviders)
+ {
+ $this->transformer = $transformer;
+ $this->dataProviders = $dataProviders;
+ }
+
+ public function getIterator(): \Traversable
+ {
+ foreach ($this->dataProviders as $dataProvider) {
+ foreach ($dataProvider() as $object) {
+ try {
+ $document = $this->transformer->transform($object);
+ } catch (\Throwable $e) {
+ $document = new ProjectionDocument();
+ $document->status = ProjectionDocument::STATUS_FAILED_TRANSFORMATION;
+ $document->source = $object;
+ $document->error = $e;
+ }
+
+ yield $document;
+ }
+ }
+ }
+}
diff --git a/src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php b/src/Domain/Projection/ProjectionDocumentTransformerInterface.php
similarity index 60%
rename from src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php
rename to src/Domain/Projection/ProjectionDocumentTransformerInterface.php
index f3881a50..2c770d98 100644
--- a/src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php
+++ b/src/Domain/Projection/ProjectionDocumentTransformerInterface.php
@@ -7,10 +7,10 @@
/**
* @author Roland Franssen
*/
-interface DomainProjectionDocumentTransformerInterface
+interface ProjectionDocumentTransformerInterface
{
/**
* @param object $object
*/
- public function transform($object): DomainProjectionDocument;
+ public function transform($object): ProjectionDocument;
}
diff --git a/src/Domain/Projection/DomainProjectionInterface.php b/src/Domain/Projection/ProjectionInterface.php
similarity index 84%
rename from src/Domain/Projection/DomainProjectionInterface.php
rename to src/Domain/Projection/ProjectionInterface.php
index c129204f..0cd141ae 100644
--- a/src/Domain/Projection/DomainProjectionInterface.php
+++ b/src/Domain/Projection/ProjectionInterface.php
@@ -7,7 +7,7 @@
/**
* @author Roland Franssen
*/
-interface DomainProjectionInterface
+interface ProjectionInterface
{
public static function fromDocument(array $document): self;
}
diff --git a/src/Domain/Projection/ProjectionRepositoryInterface.php b/src/Domain/Projection/ProjectionRepositoryInterface.php
new file mode 100644
index 00000000..73846b55
--- /dev/null
+++ b/src/Domain/Projection/ProjectionRepositoryInterface.php
@@ -0,0 +1,26 @@
+
+ */
+interface ProjectionRepositoryInterface
+{
+ /**
+ * @return PaginatedDomainCollectionInterface|ProjectionDocument[]
+ */
+ public function findAll(string $type, int $offset = 0, int $limit = 0): PaginatedDomainCollectionInterface;
+
+ public function find(string $type, string $id): ?ProjectionDocument;
+
+ public function clear(string $type): void;
+
+ public function save(ProjectionDocument $document): void;
+
+ public function delete(string $type, string $id): void;
+}
diff --git a/src/Domain/Projection/ProjectionSynchronization.php b/src/Domain/Projection/ProjectionSynchronization.php
new file mode 100644
index 00000000..24d79186
--- /dev/null
+++ b/src/Domain/Projection/ProjectionSynchronization.php
@@ -0,0 +1,52 @@
+
+ */
+final class ProjectionSynchronization
+{
+ private $typeRegistry;
+ private $repository;
+ private $documentProvider;
+
+ /**
+ * @param ProjectionDocument[] $documentProvider
+ */
+ public function __construct(ProjectionTypeRegistryInterface $typeRegistry, ProjectionRepositoryInterface $repository, iterable $documentProvider)
+ {
+ $this->typeRegistry = $typeRegistry;
+ $this->repository = $repository;
+ $this->documentProvider = $documentProvider;
+ }
+
+ /**
+ * @return ProjectionDocument[]
+ */
+ public function synchronize(): iterable
+ {
+ foreach ($this->typeRegistry->all() as $type) {
+ $this->repository->clear($type);
+ }
+
+ foreach ($this->documentProvider as $document) {
+ if (ProjectionDocument::STATUS_UNKNOWN !== $document->status) {
+ continue;
+ }
+
+ try {
+ $document->status = ProjectionDocument::STATUS_SYNCHRONIZED;
+
+ $this->repository->save($document);
+ } catch (\Throwable $e) {
+ $document->status = ProjectionDocument::STATUS_FAILED_SAVING;
+ $document->error = $e;
+ }
+
+ yield $document;
+ }
+ }
+}
diff --git a/src/Domain/Projection/DomainProjectionTypeRegistryInterface.php b/src/Domain/Projection/ProjectionTypeRegistryInterface.php
similarity index 85%
rename from src/Domain/Projection/DomainProjectionTypeRegistryInterface.php
rename to src/Domain/Projection/ProjectionTypeRegistryInterface.php
index 8d4f148a..11027ef0 100644
--- a/src/Domain/Projection/DomainProjectionTypeRegistryInterface.php
+++ b/src/Domain/Projection/ProjectionTypeRegistryInterface.php
@@ -7,7 +7,7 @@
/**
* @author Roland Franssen
*/
-interface DomainProjectionTypeRegistryInterface
+interface ProjectionTypeRegistryInterface
{
/**
* @return string[]
diff --git a/src/Domain/README.md b/src/Domain/README.md
index c6b1e737..d4badeef 100644
--- a/src/Domain/README.md
+++ b/src/Domain/README.md
@@ -1,4 +1,4 @@
-# Base domain layer
+# Base Domain Layer
A base domain layer to rapidly built other domain layers.
@@ -20,10 +20,12 @@ composer require msgphp/domain
- Common domain events
- Interface bound collection, identifier, factory & message bus
- Abstract entity repositories
+- Projection document model
- Common infrastructure
-## Blog posts
+## Blog Posts
+- [Domain-driven-design: Projections in practice with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-projections-in-practice-with-api-platform-and-elasticsearch-c785ed6d660b)
- [Domain-driven-design: Moving forward with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-moving-forward-with-api-platform-and-elasticsearch-f1705614f9e2)
- [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374)
- [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f)
diff --git a/src/Domain/Tests/AbstractDomainCollectionTest.php b/src/Domain/Tests/AbstractDomainCollectionTest.php
index 1718a700..9df0eb5e 100644
--- a/src/Domain/Tests/AbstractDomainCollectionTest.php
+++ b/src/Domain/Tests/AbstractDomainCollectionTest.php
@@ -11,13 +11,13 @@ abstract class AbstractDomainCollectionTest extends TestCase
{
public function testFromValue(): void
{
- $class = get_class(static::createCollection([]));
+ $class = \get_class(static::createCollection([]));
- $this->assertSame([], iterator_to_array($class::fromValue(null)));
- $this->assertSame([1], iterator_to_array($class::fromValue([1])));
- $this->assertSame([1, 'k' => '2'], iterator_to_array($class::fromValue([1, 'k' => '2'])));
- $this->assertSame([1, 2, 3], iterator_to_array($class::fromValue(new \ArrayIterator([1, 2, 3]))));
- $this->assertEquals(['k' => 1, 2, '3' => 'v'], iterator_to_array($class::fromValue((function () {
+ self::assertSame([], iterator_to_array($class::fromValue(null)));
+ self::assertSame([1], iterator_to_array($class::fromValue([1])));
+ self::assertSame([1, 'k' => '2'], iterator_to_array($class::fromValue([1, 'k' => '2'])));
+ self::assertSame([1, 2, 3], iterator_to_array($class::fromValue(new \ArrayIterator([1, 2, 3]))));
+ self::assertEquals(['k' => 1, 2, '3' => 'v'], iterator_to_array($class::fromValue((function () {
yield from ['k' => 1, 2];
yield '3' => 'v';
})())));
@@ -25,101 +25,101 @@ public function testFromValue(): void
public function testGetIterator(): void
{
- $this->assertSame([], iterator_to_array(static::createCollection([])));
- $this->assertSame([1], iterator_to_array(static::createCollection([1])));
- $this->assertSame([null], iterator_to_array(static::createCollection([null])));
- $this->assertSame(['k' => 'v'], iterator_to_array(static::createCollection(['k' => 'v'])));
+ self::assertSame([], iterator_to_array(static::createCollection([])));
+ self::assertSame([1], iterator_to_array(static::createCollection([1])));
+ self::assertSame([null], iterator_to_array(static::createCollection([null])));
+ self::assertSame(['k' => 'v'], iterator_to_array(static::createCollection(['k' => 'v'])));
}
public function testIsEmpty(): void
{
- $this->assertTrue(static::createCollection([])->isEmpty());
- $this->assertFalse(static::createCollection([1])->isEmpty());
- $this->assertFalse(static::createCollection([null])->isEmpty());
+ self::assertTrue(static::createCollection([])->isEmpty());
+ self::assertFalse(static::createCollection([1])->isEmpty());
+ self::assertFalse(static::createCollection([null])->isEmpty());
}
public function testContains(): void
{
- $this->assertFalse(static::createCollection([])->contains(1));
- $this->assertTrue(static::createCollection([null])->contains(null));
- $this->assertTrue(($collection = static::createCollection([1, '2']))->contains(1));
- $this->assertFalse($collection->contains(2));
- $this->assertFalse($collection->contains(null));
+ self::assertFalse(static::createCollection([])->contains(1));
+ self::assertTrue(static::createCollection([null])->contains(null));
+ self::assertTrue(($collection = static::createCollection([1, '2']))->contains(1));
+ self::assertFalse($collection->contains(2));
+ self::assertFalse($collection->contains(null));
}
public function testContainsKey(): void
{
- $this->assertFalse(static::createCollection([])->containsKey(1));
- $this->assertTrue(($collection = static::createCollection([1, 'k' => 'v', '2' => null]))->containsKey(0));
- $this->assertTrue($collection->containsKey('k'));
- $this->assertTrue($collection->containsKey(2));
- $this->assertTrue($collection->containsKey('2'));
- $this->assertFalse($collection->containsKey(1));
+ self::assertFalse(static::createCollection([])->containsKey(1));
+ self::assertTrue(($collection = static::createCollection([1, 'k' => 'v', '2' => null]))->containsKey(0));
+ self::assertTrue($collection->containsKey('k'));
+ self::assertTrue($collection->containsKey(2));
+ self::assertTrue($collection->containsKey('2'));
+ self::assertFalse($collection->containsKey(1));
}
public function testFirst(): void
{
- $this->assertFalse(static::createCollection([])->first());
- $this->assertSame(1, static::createCollection([1])->first());
- $this->assertSame(1, static::createCollection([1, 2])->first());
- $this->assertNull(static::createCollection([null, 2])->first());
+ self::assertFalse(static::createCollection([])->first());
+ self::assertSame(1, static::createCollection([1])->first());
+ self::assertSame(1, static::createCollection([1, 2])->first());
+ self::assertNull(static::createCollection([null, 2])->first());
}
public function testLast(): void
{
- $this->assertFalse(static::createCollection([])->last());
- $this->assertSame(1, static::createCollection([1])->last());
- $this->assertSame(2, static::createCollection([1, 2])->last());
- $this->assertNull(static::createCollection([1, null])->last());
+ self::assertFalse(static::createCollection([])->last());
+ self::assertSame(1, static::createCollection([1])->last());
+ self::assertSame(2, static::createCollection([1, 2])->last());
+ self::assertNull(static::createCollection([1, null])->last());
}
public function testGet(): void
{
- $this->assertNull(static::createCollection([])->get(0));
- $this->assertNull(static::createCollection([])->get('k'));
- $this->assertSame(1, ($collection = static::createCollection([1, 'k' => 'v', 2 => null]))->get(0));
- $this->assertSame(1, $collection->get('0'));
- $this->assertNull($collection->get(2));
- $this->assertNull($collection->get('2'));
+ self::assertNull(static::createCollection([])->get(0));
+ self::assertNull(static::createCollection([])->get('k'));
+ self::assertSame(1, ($collection = static::createCollection([1, 'k' => 'v', 2 => null]))->get(0));
+ self::assertSame(1, $collection->get('0'));
+ self::assertNull($collection->get(2));
+ self::assertNull($collection->get('2'));
}
public function testFilter(): void
{
- $this->assertNotSame($collection = static::createCollection([]), $filtered = $collection->filter(function (): bool {
+ self::assertNotSame($collection = static::createCollection([]), $filtered = $collection->filter(function (): bool {
return true;
}));
- $this->assertSame([], iterator_to_array($filtered));
- $this->assertSame([1, 2 => 3], iterator_to_array(static::createCollection([1, null, 3])->filter(function ($v): bool {
+ self::assertSame([], iterator_to_array($filtered));
+ self::assertSame([1, 2 => 3], iterator_to_array(static::createCollection([1, null, 3])->filter(function ($v): bool {
return null !== $v;
})));
}
public function testSlice(): void
{
- $this->assertNotSame($collection = static::createCollection([]), $slice = $collection->slice(0));
- $this->assertSame([], iterator_to_array($slice));
- $this->assertSame([3 => null, 4 => 5], iterator_to_array(($collection = static::createCollection([1, 2, 3, null, 5]))->slice(3)));
- $this->assertSame([1], iterator_to_array($collection->slice(0, 1)));
- $this->assertSame([1 => 2], iterator_to_array($collection->slice(1, 1)));
- $this->assertSame([4 => 5], iterator_to_array($collection->slice(4, 20)));
- $this->assertSame([], iterator_to_array($collection->slice(15, 20)));
+ self::assertNotSame($collection = static::createCollection([]), $slice = $collection->slice(0));
+ self::assertSame([], iterator_to_array($slice));
+ self::assertSame([3 => null, 4 => 5], iterator_to_array(($collection = static::createCollection([1, 2, 3, null, 5]))->slice(3)));
+ self::assertSame([1], iterator_to_array($collection->slice(0, 1)));
+ self::assertSame([1 => 2], iterator_to_array($collection->slice(1, 1)));
+ self::assertSame([4 => 5], iterator_to_array($collection->slice(4, 20)));
+ self::assertSame([], iterator_to_array($collection->slice(15, 20)));
}
public function testMap(): void
{
- $this->assertSame([], static::createCollection([])->map(function ($v) {
+ self::assertSame([], static::createCollection([])->map(function ($v) {
return $v;
}));
- $this->assertSame(['k' => 2, 4, 6], static::createCollection(['k' => 1, 2, 3])->map(function (int $v): int {
+ self::assertSame(['k' => 2, 4, 6], static::createCollection(['k' => 1, 2, 3])->map(function (int $v): int {
return $v * 2;
}));
}
public function testCount(): void
{
- $this->assertCount(0, static::createCollection([]));
- $this->assertCount(1, static::createCollection([null]));
- $this->assertCount(3, static::createCollection([1, 'k' => null, 2]));
+ self::assertCount(0, static::createCollection([]));
+ self::assertCount(1, static::createCollection([null]));
+ self::assertCount(3, static::createCollection([1, 'k' => null, 2]));
}
abstract protected static function createCollection(array $elements): DomainCollectionInterface;
diff --git a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php
index 30b62360..9d2a6239 100644
--- a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php
+++ b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php
@@ -125,7 +125,7 @@ public function testFind(string $class, Entities\BaseTestEntity $entity, array $
try {
$repository->doFind($ids);
- $this->fail();
+ self::fail();
} catch (EntityNotFoundException $e) {
$this->addToAssertionCount(1);
}
@@ -145,7 +145,7 @@ public function testFindByFields(string $class, array $fields): void
try {
$repository->doFindByFields($fields);
- $this->fail();
+ self::fail();
} catch (EntityNotFoundException $e) {
$this->addToAssertionCount(1);
}
@@ -186,8 +186,8 @@ public function testFindByFieldsWithPrimaryId(): void
static::flushEntities([$derivingEntity = Entities\TestDerivedEntity::create(['entity' => $entity]), $entity2]);
- $this->assertFalse($entity->getId()->isEmpty());
- $this->assertNotSame('IRRELEVANT', $entity->getId()->toString());
+ self::assertFalse($entity->getId()->isEmpty());
+ self::assertNotSame('IRRELEVANT', $entity->getId()->toString());
$this->assertEntityEquals($derivingEntity, $repository->doFindByFields(['entity' => $entity->getId()]));
}
@@ -198,11 +198,11 @@ public function testExists(string $class, Entities\BaseTestEntity $entity, array
{
$repository = static::createRepository($class);
- $this->assertFalse($repository->doExists($ids));
+ self::assertFalse($repository->doExists($ids));
$this->loadEntities($entity);
- $this->assertTrue($repository->doExists(Entities\BaseTestEntity::getPrimaryIds($entity)));
+ self::assertTrue($repository->doExists(Entities\BaseTestEntity::getPrimaryIds($entity)));
}
/**
@@ -212,11 +212,11 @@ public function testExistsByFields(string $class, array $fields): void
{
$repository = static::createRepository($class);
- $this->assertFalse($repository->doExistsByFields($fields));
+ self::assertFalse($repository->doExistsByFields($fields));
$this->loadEntities($entity = $class::create($fields));
- $this->assertTrue($repository->doExistsByFields($fields));
+ self::assertTrue($repository->doExistsByFields($fields));
}
public function testExistsByFieldsWithoutFields(): void
@@ -238,7 +238,7 @@ public function testExistsByFieldsWithEmptyDomainId(): void
$this->loadEntities();
- $this->assertFalse($repository->doExistsByFields(['entity' => $entity]));
+ self::assertFalse($repository->doExistsByFields(['entity' => $entity]));
}
/**
@@ -248,7 +248,7 @@ public function testSave(string $class, Entities\BaseTestEntity $entity, array $
{
$repository = static::createRepository($class);
- $this->assertFalse($repository->doExists($ids));
+ self::assertFalse($repository->doExists($ids));
$repository->doSave($entity);
@@ -260,7 +260,7 @@ public function testSave(string $class, Entities\BaseTestEntity $entity, array $
}
}
- $this->assertTrue($repository->doExists(Entities\BaseTestEntity::getPrimaryIds($entity)));
+ self::assertTrue($repository->doExists(Entities\BaseTestEntity::getPrimaryIds($entity)));
}
public function testSaveUpdates(): void
@@ -275,14 +275,14 @@ public function testSaveUpdates(): void
$repository->doSave($entity);
if (static::$supportsAutoGeneratedIds) {
- $this->assertFalse($entity->getId()->isEmpty());
+ self::assertFalse($entity->getId()->isEmpty());
}
- $this->assertInstanceOf(DomainIdInterface::class, $entity->getId());
- $this->assertNull($entity->strField);
- $this->assertSame(1, $entity->intField);
- $this->assertSame(-1.23, $entity->floatField);
- $this->assertFalse($entity->boolField);
+ self::assertInstanceOf(DomainIdInterface::class, $entity->getId());
+ self::assertNull($entity->strField);
+ self::assertSame(1, $entity->intField);
+ self::assertSame(-1.23, $entity->floatField);
+ self::assertFalse($entity->boolField);
$entity->strField = 'foo';
$entity->floatField = null;
@@ -290,11 +290,11 @@ public function testSaveUpdates(): void
$repository->doSave($entity);
- $this->assertInstanceOf(Entities\TestEntity::class, $entity);
- $this->assertSame('foo', $entity->strField);
- $this->assertSame(1, $entity->intField);
- $this->assertNull($entity->floatField);
- $this->assertTrue($entity->boolField);
+ self::assertInstanceOf(Entities\TestEntity::class, $entity);
+ self::assertSame('foo', $entity->strField);
+ self::assertSame(1, $entity->intField);
+ self::assertNull($entity->floatField);
+ self::assertTrue($entity->boolField);
}
public function testSaveThrowsOnDuplicate(): void
@@ -326,11 +326,11 @@ public function testDelete(string $class, Entities\BaseTestEntity $entity): void
static::flushEntities([$entity]);
- $this->assertTrue($repository->doExists($ids = Entities\BaseTestEntity::getPrimaryIds($entity)));
+ self::assertTrue($repository->doExists($ids = Entities\BaseTestEntity::getPrimaryIds($entity)));
$repository->doDelete($entity);
- $this->assertFalse($repository->doExists($ids));
+ self::assertFalse($repository->doExists($ids));
}
public function testDeleteWithInvalidClass(): void
@@ -378,8 +378,8 @@ abstract protected static function flushEntities(iterable $entities): void;
final protected function assertEntityCollectionEquals(array $expected, $actual): void
{
- $this->assertInstanceOf(DomainCollectionInterface::class, $actual);
- $this->assertCount(count($expected), $actual);
+ self::assertInstanceOf(DomainCollectionInterface::class, $actual);
+ self::assertCount(\count($expected), $actual);
$equals = true;
foreach ($actual as $i => $entity) {
@@ -389,15 +389,15 @@ final protected function assertEntityCollectionEquals(array $expected, $actual):
}
}
- $this->assertTrue($equals);
+ self::assertTrue($equals);
}
final protected function assertEntityEquals($expected, $actual): void
{
- $this->assertSame(get_class($expected), get_class($actual));
+ self::assertSame(\get_class($expected), \get_class($actual));
if (!$this->equalsEntity($expected, $actual)) {
- $this->fail();
+ self::fail();
} else {
$this->addToAssertionCount(1);
}
@@ -411,7 +411,7 @@ protected function equalsEntity($expected, $actual)
private function loadEntities(Entities\BaseTestEntity ...$context): void
{
$entities = [];
- foreach (func_get_args() as $entity) {
+ foreach (\func_get_args() as $entity) {
Entities\BaseTestEntity::getPrimaryIds($entity, $primitiveIds);
$entities[serialize($primitiveIds)] = $entity;
}
diff --git a/src/Domain/Tests/AbstractDomainIdentityMappingTest.php b/src/Domain/Tests/AbstractDomainIdentityMappingTest.php
index 0058f073..aca66141 100644
--- a/src/Domain/Tests/AbstractDomainIdentityMappingTest.php
+++ b/src/Domain/Tests/AbstractDomainIdentityMappingTest.php
@@ -16,11 +16,11 @@ public function testGetIdentifierFieldNames(): void
{
$mapping = static::createMapping();
- $this->assertSame(['idA', 'idB'], $mapping->getIdentifierFieldNames(Entities\TestCompositeEntity::class));
- $this->assertSame(['entity', 'id'], $mapping->getIdentifierFieldNames(Entities\TestDerivedCompositeEntity::class));
- $this->assertSame(['entity'], $mapping->getIdentifierFieldNames(Entities\TestDerivedEntity::class));
- $this->assertSame(['id'], $mapping->getIdentifierFieldNames(Entities\TestEntity::class));
- $this->assertSame(['id'], $mapping->getIdentifierFieldNames(Entities\TestPrimitiveEntity::class));
+ self::assertSame(['idA', 'idB'], $mapping->getIdentifierFieldNames(Entities\TestCompositeEntity::class));
+ self::assertSame(['entity', 'id'], $mapping->getIdentifierFieldNames(Entities\TestDerivedCompositeEntity::class));
+ self::assertSame(['entity'], $mapping->getIdentifierFieldNames(Entities\TestDerivedEntity::class));
+ self::assertSame(['id'], $mapping->getIdentifierFieldNames(Entities\TestEntity::class));
+ self::assertSame(['id'], $mapping->getIdentifierFieldNames(Entities\TestPrimitiveEntity::class));
}
public function testGetIdentifierFieldNamesWithInvalidClass(): void
@@ -36,40 +36,40 @@ public function testGetIdentity(): void
{
$mapping = static::createMapping();
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('isEmpty')
->willReturn(false);
$entity = Entities\TestPrimitiveEntity::create(['id' => $id]);
- $this->assertSame($identity = ['idA' => $id, 'idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create($identity)));
- $this->assertSame($identity = ['entity' => $entity, 'id' => 0], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create($identity)));
- $this->assertSame($identity = ['entity' => $entity], $mapping->getIdentity(Entities\TestDerivedEntity::create($identity)));
- $this->assertSame($identity = ['id' => $id], $mapping->getIdentity(Entities\TestEntity::create($identity + ['strField' => 'foo'])));
- $this->assertSame($identity = ['id' => $id], $mapping->getIdentity(Entities\TestPrimitiveEntity::create($identity)));
+ self::assertSame($identity = ['idA' => $id, 'idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create($identity)));
+ self::assertSame($identity = ['entity' => $entity, 'id' => 0], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create($identity)));
+ self::assertSame($identity = ['entity' => $entity], $mapping->getIdentity(Entities\TestDerivedEntity::create($identity)));
+ self::assertSame($identity = ['id' => $id], $mapping->getIdentity(Entities\TestEntity::create($identity + ['strField' => 'foo'])));
+ self::assertSame($identity = ['id' => $id], $mapping->getIdentity(Entities\TestPrimitiveEntity::create($identity)));
}
public function testGetIdentityWithEmptyIdentifier(): void
{
$mapping = static::createMapping();
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('isEmpty')
->willReturn(true);
$entity = Entities\TestPrimitiveEntity::create(['id' => $id]);
- $this->assertSame(['idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create(['idA' => $id, 'idB' => 'foo'])));
- $this->assertSame(['id' => 0], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create(['entity' => $entity, 'id' => 0])));
- $this->assertSame([], $mapping->getIdentity(Entities\TestDerivedEntity::create(['entity' => $entity])));
- $this->assertSame([], $mapping->getIdentity(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
- $this->assertSame([], $mapping->getIdentity(Entities\TestPrimitiveEntity::create(['id' => $id])));
+ self::assertSame(['idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create(['idA' => $id, 'idB' => 'foo'])));
+ self::assertSame(['id' => 0], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create(['entity' => $entity, 'id' => 0])));
+ self::assertSame([], $mapping->getIdentity(Entities\TestDerivedEntity::create(['entity' => $entity])));
+ self::assertSame([], $mapping->getIdentity(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
+ self::assertSame([], $mapping->getIdentity(Entities\TestPrimitiveEntity::create(['id' => $id])));
}
public function testGetIdentityWithIncompleteIdentifier(): void
{
$mapping = static::createMapping();
- $this->assertSame($identity = ['idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create($identity)));
- $this->assertSame($identity = ['id' => 1], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create($identity)));
+ self::assertSame($identity = ['idB' => 'foo'], $mapping->getIdentity(Entities\TestCompositeEntity::create($identity)));
+ self::assertSame($identity = ['id' => 1], $mapping->getIdentity(Entities\TestDerivedCompositeEntity::create($identity)));
}
public function testGetIdentityWithInvalidClass(): void
diff --git a/src/Domain/Tests/DomainCollectionTest.php b/src/Domain/Tests/DomainCollectionTest.php
index 6b8d0c05..b8aed8fb 100644
--- a/src/Domain/Tests/DomainCollectionTest.php
+++ b/src/Domain/Tests/DomainCollectionTest.php
@@ -11,14 +11,14 @@ final class DomainCollectionTest extends AbstractDomainCollectionTest
{
public function testLazyFromValue(): void
{
- $this->assertEquals(new DomainCollection($generator = self::getGenerator([])), DomainCollection::fromValue($generator));
+ self::assertEquals(new DomainCollection($generator = self::getGenerator([])), DomainCollection::fromValue($generator));
}
public function testLazyGetIterator(): void
{
- $this->assertSame([], iterator_to_array(self::createLazyCollection([])));
- $this->assertSame($elements = [2, 'key' => 'val'], iterator_to_array($collection = self::createLazyCollection($elements, $visited)));
- $this->assertSame($elements, $visited);
+ self::assertSame([], iterator_to_array(self::createLazyCollection([])));
+ self::assertSame($elements = [2, 'key' => 'val'], iterator_to_array($collection = self::createLazyCollection($elements, $visited)));
+ self::assertSame($elements, $visited);
$this->assertClosedGenerator();
@@ -27,11 +27,11 @@ public function testLazyGetIterator(): void
public function testLazyIsEmpty(): void
{
- $this->assertTrue(self::createLazyCollection([])->isEmpty());
- $this->assertFalse(($collection = self::createLazyCollection([1, 2], $visited))->isEmpty());
- $this->assertSame([1], $visited);
- $this->assertFalse($collection->isEmpty());
- $this->assertSame([1, 2], iterator_to_array($collection));
+ self::assertTrue(self::createLazyCollection([])->isEmpty());
+ self::assertFalse(($collection = self::createLazyCollection([1, 2], $visited))->isEmpty());
+ self::assertSame([1], $visited);
+ self::assertFalse($collection->isEmpty());
+ self::assertSame([1, 2], iterator_to_array($collection));
$this->assertClosedGenerator();
@@ -40,13 +40,13 @@ public function testLazyIsEmpty(): void
public function testLazyContains(): void
{
- $this->assertFalse(static::createLazyCollection([])->contains(1));
- $this->assertTrue(static::createLazyCollection([null], $visited)->contains(null));
- $this->assertSame([null], $visited);
- $this->assertTrue(($collection = static::createLazyCollection([1, '2'], $visited))->contains(1));
- $this->assertSame([1], $visited);
- $this->assertFalse($collection->contains(2));
- $this->assertSame([1, '2'], $visited);
+ self::assertFalse(static::createLazyCollection([])->contains(1));
+ self::assertTrue(static::createLazyCollection([null], $visited)->contains(null));
+ self::assertSame([null], $visited);
+ self::assertTrue(($collection = static::createLazyCollection([1, '2'], $visited))->contains(1));
+ self::assertSame([1], $visited);
+ self::assertFalse($collection->contains(2));
+ self::assertSame([1, '2'], $visited);
$this->assertClosedGenerator();
@@ -55,9 +55,9 @@ public function testLazyContains(): void
public function testLazyContainsKey(): void
{
- $this->assertFalse(static::createLazyCollection([])->containsKey(1));
- $this->assertTrue(($collection = static::createLazyCollection([1, 'k' => 'v', '2' => null], $visited))->containsKey(2));
- $this->assertSame([1, 'k' => 'v', '2' => null], $visited);
+ self::assertFalse(static::createLazyCollection([])->containsKey(1));
+ self::assertTrue(($collection = static::createLazyCollection([1, 'k' => 'v', '2' => null], $visited))->containsKey(2));
+ self::assertSame([1, 'k' => 'v', '2' => null], $visited);
$this->assertUnrewindableGenerator();
@@ -66,11 +66,11 @@ public function testLazyContainsKey(): void
public function testLazyFirst(): void
{
- $this->assertFalse(self::createLazyCollection([])->first());
- $this->assertSame(1, ($collection = static::createLazyCollection([1, 2], $visited))->first());
- $this->assertSame([1], $visited);
- $this->assertSame(1, $collection->first());
- $this->assertSame([1, 2], iterator_to_array($collection));
+ self::assertFalse(self::createLazyCollection([])->first());
+ self::assertSame(1, ($collection = static::createLazyCollection([1, 2], $visited))->first());
+ self::assertSame([1], $visited);
+ self::assertSame(1, $collection->first());
+ self::assertSame([1, 2], iterator_to_array($collection));
$this->assertClosedGenerator();
@@ -79,9 +79,9 @@ public function testLazyFirst(): void
public function testLazyLast(): void
{
- $this->assertFalse(self::createLazyCollection([])->last());
- $this->assertSame(2, ($collection = static::createLazyCollection([1, 2], $visited))->last());
- $this->assertSame([1, 2], $visited);
+ self::assertFalse(self::createLazyCollection([])->last());
+ self::assertSame(2, ($collection = static::createLazyCollection([1, 2], $visited))->last());
+ self::assertSame([1, 2], $visited);
$this->assertClosedGenerator();
@@ -90,11 +90,11 @@ public function testLazyLast(): void
public function testLazyGet(): void
{
- $this->assertNull(self::createLazyCollection([])->get('key'));
- $this->assertSame(1, ($collection = static::createLazyCollection([1], $visited))->get(0));
- $this->assertSame([1], $visited);
- $this->assertSame(1, $collection->get('0'));
- $this->assertNull($collection->get('foo'));
+ self::assertNull(self::createLazyCollection([])->get('key'));
+ self::assertSame(1, ($collection = static::createLazyCollection([1], $visited))->get(0));
+ self::assertSame([1], $visited);
+ self::assertSame(1, $collection->get('0'));
+ self::assertNull($collection->get('foo'));
$this->assertClosedGenerator();
@@ -103,14 +103,14 @@ public function testLazyGet(): void
public function testLazyFilter(): void
{
- $this->assertNotSame($collection = self::createLazyCollection([]), $filtered = $collection->filter(function (): bool {
+ self::assertNotSame($collection = self::createLazyCollection([]), $filtered = $collection->filter(function (): bool {
return true;
}));
- $this->assertSame([], iterator_to_array($filtered));
- $this->assertSame([1, 2 => 3], iterator_to_array(($collection = static::createLazyCollection([1, null, 3], $visited))->filter($filter = function ($v): bool {
+ self::assertSame([], iterator_to_array($filtered));
+ self::assertSame([1, 2 => 3], iterator_to_array(($collection = static::createLazyCollection([1, null, 3], $visited))->filter($filter = function ($v): bool {
return null !== $v;
})));
- $this->assertSame([1, null, 3], $visited);
+ self::assertSame([1, null, 3], $visited);
$this->assertClosedGenerator();
@@ -119,10 +119,10 @@ public function testLazyFilter(): void
public function testLazySlice(): void
{
- $this->assertNotSame($collection = self::createLazyCollection([]), $slice = $collection->slice(0));
- $this->assertSame([], iterator_to_array($slice));
- $this->assertSame([1 => 2], iterator_to_array(($collection = static::createLazyCollection([1, 2, 3, null, 5], $visited))->slice(1, 1)));
- $this->assertSame([1, 2, 3], $visited);
+ self::assertNotSame($collection = self::createLazyCollection([]), $slice = $collection->slice(0));
+ self::assertSame([], iterator_to_array($slice));
+ self::assertSame([1 => 2], iterator_to_array(($collection = static::createLazyCollection([1, 2, 3, null, 5], $visited))->slice(1, 1)));
+ self::assertSame([1, 2, 3], $visited);
$this->assertUnrewindableGenerator();
@@ -131,10 +131,10 @@ public function testLazySlice(): void
public function testLazyMap(): void
{
- $this->assertSame([], self::createLazyCollection([])->map(function ($v) {
+ self::assertSame([], self::createLazyCollection([])->map(function ($v) {
return $v;
}));
- $this->assertSame([2], ($collection = self::createLazyCollection([1]))->map($mapper = function (int $v): int {
+ self::assertSame([2], ($collection = self::createLazyCollection([1]))->map($mapper = function (int $v): int {
return $v * 2;
}));
@@ -145,13 +145,13 @@ public function testLazyMap(): void
public function testLazyCount(): void
{
- $this->assertCount(0, self::createLazyCollection([]));
- $this->assertCount(2, $collection = self::createLazyCollection([1, 2], $visited));
- $this->assertSame([1, 2], $visited);
+ self::assertCount(0, self::createLazyCollection([]));
+ self::assertCount(2, $collection = self::createLazyCollection([1, 2], $visited));
+ self::assertSame([1, 2], $visited);
$this->assertClosedGenerator();
- count($collection);
+ \count($collection);
}
protected static function createCollection(array $elements): DomainCollectionInterface
diff --git a/src/Domain/Tests/DomainIdTest.php b/src/Domain/Tests/DomainIdTest.php
index b77227bb..a12bfb8b 100644
--- a/src/Domain/Tests/DomainIdTest.php
+++ b/src/Domain/Tests/DomainIdTest.php
@@ -11,22 +11,22 @@ final class DomainIdTest extends TestCase
{
public function testFromValue(): void
{
- $this->assertSame((array) new DomainId(), (array) DomainId::fromValue(null));
- $this->assertSame((array) new DomainId(null), (array) DomainId::fromValue(null));
- $this->assertNotSame((array) new DomainId(''), (array) DomainId::fromValue(null));
- $this->assertSame((array) new DomainId(''), (array) DomainId::fromValue(''));
- $this->assertSame((array) new DomainId(''), (array) DomainId::fromValue(false));
- $this->assertSame((array) new DomainId('1'), (array) DomainId::fromValue(1));
- $this->assertInstanceOf(OtherTestDomainId::class, OtherTestDomainId::fromValue(null));
+ self::assertSame((array) new DomainId(), (array) DomainId::fromValue(null));
+ self::assertSame((array) new DomainId(null), (array) DomainId::fromValue(null));
+ self::assertNotSame((array) new DomainId(''), (array) DomainId::fromValue(null));
+ self::assertSame((array) new DomainId(''), (array) DomainId::fromValue(''));
+ self::assertSame((array) new DomainId(''), (array) DomainId::fromValue(false));
+ self::assertSame((array) new DomainId('1'), (array) DomainId::fromValue(1));
+ self::assertInstanceOf(OtherTestDomainId::class, OtherTestDomainId::fromValue(null));
}
public function testIsEmpty(): void
{
- $this->assertTrue((new DomainId())->isEmpty());
- $this->assertTrue((new DomainId(null))->isEmpty());
- $this->assertFalse((new DomainId(''))->isEmpty());
- $this->assertFalse((new DomainId(' '))->isEmpty());
- $this->assertFalse((new DomainId('foo'))->isEmpty());
+ self::assertTrue((new DomainId())->isEmpty());
+ self::assertTrue((new DomainId(null))->isEmpty());
+ self::assertFalse((new DomainId(''))->isEmpty());
+ self::assertFalse((new DomainId(' '))->isEmpty());
+ self::assertFalse((new DomainId('foo'))->isEmpty());
}
public function testEquals(): void
@@ -34,13 +34,13 @@ public function testEquals(): void
$id = new DomainId('foo');
$emptyId = new DomainId();
- $this->assertTrue($id->equals($id));
- $this->assertTrue($id->equals(new DomainId('foo')));
- $this->assertFalse($id->equals($emptyId));
- $this->assertFalse($id->equals(new OtherTestDomainId('foo')));
- $this->assertTrue($emptyId->equals($emptyId));
- $this->assertFalse($emptyId->equals(new DomainId()));
- $this->assertFalse($emptyId->equals(new OtherTestDomainId()));
+ self::assertTrue($id->equals($id));
+ self::assertTrue($id->equals(new DomainId('foo')));
+ self::assertFalse($id->equals($emptyId));
+ self::assertFalse($id->equals(new OtherTestDomainId('foo')));
+ self::assertTrue($emptyId->equals($emptyId));
+ self::assertFalse($emptyId->equals(new DomainId()));
+ self::assertFalse($emptyId->equals(new OtherTestDomainId()));
}
/**
@@ -48,8 +48,8 @@ public function testEquals(): void
*/
public function testToString(DomainId $id, string $value): void
{
- $this->assertSame($value, $id->toString());
- $this->assertSame($value, (string) $id);
+ self::assertSame($value, $id->toString());
+ self::assertSame($value, (string) $id);
}
/**
@@ -57,7 +57,7 @@ public function testToString(DomainId $id, string $value): void
*/
public function testSerialize(DomainId $id): void
{
- $this->assertSame((array) $id, (array) unserialize(serialize($id)));
+ self::assertSame((array) $id, (array) unserialize(serialize($id)));
}
/**
@@ -65,7 +65,7 @@ public function testSerialize(DomainId $id): void
*/
public function testJsonSerialize(DomainId $id): void
{
- $this->assertSame($id->isEmpty() ? null : $id->toString(), json_decode(json_encode($id)));
+ self::assertSame($id->isEmpty() ? null : $id->toString(), json_decode((string) json_encode($id)));
}
public function provideIds(): iterable
diff --git a/src/Domain/Tests/DomainIdentityHelperTest.php b/src/Domain/Tests/DomainIdentityHelperTest.php
index 792ea87d..e649f88a 100644
--- a/src/Domain/Tests/DomainIdentityHelperTest.php
+++ b/src/Domain/Tests/DomainIdentityHelperTest.php
@@ -16,7 +16,7 @@ final class DomainIdentityHelperTest extends TestCase
protected function setUp(): void
{
$this->mapping = $this->createMock(DomainIdentityMappingInterface::class);
- $this->mapping->expects($this->any())
+ $this->mapping->expects(self::any())
->method('getIdentifierFieldNames')
->willReturnCallback(function ($class): array {
if (is_subclass_of($class, Entities\BaseTestEntity::class)) {
@@ -25,14 +25,14 @@ protected function setUp(): void
throw InvalidClassException::create($class);
});
- $this->mapping->expects($this->any())
+ $this->mapping->expects(self::any())
->method('getIdentity')
->willReturnCallback(function ($object): array {
if ($object instanceof Entities\BaseTestEntity) {
return Entities\BaseTestEntity::getPrimaryIds($object);
}
- throw InvalidClassException::create(get_class($object));
+ throw InvalidClassException::create(\get_class($object));
});
}
@@ -40,14 +40,14 @@ public function testIsIdentifier(): void
{
$helper = new DomainIdentityHelper($this->mapping);
- $this->assertTrue($helper->isIdentifier($this->createMock(DomainIdInterface::class)));
- $this->assertTrue($helper->isIdentifier(Entities\TestEntity::create()));
- $this->assertTrue($helper->isIdentifier(Entities\TestCompositeEntity::create()));
- $this->assertFalse($helper->isIdentifier(new \stdClass()));
- $this->assertFalse($helper->isIdentifier(null));
- $this->assertFalse($helper->isIdentifier([]));
- $this->assertFalse($helper->isIdentifier(1));
- $this->assertFalse($helper->isIdentifier('foo'));
+ self::assertTrue($helper->isIdentifier($this->createMock(DomainIdInterface::class)));
+ self::assertTrue($helper->isIdentifier(Entities\TestEntity::create()));
+ self::assertTrue($helper->isIdentifier(Entities\TestCompositeEntity::create()));
+ self::assertFalse($helper->isIdentifier(new \stdClass()));
+ self::assertFalse($helper->isIdentifier(null));
+ self::assertFalse($helper->isIdentifier([]));
+ self::assertFalse($helper->isIdentifier(1));
+ self::assertFalse($helper->isIdentifier('foo'));
}
public function testIsEmptyIdentifier(): void
@@ -55,25 +55,25 @@ public function testIsEmptyIdentifier(): void
$helper = new DomainIdentityHelper($this->mapping);
$emptyId = $this->createMock(DomainIdInterface::class);
- $emptyId->expects($this->any())
+ $emptyId->expects(self::any())
->method('isEmpty')
->willReturn(true);
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('isEmpty')
->willReturn(false);
- $this->assertTrue($helper->isEmptyIdentifier(null));
- $this->assertTrue($helper->isEmptyIdentifier($emptyId));
- $this->assertTrue($helper->isEmptyIdentifier(Entities\TestEntity::create()));
- $this->assertTrue($helper->isEmptyIdentifier(Entities\TestEntity::create(['id' => $emptyId, 'strField' => 'foo'])));
- $this->assertTrue($helper->isEmptyIdentifier(Entities\TestCompositeEntity::create(['idB' => 'foo'])));
- $this->assertFalse($helper->isEmptyIdentifier($id));
- $this->assertFalse($helper->isEmptyIdentifier(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
- $this->assertFalse($helper->isEmptyIdentifier([]));
- $this->assertFalse($helper->isEmptyIdentifier(1));
- $this->assertFalse($helper->isEmptyIdentifier('foo'));
- $this->assertFalse($helper->isEmptyIdentifier(new \stdClass()));
+ self::assertTrue($helper->isEmptyIdentifier(null));
+ self::assertTrue($helper->isEmptyIdentifier($emptyId));
+ self::assertTrue($helper->isEmptyIdentifier(Entities\TestEntity::create()));
+ self::assertTrue($helper->isEmptyIdentifier(Entities\TestEntity::create(['id' => $emptyId, 'strField' => 'foo'])));
+ self::assertTrue($helper->isEmptyIdentifier(Entities\TestCompositeEntity::create(['idB' => 'foo'])));
+ self::assertFalse($helper->isEmptyIdentifier($id));
+ self::assertFalse($helper->isEmptyIdentifier(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
+ self::assertFalse($helper->isEmptyIdentifier([]));
+ self::assertFalse($helper->isEmptyIdentifier(1));
+ self::assertFalse($helper->isEmptyIdentifier('foo'));
+ self::assertFalse($helper->isEmptyIdentifier(new \stdClass()));
}
public function testNormalizeIdentifier(): void
@@ -81,42 +81,42 @@ public function testNormalizeIdentifier(): void
$helper = new DomainIdentityHelper($this->mapping);
$emptyId = $this->createMock(DomainIdInterface::class);
- $emptyId->expects($this->any())
+ $emptyId->expects(self::any())
->method('isEmpty')
->willReturn(true);
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('isEmpty')
->willReturn(false);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('toString')
->willReturn('id');
- $this->assertNull($helper->normalizeIdentifier(null));
- $this->assertNull($helper->normalizeIdentifier($emptyId));
- $this->assertNull($helper->normalizeIdentifier(Entities\TestEntity::create()));
- $this->assertNull($helper->normalizeIdentifier($entity = Entities\TestEntity::create(['id' => $emptyId, 'strField' => 'foo'])));
- $this->assertNull($helper->normalizeIdentifier(Entities\TestDerivedEntity::create(['entity' => $entity])));
- $this->assertSame('id', $helper->normalizeIdentifier($id));
- $this->assertSame('id', $helper->normalizeIdentifier(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
- $this->assertSame(['idA' => 'id', 'idB' => null], $helper->normalizeIdentifier(Entities\TestCompositeEntity::create(['idA' => $id])));
- $this->assertSame(['idA' => 'id', 'idB' => 'id-b'], $helper->normalizeIdentifier(Entities\TestCompositeEntity::create(['idA' => $id, 'idB' => 'id-b'])));
- $this->assertSame(['entity' => null, 'id' => 0], $helper->normalizeIdentifier(Entities\TestDerivedCompositeEntity::create(['entity' => Entities\TestPrimitiveEntity::create([]), 'id' => 0])));
- $this->assertSame([], $helper->normalizeIdentifier([]));
- $this->assertSame(['id' => 1], $helper->normalizeIdentifier(['id' => 1]));
- $this->assertSame(1, $helper->normalizeIdentifier(1));
- $this->assertSame('foo', $helper->normalizeIdentifier('foo'));
- $this->assertSame($object = new \stdClass(), $helper->normalizeIdentifier($object));
+ self::assertNull($helper->normalizeIdentifier(null));
+ self::assertNull($helper->normalizeIdentifier($emptyId));
+ self::assertNull($helper->normalizeIdentifier(Entities\TestEntity::create()));
+ self::assertNull($helper->normalizeIdentifier($entity = Entities\TestEntity::create(['id' => $emptyId, 'strField' => 'foo'])));
+ self::assertNull($helper->normalizeIdentifier(Entities\TestDerivedEntity::create(['entity' => $entity])));
+ self::assertSame('id', $helper->normalizeIdentifier($id));
+ self::assertSame('id', $helper->normalizeIdentifier(Entities\TestEntity::create(['id' => $id, 'strField' => 'foo'])));
+ self::assertSame(['idA' => 'id', 'idB' => null], $helper->normalizeIdentifier(Entities\TestCompositeEntity::create(['idA' => $id])));
+ self::assertSame(['idA' => 'id', 'idB' => 'id-b'], $helper->normalizeIdentifier(Entities\TestCompositeEntity::create(['idA' => $id, 'idB' => 'id-b'])));
+ self::assertSame(['entity' => null, 'id' => 0], $helper->normalizeIdentifier(Entities\TestDerivedCompositeEntity::create(['entity' => Entities\TestPrimitiveEntity::create([]), 'id' => 0])));
+ self::assertSame([], $helper->normalizeIdentifier([]));
+ self::assertSame(['id' => 1], $helper->normalizeIdentifier(['id' => 1]));
+ self::assertSame(1, $helper->normalizeIdentifier(1));
+ self::assertSame('foo', $helper->normalizeIdentifier('foo'));
+ self::assertSame($object = new \stdClass(), $helper->normalizeIdentifier($object));
}
public function testGetIdentifiers(): void
{
$helper = new DomainIdentityHelper($this->mapping);
- $this->assertSame([$id = $this->createMock(DomainIdInterface::class)], $helper->getIdentifiers($entity = Entities\TestEntity::create(['id' => $id])));
- $this->assertSame([$entity], $helper->getIdentifiers(Entities\TestDerivedEntity::create(['entity' => $entity])));
- $this->assertSame([null, 'bar'], $helper->getIdentifiers(Entities\TestCompositeEntity::create(['idB' => 'bar'])));
- $this->assertSame([null], $helper->getIdentifiers(Entities\TestPrimitiveEntity::create()));
+ self::assertSame([$id = $this->createMock(DomainIdInterface::class)], $helper->getIdentifiers($entity = Entities\TestEntity::create(['id' => $id])));
+ self::assertSame([$entity], $helper->getIdentifiers(Entities\TestDerivedEntity::create(['entity' => $entity])));
+ self::assertSame([null, 'bar'], $helper->getIdentifiers(Entities\TestCompositeEntity::create(['idB' => 'bar'])));
+ self::assertSame([null], $helper->getIdentifiers(Entities\TestPrimitiveEntity::create()));
}
public function testGetIdentifiersWithInvalidEntity(): void
@@ -132,8 +132,8 @@ public function testGetIdentifierFieldNames(): void
{
$helper = new DomainIdentityHelper($this->mapping);
- $this->assertSame(['id'], $helper->getIdentifierFieldNames(Entities\TestPrimitiveEntity::class));
- $this->assertSame(['idA', 'idB'], $helper->getIdentifierFieldNames(Entities\TestCompositeEntity::class));
+ self::assertSame(['id'], $helper->getIdentifierFieldNames(Entities\TestPrimitiveEntity::class));
+ self::assertSame(['idA', 'idB'], $helper->getIdentifierFieldNames(Entities\TestCompositeEntity::class));
}
public function testGetIdentifierFieldNamesWithInvalidEntity(): void
@@ -150,28 +150,28 @@ public function testIsIdentity(): void
$helper = new DomainIdentityHelper($this->mapping);
$emptyId = $this->createMock(DomainIdInterface::class);
- $emptyId->expects($this->any())
+ $emptyId->expects(self::any())
->method('isEmpty')
->willReturn(true);
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('isEmpty')
->willReturn(false);
- $this->assertTrue($helper->isIdentity(Entities\TestEntity::class, ['id' => $id]));
- $this->assertTrue($helper->isIdentity(Entities\TestEntity::class, $id));
- $this->assertTrue($helper->isIdentity(Entities\TestEntity::class, 'foo'));
- $this->assertTrue($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => 'b']));
- $this->assertTrue($helper->isIdentity(Entities\TestDerivedEntity::class, ['entity' => Entities\TestEntity::create(['id' => $id])]));
- $this->assertFalse($helper->isIdentity(Entities\TestEntity::class, null));
- $this->assertFalse($helper->isIdentity(Entities\TestEntity::class, []));
- $this->assertFalse($helper->isIdentity(Entities\TestEntity::class, ['id' => $emptyId]));
- $this->assertFalse($helper->isIdentity(Entities\TestEntity::class, $emptyId));
- $this->assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id]));
- $this->assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => null]));
- $this->assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $emptyId, 'idB' => 'foo']));
- $this->assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => 'b', 'foo' => 'bar']));
- $this->assertFalse($helper->isIdentity(Entities\TestDerivedEntity::class, ['entity' => Entities\TestEntity::create(['id' => $emptyId])]));
+ self::assertTrue($helper->isIdentity(Entities\TestEntity::class, ['id' => $id]));
+ self::assertTrue($helper->isIdentity(Entities\TestEntity::class, $id));
+ self::assertTrue($helper->isIdentity(Entities\TestEntity::class, 'foo'));
+ self::assertTrue($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => 'b']));
+ self::assertTrue($helper->isIdentity(Entities\TestDerivedEntity::class, ['entity' => Entities\TestEntity::create(['id' => $id])]));
+ self::assertFalse($helper->isIdentity(Entities\TestEntity::class, null));
+ self::assertFalse($helper->isIdentity(Entities\TestEntity::class, []));
+ self::assertFalse($helper->isIdentity(Entities\TestEntity::class, ['id' => $emptyId]));
+ self::assertFalse($helper->isIdentity(Entities\TestEntity::class, $emptyId));
+ self::assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id]));
+ self::assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => null]));
+ self::assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $emptyId, 'idB' => 'foo']));
+ self::assertFalse($helper->isIdentity(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => 'b', 'foo' => 'bar']));
+ self::assertFalse($helper->isIdentity(Entities\TestDerivedEntity::class, ['entity' => Entities\TestEntity::create(['id' => $emptyId])]));
}
public function testIsIdentityWithInvalidClass(): void
@@ -187,13 +187,13 @@ public function testToIdentity(): void
{
$helper = new DomainIdentityHelper($this->mapping);
- $this->assertSame([], $helper->toIdentity(Entities\TestEntity::class, []));
- $this->assertSame([null], $helper->toIdentity(Entities\TestEntity::class, [null]));
- $this->assertSame([1], $helper->toIdentity(Entities\TestEntity::class, [1]));
- $this->assertSame(['foo' => 'bar', 'bar' => null], $helper->toIdentity(Entities\TestEntity::class, ['foo' => 'bar', 'bar' => null]));
- $this->assertSame(['id' => null], $helper->toIdentity(Entities\TestEntity::class, null));
- $this->assertSame(['id' => 'foo'], $helper->toIdentity(Entities\TestEntity::class, 'foo'));
- $this->assertSame(['idA' => 'foo'], $helper->toIdentity(Entities\TestCompositeEntity::class, 'foo'));
+ self::assertSame([], $helper->toIdentity(Entities\TestEntity::class, []));
+ self::assertSame([null], $helper->toIdentity(Entities\TestEntity::class, [null]));
+ self::assertSame([1], $helper->toIdentity(Entities\TestEntity::class, [1]));
+ self::assertSame(['foo' => 'bar', 'bar' => null], $helper->toIdentity(Entities\TestEntity::class, ['foo' => 'bar', 'bar' => null]));
+ self::assertSame(['id' => null], $helper->toIdentity(Entities\TestEntity::class, null));
+ self::assertSame(['id' => 'foo'], $helper->toIdentity(Entities\TestEntity::class, 'foo'));
+ self::assertSame(['idA' => 'foo'], $helper->toIdentity(Entities\TestCompositeEntity::class, 'foo'));
}
public function testToIdentityWithInvalidClass(): void
@@ -209,9 +209,9 @@ public function testGetIdentity(): void
{
$helper = new DomainIdentityHelper($this->mapping);
- $this->assertSame(['id' => $id = $this->createMock(DomainIdInterface::class)], $helper->getIdentity(Entities\TestEntity::create(['id' => $id])));
- $this->assertSame(['idA' => null, 'idB' => 'bar'], $helper->getIdentity(Entities\TestCompositeEntity::create(['idB' => 'bar'])));
- $this->assertSame(['id' => null], $helper->getIdentity(Entities\TestPrimitiveEntity::create()));
+ self::assertSame(['id' => $id = $this->createMock(DomainIdInterface::class)], $helper->getIdentity(Entities\TestEntity::create(['id' => $id])));
+ self::assertSame(['idA' => null, 'idB' => 'bar'], $helper->getIdentity(Entities\TestCompositeEntity::create(['idB' => 'bar'])));
+ self::assertSame(['id' => null], $helper->getIdentity(Entities\TestPrimitiveEntity::create()));
}
public function testGetIdentityWithInvalidClass(): void
diff --git a/src/Domain/Tests/Entity/Features/CanBeConfirmedTest.php b/src/Domain/Tests/Entity/Features/CanBeConfirmedTest.php
index 3b8ca407..abbf6998 100644
--- a/src/Domain/Tests/Entity/Features/CanBeConfirmedTest.php
+++ b/src/Domain/Tests/Entity/Features/CanBeConfirmedTest.php
@@ -14,27 +14,27 @@ public function testConfirm(): void
{
$object = $this->getObject('foo', null);
- $this->assertSame('foo', $object->getConfirmationToken());
- $this->assertNull($object->getConfirmedAt());
- $this->assertFalse($object->isConfirmed());
+ self::assertSame('foo', $object->getConfirmationToken());
+ self::assertNull($object->getConfirmedAt());
+ self::assertFalse($object->isConfirmed());
$object->confirm();
- $this->assertNull($object->getConfirmationToken());
- $this->assertInstanceOf(\DateTimeImmutable::class, $object->getConfirmedAt());
- $this->assertTrue($object->isConfirmed());
+ self::assertNull($object->getConfirmationToken());
+ self::assertInstanceOf(\DateTimeImmutable::class, $object->getConfirmedAt());
+ self::assertTrue($object->isConfirmed());
}
public function testHandleConfirmEvent(): void
{
$object = $this->getObject('foo', null);
- $this->assertTrue($object->handleConfirmEvent($this->createMock(ConfirmEvent::class)));
- $this->assertNull($prevToken = $object->getConfirmationToken());
- $this->assertInstanceOf(\DateTimeImmutable::class, $object->getConfirmedAt());
- $this->assertTrue($object->isConfirmed());
- $this->assertFalse($object->handleConfirmEvent($this->createMock(ConfirmEvent::class)));
- $this->assertTrue($object->isConfirmed());
+ self::assertTrue($object->handleConfirmEvent($this->createMock(ConfirmEvent::class)));
+ self::assertNull($prevToken = $object->getConfirmationToken());
+ self::assertInstanceOf(\DateTimeImmutable::class, $object->getConfirmedAt());
+ self::assertTrue($object->isConfirmed());
+ self::assertFalse($object->handleConfirmEvent($this->createMock(ConfirmEvent::class)));
+ self::assertTrue($object->isConfirmed());
}
private function getObject($confirmationToken, $confirmedAt)
diff --git a/src/Domain/Tests/Entity/Features/CanBeEnabledTest.php b/src/Domain/Tests/Entity/Features/CanBeEnabledTest.php
index e34cff10..06a5031d 100644
--- a/src/Domain/Tests/Entity/Features/CanBeEnabledTest.php
+++ b/src/Domain/Tests/Entity/Features/CanBeEnabledTest.php
@@ -14,42 +14,42 @@ public function testEnable(): void
{
$object = $this->getObject(false);
- $this->assertFalse($object->isEnabled());
+ self::assertFalse($object->isEnabled());
$object->enable();
- $this->assertTrue($object->isEnabled());
+ self::assertTrue($object->isEnabled());
}
public function testDisable(): void
{
$object = $this->getObject(true);
- $this->assertTrue($object->isEnabled());
+ self::assertTrue($object->isEnabled());
$object->disable();
- $this->assertFalse($object->isEnabled());
+ self::assertFalse($object->isEnabled());
}
public function testHandleEnableEvent(): void
{
$object = $this->getObject(false);
- $this->assertTrue($object->handleEnableEvent($this->createMock(EnableEvent::class)));
- $this->assertTrue($object->isEnabled());
- $this->assertFalse($object->handleEnableEvent($this->createMock(EnableEvent::class)));
- $this->assertTrue($object->isEnabled());
+ self::assertTrue($object->handleEnableEvent($this->createMock(EnableEvent::class)));
+ self::assertTrue($object->isEnabled());
+ self::assertFalse($object->handleEnableEvent($this->createMock(EnableEvent::class)));
+ self::assertTrue($object->isEnabled());
}
public function testHandleDisableEvent(): void
{
$object = $this->getObject(true);
- $this->assertTrue($object->handleDisableEvent($this->createMock(DisableEvent::class)));
- $this->assertFalse($object->isEnabled());
- $this->assertFalse($object->handleDisableEvent($this->createMock(DisableEvent::class)));
- $this->assertFalse($object->isEnabled());
+ self::assertTrue($object->handleDisableEvent($this->createMock(DisableEvent::class)));
+ self::assertFalse($object->isEnabled());
+ self::assertFalse($object->handleDisableEvent($this->createMock(DisableEvent::class)));
+ self::assertFalse($object->isEnabled());
}
private function getObject($value)
diff --git a/src/Domain/Tests/Entity/Fields/CreatedAtFieldTest.php b/src/Domain/Tests/Entity/Fields/CreatedAtFieldTest.php
index ba1e4b76..d93865e4 100644
--- a/src/Domain/Tests/Entity/Fields/CreatedAtFieldTest.php
+++ b/src/Domain/Tests/Entity/Fields/CreatedAtFieldTest.php
@@ -13,7 +13,7 @@ public function testField(): void
{
$object = $this->getObject($value = new \DateTime());
- $this->assertSame($value, $object->getCreatedAt());
+ self::assertSame($value, $object->getCreatedAt());
}
private function getObject($value)
diff --git a/src/Domain/Tests/Entity/Fields/EnabledFieldTest.php b/src/Domain/Tests/Entity/Fields/EnabledFieldTest.php
index c970bd11..86ccf932 100644
--- a/src/Domain/Tests/Entity/Fields/EnabledFieldTest.php
+++ b/src/Domain/Tests/Entity/Fields/EnabledFieldTest.php
@@ -11,14 +11,14 @@ final class EnabledFieldTest extends TestCase
{
public function testField(): void
{
- $this->assertFalse($this->getObject()->isEnabled());
- $this->assertTrue($this->getObject(true)->isEnabled());
- $this->assertFalse($this->getObject(false)->isEnabled());
+ self::assertFalse($this->getObject()->isEnabled());
+ self::assertTrue($this->getObject(true)->isEnabled());
+ self::assertFalse($this->getObject(false)->isEnabled());
}
private function getObject($value = null)
{
- if (func_num_args()) {
+ if (\func_num_args()) {
return new class($value) {
use EnabledField;
diff --git a/src/Domain/Tests/Entity/Fields/LastUpdatedAtFieldTest.php b/src/Domain/Tests/Entity/Fields/LastUpdatedAtFieldTest.php
index 0e802144..3b5aea0b 100644
--- a/src/Domain/Tests/Entity/Fields/LastUpdatedAtFieldTest.php
+++ b/src/Domain/Tests/Entity/Fields/LastUpdatedAtFieldTest.php
@@ -13,7 +13,7 @@ public function testField(): void
{
$object = $this->getObject($value = new \DateTime());
- $this->assertSame($value, $object->getLastUpdatedAt());
+ self::assertSame($value, $object->getLastUpdatedAt());
}
private function getObject($value)
diff --git a/src/Domain/Tests/Event/DomainEventHandlerTraitTest.php b/src/Domain/Tests/Event/DomainEventHandlerTraitTest.php
index 9d3dc012..ac050ee6 100644
--- a/src/Domain/Tests/Event/DomainEventHandlerTraitTest.php
+++ b/src/Domain/Tests/Event/DomainEventHandlerTraitTest.php
@@ -14,15 +14,15 @@ public function testHandleEvent(): void
{
$object = $this->getObject();
- $this->assertTrue($object->handleEvent($event = new TestEvent()));
- $this->assertTrue($event->handled);
- $this->assertTrue($object->handleEvent($event = new TestEventDifferentSuffix()));
- $this->assertTrue($event->handled);
+ self::assertTrue($object->handleEvent($event = new TestEvent()));
+ self::assertTrue($event->handled);
+ self::assertTrue($object->handleEvent($event = new TestEventDifferentSuffix()));
+ self::assertTrue($event->handled);
$event = $this->getMockBuilder(DomainEventInterface::class)
->setMockClassName('MsgPhp_Test_Root_Event')
->getMock();
- $this->assertFalse($object->handleEvent($event));
+ self::assertFalse($object->handleEvent($event));
}
public function testHandleEventWithUnknownEvent(): void
diff --git a/src/Domain/Tests/Factory/ChainObjectFactoryTest.php b/src/Domain/Tests/Factory/ChainObjectFactoryTest.php
index c9f6b3e3..307ffb3f 100644
--- a/src/Domain/Tests/Factory/ChainObjectFactoryTest.php
+++ b/src/Domain/Tests/Factory/ChainObjectFactoryTest.php
@@ -13,15 +13,15 @@ final class ChainObjectFactoryTest extends TestCase
public function testCreate(): void
{
$factory1 = $this->createMock(DomainObjectFactoryInterface::class);
- $factory1->expects($this->any())
+ $factory1->expects(self::any())
->method('create')
->willThrowException(InvalidClassException::create('some'));
$factory2 = $this->createMock(DomainObjectFactoryInterface::class);
- $factory2->expects($this->any())
+ $factory2->expects(self::any())
->method('create')
->willReturn($object = new \stdClass());
- $this->assertSame($object, (new ChainObjectFactory([$factory1, $factory2]))->create('some'));
+ self::assertSame($object, (new ChainObjectFactory([$factory1, $factory2]))->create('some'));
}
public function testCreateWithoutFactories(): void
diff --git a/src/Domain/Tests/Factory/ClassMappingObjectFactoryTest.php b/src/Domain/Tests/Factory/ClassMappingObjectFactoryTest.php
index 9c6ebabf..5ce63ee3 100644
--- a/src/Domain/Tests/Factory/ClassMappingObjectFactoryTest.php
+++ b/src/Domain/Tests/Factory/ClassMappingObjectFactoryTest.php
@@ -12,7 +12,7 @@ final class ClassMappingObjectFactoryTest extends TestCase
public function testCreate(): void
{
$innerFactory = $this->createMock(DomainObjectFactoryInterface::class);
- $innerFactory->expects($this->any())
+ $innerFactory->expects(self::any())
->method('create')
->willReturnCallback(function ($class, $context) {
$o = new \stdClass();
@@ -23,7 +23,7 @@ public function testCreate(): void
});
$factory = new ClassMappingObjectFactory($innerFactory, ['foo' => 'bar']);
- $this->assertSame(['class' => 'bar', 'context' => []], (array) $factory->create('foo'));
- $this->assertSame(['class' => 'Foo', 'context' => ['context']], (array) $factory->create('Foo', ['context']));
+ self::assertSame(['class' => 'bar', 'context' => []], (array) $factory->create('foo'));
+ self::assertSame(['class' => 'Foo', 'context' => ['context']], (array) $factory->create('Foo', ['context']));
}
}
diff --git a/src/Domain/Tests/Factory/ClassMethodResolverTest.php b/src/Domain/Tests/Factory/ClassMethodResolverTest.php
index 8f7d5f6e..aaa0fb9f 100644
--- a/src/Domain/Tests/Factory/ClassMethodResolverTest.php
+++ b/src/Domain/Tests/Factory/ClassMethodResolverTest.php
@@ -14,7 +14,7 @@ public function testResolve(): void
{
$arguments = ClassMethodResolver::resolve(TestClass::class, '__construct');
- $this->assertSame([
+ self::assertSame([
['name' => 'fooBar', 'key' => 'foo_bar', 'required' => true, 'default' => null, 'type' => 'string'],
['name' => 'foo_bar', 'key' => 'foo_bar', 'required' => false, 'default' => null, 'type' => WrongCase::class],
['name' => 'fooBar_Baz', 'key' => 'foo_bar_baz', 'required' => false, 'default' => null, 'type' => null],
@@ -29,9 +29,9 @@ public function testResolveWithoutConstructor(): void
{
$object = new class() {
};
- $arguments = ClassMethodResolver::resolve(get_class($object), '__construct');
+ $arguments = ClassMethodResolver::resolve(\get_class($object), '__construct');
- $this->assertSame([], $arguments);
+ self::assertSame([], $arguments);
}
public function testResolveWithUnknownClass(): void
@@ -43,18 +43,18 @@ public function testResolveWithUnknownClass(): void
public function testResolveWithUnknownMethod(): void
{
- $object = new class('foo', null, null) {
+ $object = new class() {
};
$this->expectException(InvalidClassException::class);
- ClassMethodResolver::resolve(get_class($object), 'bar');
+ ClassMethodResolver::resolve(\get_class($object), 'bar');
}
}
class TestClass
{
- public function __construct(string $fooBar, ?wrongcase $foo_bar, $fooBar_Baz, int $foo = 1, SELF $bar = null, array $baz = [1], iterable $qux)
+ public function __construct(string $fooBar, ?wrongcase $foo_bar, $fooBar_Baz, int $foo = 1, self $bar = null, array $baz = [1], iterable $qux)
{
$fooBar;
$foo_bar;
diff --git a/src/Domain/Tests/Factory/DomainCollectionFactoryTest.php b/src/Domain/Tests/Factory/DomainCollectionFactoryTest.php
index e1eeffb5..70a75a36 100644
--- a/src/Domain/Tests/Factory/DomainCollectionFactoryTest.php
+++ b/src/Domain/Tests/Factory/DomainCollectionFactoryTest.php
@@ -14,10 +14,28 @@ final class DomainCollectionFactoryTest extends TestCase
{
public function testCreate(): void
{
- $this->assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create(null));
- $this->assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create([]));
- $this->assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create($this->createMock(DomainCollectionInterface::class)));
- $this->assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create([1, 2, 3]));
- $this->assertInstanceOf(DoctrineDomainCollection::class, DomainCollectionFactory::create($this->createMock(Collection::class)));
+ self::assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create(null));
+ self::assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create([]));
+ self::assertInstanceOf(DomainCollection::class, DomainCollectionFactory::create([1, 2, 3]));
+ self::assertInstanceOf(DoctrineDomainCollection::class, DomainCollectionFactory::create($this->createMock(Collection::class)));
+ self::assertSame($collection = $this->createMock(DomainCollectionInterface::class), DomainCollectionFactory::create($collection));
+ }
+
+ public function testCreateFromCallable(): void
+ {
+ $visited = false;
+ $collection = DomainCollectionFactory::createFromCallable(function () use (&$visited): iterable {
+ $visited = false;
+ yield 1;
+ yield 2;
+ $visited = true;
+ });
+
+ self::assertFalse($collection->isEmpty());
+ self::assertFalse($visited);
+ self::assertSame([1, 2], iterator_to_array($collection));
+ self::assertTrue($visited);
+ self::assertSame(1, $collection->first());
+ self::assertFalse($visited);
}
}
diff --git a/src/Domain/Tests/Factory/DomainIdFactoryTest.php b/src/Domain/Tests/Factory/DomainIdFactoryTest.php
index 558e6efa..ecbc07c1 100644
--- a/src/Domain/Tests/Factory/DomainIdFactoryTest.php
+++ b/src/Domain/Tests/Factory/DomainIdFactoryTest.php
@@ -14,11 +14,11 @@ final class DomainIdFactoryTest extends TestCase
{
public function testCreate(): void
{
- $this->assertInstanceOf(DomainId::class, DomainIdFactory::create(null));
- $this->assertInstanceOf(DomainId::class, DomainIdFactory::create(1));
- $this->assertInstanceOf(DomainId::class, DomainIdFactory::create($this->createMock(DomainIdInterface::class)));
- $this->assertInstanceOf(DomainId::class, DomainIdFactory::create('vgi00000000-0000-0000-0000-00000000000'));
- $this->assertInstanceOf(DomainUuid::class, DomainIdFactory::create($this->createMock(UuidInterface::class)));
- $this->assertInstanceOf(DomainUuid::class, DomainIdFactory::create('00000000-0000-0000-0000-000000000000'));
+ self::assertInstanceOf(DomainId::class, DomainIdFactory::create(null));
+ self::assertInstanceOf(DomainId::class, DomainIdFactory::create(1));
+ self::assertInstanceOf(DomainId::class, DomainIdFactory::create('vgi00000000-0000-0000-0000-00000000000'));
+ self::assertInstanceOf(DomainUuid::class, DomainIdFactory::create($this->createMock(UuidInterface::class)));
+ self::assertInstanceOf(DomainUuid::class, DomainIdFactory::create('00000000-0000-0000-0000-000000000000'));
+ self::assertSame($id = $this->createMock(DomainIdInterface::class), DomainIdFactory::create($id));
}
}
diff --git a/src/Domain/Tests/Factory/DomainObjectFactoryTest.php b/src/Domain/Tests/Factory/DomainObjectFactoryTest.php
index d9c7c524..5e0d79eb 100644
--- a/src/Domain/Tests/Factory/DomainObjectFactoryTest.php
+++ b/src/Domain/Tests/Factory/DomainObjectFactoryTest.php
@@ -15,19 +15,19 @@ public function testCreate(): void
{
$object = (new DomainObjectFactory())->create(TestObject::class, ['arg_a' => 1, 'arg_b' => 'foo', 'bar' => 'baz']);
- $this->assertInstanceOf(TestObject::class, $object);
- $this->assertSame(1, $object->a);
- $this->assertSame('foo', $object->b);
+ self::assertInstanceOf(TestObject::class, $object);
+ self::assertSame(1, $object->a);
+ self::assertSame('foo', $object->b);
}
public function testCreateWithDomainId(): void
{
- $this->assertInstanceOf(DomainId::class, (new DomainObjectFactory())->create(DomainId::class, [1]));
+ self::assertInstanceOf(DomainId::class, (new DomainObjectFactory())->create(DomainId::class, [1]));
}
public function testCreateWithDomainCollection(): void
{
- $this->assertInstanceOf(DomainCollection::class, (new DomainObjectFactory())->create(DomainCollection::class, [null]));
+ self::assertInstanceOf(DomainCollection::class, (new DomainObjectFactory())->create(DomainCollection::class, [null]));
}
public function testCreateWithUnknownObject(): void
@@ -43,8 +43,8 @@ public function testCreateWithUnknownNestedObject(): void
{
$factory = new DomainObjectFactory();
- $this->assertInstanceOf(KnownTestObject::class, $factory->create(KnownTestObject::class));
- $this->assertInstanceOf(KnownTestObject::class, $factory->create(KnownTestObject::class, ['unknown' => 'foo']));
+ self::assertInstanceOf(KnownTestObject::class, $factory->create(KnownTestObject::class));
+ self::assertInstanceOf(KnownTestObject::class, $factory->create(KnownTestObject::class, ['unknown' => 'foo']));
$this->expectException(\TypeError::class);
$this->expectExceptionMessageRegExp(sprintf('/^Argument 1 passed to %s::__construct\(\) must be an instance of MsgPhp\\\Domain\\\Tests\\\Factory\\\UnknownTestObject or null, integer given\b/', preg_quote(KnownTestObject::class)));
@@ -56,21 +56,21 @@ public function testCreateWithNamedContext(): void
{
$object = (new DomainObjectFactory())->create(TestObject::class, [1 => 'b', 'arg_a' => 'ignore', 0 => 'ignore', 'argA' => 'a']);
- $this->assertSame('a', $object->a);
- $this->assertSame('b', $object->b);
+ self::assertSame('a', $object->a);
+ self::assertSame('b', $object->b);
}
public function testCreateWithNumericContext(): void
{
$object = (new DomainObjectFactory())->create(TestObject::class, [1 => 'b', 'arg_a' => 'a', 0 => 'ignore']);
- $this->assertSame('a', $object->a);
- $this->assertSame('b', $object->b);
+ self::assertSame('a', $object->a);
+ self::assertSame('b', $object->b);
}
public function testCreateWithoutConstructor(): void
{
- $this->assertInstanceOf(EmptyTestObject::class, (new DomainObjectFactory())->create(EmptyTestObject::class, ['arg']));
+ self::assertInstanceOf(EmptyTestObject::class, (new DomainObjectFactory())->create(EmptyTestObject::class, ['arg']));
}
public function testCreateWithPrivateConstructor(): void
@@ -89,14 +89,14 @@ public function testNestedCreate(): void
'self' => ['test' => ['arg_a' => 'foo', 'arg_b' => 'bar'], 'other' => $other = new TestObject(1, 2)],
]);
- $this->assertInstanceOf(NestedTestObject::class, $object);
- $this->assertInstanceOf(TestObject::class, $object->test);
- $this->assertSame('nested_a', $object->test->a);
- $this->assertSame('nested_b', $object->test->b);
- $this->assertInstanceOf(NestedTestObject::class, $object->self);
- $this->assertSame('foo', $object->self->test->a);
- $this->assertSame('bar', $object->self->test->b);
- $this->assertSame($other, $object->self->other);
+ self::assertInstanceOf(NestedTestObject::class, $object);
+ self::assertInstanceOf(TestObject::class, $object->test);
+ self::assertSame('nested_a', $object->test->a);
+ self::assertSame('nested_b', $object->test->b);
+ self::assertInstanceOf(NestedTestObject::class, $object->self);
+ self::assertSame('foo', $object->self->test->a);
+ self::assertSame('bar', $object->self->test->b);
+ self::assertSame($other, $object->self->other);
}
public function testNestedCreateWithoutContext(): void
@@ -111,21 +111,21 @@ public function testNestedCreateWithoutContext(): void
public function testNestedCreateWithNestedFactory(): void
{
$nestedFactory = $this->createMock(DomainObjectFactoryInterface::class);
- $nestedFactory->expects($this->any())
+ $nestedFactory->expects(self::any())
->method('create')
->willReturn($nested = new TestObject(1, 2));
$factory = new DomainObjectFactory();
$factory->setNestedFactory($nestedFactory);
- $this->assertInstanceOf(NestedTestObject::class, $object = $factory->create(NestedTestObject::class, [['a', 'b']]));
- $this->assertSame($nested, $object->test);
+ self::assertInstanceOf(NestedTestObject::class, $object = $factory->create(NestedTestObject::class, [['a', 'b']]));
+ self::assertSame($nested, $object->test);
}
public function testNestedCreateWithInvalidNestedFactory(): void
{
$nestedFactory = $this->createMock(DomainObjectFactoryInterface::class);
- $nestedFactory->expects($this->any())
+ $nestedFactory->expects(self::any())
->method('create')
->willThrowException(InvalidClassException::create(TestObject::class));
diff --git a/src/Domain/Tests/Factory/EntityAwareFactoryTest.php b/src/Domain/Tests/Factory/EntityAwareFactoryTest.php
index 8d8364b5..c0168093 100644
--- a/src/Domain/Tests/Factory/EntityAwareFactoryTest.php
+++ b/src/Domain/Tests/Factory/EntityAwareFactoryTest.php
@@ -18,12 +18,12 @@ final class EntityAwareFactoryTest extends TestCase
protected function setUp(): void
{
$innerFactory = $this->createMock(DomainObjectFactoryInterface::class);
- $innerFactory->expects($this->any())
+ $innerFactory->expects(self::any())
->method('create')
->willReturnCallback(function ($class, $context) {
if ('id' === $class) {
$id = $this->createMock(DomainIdInterface::class);
- $id->expects($this->any())
+ $id->expects(self::any())
->method('toString')
->willReturn($context ? reset($context) : 'new');
@@ -38,7 +38,7 @@ protected function setUp(): void
});
$identityMapping = $this->createMock(DomainIdentityMappingInterface::class);
- $identityMapping->expects($this->any())
+ $identityMapping->expects(self::any())
->method('getIdentifierFieldNames')
->willReturn(['id_field', 'id_field2']);
@@ -47,26 +47,26 @@ protected function setUp(): void
public function testCreate(): void
{
- $this->assertInstanceOf(\stdClass::class, $object = $this->factory->create('foo'));
- $this->assertSame(['class' => 'foo', 'context' => []], (array) $object);
- $this->assertInstanceOf(\stdClass::class, $object = $this->factory->create('bar', ['context']));
- $this->assertSame(['class' => 'bar', 'context' => ['context']], (array) $object);
+ self::assertInstanceOf(\stdClass::class, $object = $this->factory->create('foo'));
+ self::assertSame(['class' => 'foo', 'context' => []], (array) $object);
+ self::assertInstanceOf(\stdClass::class, $object = $this->factory->create('bar', ['context']));
+ self::assertSame(['class' => 'bar', 'context' => ['context']], (array) $object);
}
public function testReference(): void
{
- $this->assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', 1));
- $this->assertSame(['class' => 'foo', 'context' => ['id_field' => 1, 'id_field2' => null]], (array) $object);
- $this->assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', ['id_field2' => 2, 'foo' => 'bar']));
- $this->assertSame(['class' => 'foo', 'context' => ['id_field2' => 2, 'foo' => 'bar', 'id_field' => null]], (array) $object);
+ self::assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', 1));
+ self::assertSame(['class' => 'foo', 'context' => ['id_field' => 1, 'id_field2' => null]], (array) $object);
+ self::assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', ['id_field2' => 2, 'foo' => 'bar']));
+ self::assertSame(['class' => 'foo', 'context' => ['id_field2' => 2, 'foo' => 'bar', 'id_field' => null]], (array) $object);
}
public function testIdentify(): void
{
- $this->assertSame('1', $this->factory->identify('id', '1')->toString());
- $this->assertSame('1', $this->factory->identify('alias_id', '1')->toString());
- $this->assertSame($id = $this->createMock(DomainIdInterface::class), $this->factory->identify('id', $id));
- $this->assertSame($id = $this->createMock(DomainIdInterface::class), $this->factory->identify('alias_id', $id));
+ self::assertSame('1', $this->factory->identify('id', '1')->toString());
+ self::assertSame('1', $this->factory->identify('alias_id', '1')->toString());
+ self::assertSame($id = $this->createMock(DomainIdInterface::class), $this->factory->identify('id', $id));
+ self::assertSame($id = $this->createMock(DomainIdInterface::class), $this->factory->identify('alias_id', $id));
$this->expectException(InvalidClassException::class);
@@ -75,8 +75,8 @@ public function testIdentify(): void
public function testNextIdentifier(): void
{
- $this->assertSame('new', $this->factory->nextIdentifier('id')->toString());
- $this->assertSame('new', $this->factory->nextIdentifier('alias_id')->toString());
+ self::assertSame('new', $this->factory->nextIdentifier('id')->toString());
+ self::assertSame('new', $this->factory->nextIdentifier('alias_id')->toString());
$this->expectException(InvalidClassException::class);
diff --git a/src/Domain/Tests/Infra/Doctrine/DomainCollectionTest.php b/src/Domain/Tests/Infra/Doctrine/DomainCollectionTest.php
index 9c4f97ec..c03793ec 100644
--- a/src/Domain/Tests/Infra/Doctrine/DomainCollectionTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/DomainCollectionTest.php
@@ -14,7 +14,7 @@ final class DomainCollectionTest extends AbstractDomainCollectionTest
{
public function testFromValueWithCollection(): void
{
- $this->assertEquals(new DomainCollection($collection = $this->createMock(Collection::class)), DomainCollection::fromValue($collection));
+ self::assertEquals(new DomainCollection($collection = $this->createMock(Collection::class)), DomainCollection::fromValue($collection));
}
protected static function createCollection(array $elements): DomainCollectionInterface
diff --git a/src/Domain/Tests/Infra/Doctrine/DomainEntityRepositoryTraitTest.php b/src/Domain/Tests/Infra/Doctrine/DomainEntityRepositoryTraitTest.php
index b97addb5..924ebe59 100644
--- a/src/Domain/Tests/Infra/Doctrine/DomainEntityRepositoryTraitTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/DomainEntityRepositoryTraitTest.php
@@ -30,7 +30,7 @@ public function testDuplicateFieldParameters(): void
$repository->addFieldCriteria($qb, ['foo.bar' => 'bar2']);
$repository->addFieldCriteria($qb, ['foo.bar' => 'bar3']);
- $this->assertCount(3, $qb->getParameters());
+ self::assertCount(3, $qb->getParameters());
}
protected function equalsEntity($expected, $actual)
@@ -41,7 +41,7 @@ protected function equalsEntity($expected, $actual)
$expectedValue = $property->getValue($expected);
$actualValue = $property->getValue($actual);
- if (is_object($expectedValue) && is_object($actualValue)) {
+ if (\is_object($expectedValue) && \is_object($actualValue)) {
if (!$this->equalsEntity($expectedValue, $actualValue)) {
$equals = false;
break;
diff --git a/src/Domain/Tests/Infra/Doctrine/DomainIdTypeTest.php b/src/Domain/Tests/Infra/Doctrine/DomainIdTypeTest.php
index b785a154..2de5ba45 100644
--- a/src/Domain/Tests/Infra/Doctrine/DomainIdTypeTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/DomainIdTypeTest.php
@@ -41,10 +41,10 @@ protected function setUp(): void
$this->type = Type::getType('domain_id');
$this->otherType = Type::getType('other_domain_id');
$this->platform = $this->createMock(AbstractPlatform::class);
- $this->platform->expects($this->any())
+ $this->platform->expects(self::any())
->method('getIntegerTypeDeclarationSQL')
->willReturn('native_integer_type');
- $this->platform->expects($this->any())
+ $this->platform->expects(self::any())
->method('getVarcharTypeDeclarationSQL')
->willReturn('native_string_type');
@@ -62,25 +62,25 @@ public function testSetClassWithInvalidClass(): void
public function testGetName(): void
{
- $this->assertSame(DomainIdType::NAME, $this->type->getName());
- $this->assertSame(OtherTestDomainIdType::NAME, $this->otherType->getName());
+ self::assertSame(DomainIdType::NAME, $this->type->getName());
+ self::assertSame(OtherTestDomainIdType::NAME, $this->otherType->getName());
}
public function testGetSQLDeclaration(): void
{
OtherTestDomainIdType::setDataType(Type::STRING);
- $this->assertSame('native_integer_type', $this->type->getSQLDeclaration([], $this->platform));
- $this->assertSame(Type::INTEGER, DomainIdType::getDataType());
- $this->assertSame('native_string_type', $this->otherType->getSQLDeclaration([], $this->platform));
- $this->assertSame(Type::STRING, OtherTestDomainIdType::getDataType());
+ self::assertSame('native_integer_type', $this->type->getSQLDeclaration([], $this->platform));
+ self::assertSame(Type::INTEGER, DomainIdType::getDataType());
+ self::assertSame('native_string_type', $this->otherType->getSQLDeclaration([], $this->platform));
+ self::assertSame(Type::STRING, OtherTestDomainIdType::getDataType());
}
public function testConvertToDatabaseValue(): void
{
- $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform));
- $this->assertSame('1', $this->type->convertToDatabaseValue(new DomainId('1'), $this->platform));
- $this->assertSame('1', $this->type->convertToDatabaseValue('1', $this->platform));
+ self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
+ self::assertSame('1', $this->type->convertToDatabaseValue(new DomainId('1'), $this->platform));
+ self::assertSame('1', $this->type->convertToDatabaseValue('1', $this->platform));
}
public function testConvertToPHPValue(): void
@@ -88,11 +88,11 @@ public function testConvertToPHPValue(): void
OtherTestDomainIdType::setDataType('string');
OtherTestDomainIdType::setClass(OtherTestDomainId::class);
- $this->assertNull($this->type->convertToPHPValue(null, $this->platform));
- $this->assertEquals(new DomainId('0'), $this->type->convertToPHPValue('foo', $this->platform));
- $this->assertEquals(new DomainId('1'), $this->type->convertToPHPValue('1', $this->platform));
- $this->assertEquals(new OtherTestDomainId('foo'), $this->otherType->convertToPHPValue('foo', $this->platform));
- $this->assertEquals(new OtherTestDomainId('1'), $this->otherType->convertToPHPValue('1', $this->platform));
+ self::assertNull($this->type->convertToPHPValue(null, $this->platform));
+ self::assertEquals(new DomainId('0'), $this->type->convertToPHPValue('foo', $this->platform));
+ self::assertEquals(new DomainId('1'), $this->type->convertToPHPValue('1', $this->platform));
+ self::assertEquals(new OtherTestDomainId('foo'), $this->otherType->convertToPHPValue('foo', $this->platform));
+ self::assertEquals(new OtherTestDomainId('1'), $this->otherType->convertToPHPValue('1', $this->platform));
}
}
diff --git a/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php b/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php
index c80cd6fd..e664b796 100644
--- a/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php
@@ -17,7 +17,7 @@ final class DomainIdentityMappingTest extends AbstractDomainIdentityMappingTest
public function testClassMapping(): void
{
- $this->assertSame(['id'], (new DomainIdentityMapping(self::$em, ['foo' => Entities\TestEntity::class]))->getIdentifierFieldNames('foo'));
+ self::assertSame(['id'], (new DomainIdentityMapping(self::$em, ['foo' => Entities\TestEntity::class]))->getIdentifierFieldNames('foo'));
}
protected static function createMapping(): DomainIdentityMappingInterface
diff --git a/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php
index 6ad5c50a..849896bc 100644
--- a/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php
@@ -21,59 +21,59 @@ final class EntityAwareFactoryTest extends TestCase
public function testCreate(): void
{
$innerFactory = $this->createMock(EntityAwareFactoryInterface::class);
- $innerFactory->expects($this->once())
+ $innerFactory->expects(self::once())
->method('create')
->with(Entities\TestEntity::class, ['foo' => 'bar'])
->willReturn($obj = new \stdClass());
$factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]);
- $this->assertSame($obj, $factory->create('alias', ['foo' => 'bar']));
+ self::assertSame($obj, $factory->create('alias', ['foo' => 'bar']));
}
public function testCreateWithDiscriminator(): void
{
$innerFactory = $this->createMock(EntityAwareFactoryInterface::class);
- $innerFactory->expects($this->once())
+ $innerFactory->expects(self::once())
->method('create')
->with(Entities\TestChildEntity::class, ['foo' => 'bar', 'discriminator' => 'child'])
->willReturn($obj = new \stdClass());
$factory = new EntityAwareFactory($innerFactory, self::$em);
- $this->assertSame($obj, $factory->create(Entities\TestParentEntity::class, ['foo' => 'bar', 'discriminator' => 'child']));
+ self::assertSame($obj, $factory->create(Entities\TestParentEntity::class, ['foo' => 'bar', 'discriminator' => 'child']));
}
public function testCreateWithObject(): void
{
$innerFactory = $this->createMock(EntityAwareFactoryInterface::class);
- $innerFactory->expects($this->once())
+ $innerFactory->expects(self::once())
->method('create')
->with(\stdClass::class)
->willReturn($obj = new \stdClass());
$factory = new EntityAwareFactory($innerFactory, self::$em);
- $this->assertSame($obj, $factory->create(\stdClass::class));
+ self::assertSame($obj, $factory->create(\stdClass::class));
}
public function testReference(): void
{
$factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em, ['alias' => Entities\TestEntity::class]);
- $this->assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestEntity::class, $id = $this->createMock(DomainIdInterface::class)));
- $this->assertInstanceOf(Entities\TestEntity::class, $ref);
- $this->assertSame($id, $ref->getId());
- $this->assertInstanceOf(Proxy::class, $ref = $factory->reference('alias', $id));
- $this->assertInstanceOf(Entities\TestEntity::class, $ref);
- $this->assertSame($id, $ref->getId());
+ self::assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestEntity::class, $id = $this->createMock(DomainIdInterface::class)));
+ self::assertInstanceOf(Entities\TestEntity::class, $ref);
+ self::assertSame($id, $ref->getId());
+ self::assertInstanceOf(Proxy::class, $ref = $factory->reference('alias', $id));
+ self::assertInstanceOf(Entities\TestEntity::class, $ref);
+ self::assertSame($id, $ref->getId());
}
public function testReferenceWithDiscriminator(): void
{
$factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em);
- $this->assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestParentEntity::class, ['id' => 'foo', 'discriminator' => 'child']));
- $this->assertInstanceOf(Entities\TestChildEntity::class, $ref);
- $this->assertSame('foo', $ref->id);
+ self::assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestParentEntity::class, ['id' => 'foo', 'discriminator' => 'child']));
+ self::assertInstanceOf(Entities\TestChildEntity::class, $ref);
+ self::assertSame('foo', $ref->id);
}
public function testReferenceWithUnknownClass(): void
@@ -97,13 +97,13 @@ public function testReferenceWithUnknownEntity(): void
public function testIdentify(): void
{
$innerFactory = $this->createMock(EntityAwareFactoryInterface::class);
- $innerFactory->expects($this->once())
+ $innerFactory->expects(self::once())
->method('identify')
->with(Entities\TestEntity::class, 1)
->willReturn($obj = $this->createMock(DomainIdInterface::class));
$factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]);
- $this->assertSame($obj, $factory->identify('alias', 1));
+ self::assertSame($obj, $factory->identify('alias', 1));
}
public function testIdentifyWithUnknownClass(): void
@@ -127,13 +127,13 @@ public function testIdentifyWithUnknownEntity(): void
public function testNextIdentifier(): void
{
$innerFactory = $this->createMock(EntityAwareFactoryInterface::class);
- $innerFactory->expects($this->once())
+ $innerFactory->expects(self::once())
->method('nextIdentifier')
->with(Entities\TestEntity::class)
->willReturn($obj = $this->createMock(DomainIdInterface::class));
$factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]);
- $this->assertSame($obj, $factory->nextIdentifier('alias'));
+ self::assertSame($obj, $factory->nextIdentifier('alias'));
}
public function testNextIdentifierWithUnknownClass(): void
diff --git a/src/Domain/Tests/Infra/Doctrine/EntityFieldsMappingTest.php b/src/Domain/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
deleted file mode 100644
index a5a15f59..00000000
--- a/src/Domain/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-assertSame(array_keys($available), $mapping);
- }
-}
diff --git a/src/Domain/Tests/Infra/Doctrine/EntityManagerTrait.php b/src/Domain/Tests/Infra/Doctrine/EntityManagerTrait.php
index 76fff5b8..da247cbf 100644
--- a/src/Domain/Tests/Infra/Doctrine/EntityManagerTrait.php
+++ b/src/Domain/Tests/Infra/Doctrine/EntityManagerTrait.php
@@ -26,7 +26,7 @@ public static function setUpBeforeClass(): void
}
$config = new Configuration();
- $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), dirname(dirname(__DIR__)).'/Fixtures/Entities'));
+ $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), \dirname(__DIR__, 2).'/Fixtures/Entities'));
$config->setProxyDir(\sys_get_temp_dir().'/msgphp_'.md5(microtime()));
$config->setProxyNamespace(__NAMESPACE__);
@@ -35,7 +35,9 @@ public static function setUpBeforeClass(): void
public static function tearDownAfterClass(): void
{
- if (null !== ($proxyDir = self::$em->getConfiguration()->getProxyDir()) && is_dir($proxyDir)) {
+ $proxyDir = self::$em->getConfiguration()->getProxyDir();
+
+ if (null !== $proxyDir && is_dir($proxyDir)) {
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($proxyDir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST) as $file) {
@($file->isDir() ? 'rmdir' : 'unlink')($file->getRealPath());
}
diff --git a/src/Domain/Tests/Infra/Doctrine/Hydration/ScalarHydratorTest.php b/src/Domain/Tests/Infra/Doctrine/Hydration/ScalarHydratorTest.php
index 75888bd0..c70a860d 100644
--- a/src/Domain/Tests/Infra/Doctrine/Hydration/ScalarHydratorTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/Hydration/ScalarHydratorTest.php
@@ -21,12 +21,12 @@ public function testHydrator(): void
self::$em->persist($entity = Entities\TestPrimitiveEntity::create(['id' => new DomainId('1')]));
self::$em->flush();
- $query = self::$em->createQuery('SELECT root.id FROM '.get_class($entity).' root');
+ $query = self::$em->createQuery('SELECT root.id FROM '.\get_class($entity).' root');
- $this->assertSame('1', $query->getScalarResult()[0]['id']);
+ self::assertSame('1', $query->getScalarResult()[0]['id']);
self::$em->getConfiguration()->addCustomHydrationMode(ScalarHydrator::NAME, ScalarHydrator::class);
- $this->assertSame(1, $query->getResult(ScalarHydrator::NAME)[0]['id']);
+ self::assertSame(1, $query->getResult(ScalarHydrator::NAME)[0]['id']);
}
}
diff --git a/src/Domain/Tests/Infra/Doctrine/Hydration/SingleScalarHydratorTest.php b/src/Domain/Tests/Infra/Doctrine/Hydration/SingleScalarHydratorTest.php
index 00d3b4f3..28b6b0f9 100644
--- a/src/Domain/Tests/Infra/Doctrine/Hydration/SingleScalarHydratorTest.php
+++ b/src/Domain/Tests/Infra/Doctrine/Hydration/SingleScalarHydratorTest.php
@@ -21,13 +21,13 @@ public function testHydrator(): void
self::$em->persist($entity = Entities\TestPrimitiveEntity::create(['id' => new DomainId('1')]));
self::$em->flush();
- $query = self::$em->createQuery('SELECT root.id FROM '.get_class($entity).' root');
+ $query = self::$em->createQuery('SELECT root.id FROM '.\get_class($entity).' root');
- $this->assertSame('1', $query->getSingleScalarResult());
+ self::assertSame('1', $query->getSingleScalarResult());
self::$em->getConfiguration()->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator::class);
- $this->assertSame(1, $query->getResult(SingleScalarHydrator::NAME));
- $this->assertSame(1, $query->getSingleResult(SingleScalarHydrator::NAME));
+ self::assertSame(1, $query->getResult(SingleScalarHydrator::NAME));
+ self::assertSame(1, $query->getSingleResult(SingleScalarHydrator::NAME));
}
}
diff --git a/src/Domain/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php b/src/Domain/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
new file mode 100644
index 00000000..453d03c8
--- /dev/null
+++ b/src/Domain/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
@@ -0,0 +1,26 @@
+ 'a', 'bar' => ['b']]);
- $this->assertSame(['a'], $mapping->getIdentifierFieldNames('foo'));
- $this->assertSame(['b'], $mapping->getIdentifierFieldNames('bar'));
+ self::assertSame(['a'], $mapping->getIdentifierFieldNames('foo'));
+ self::assertSame(['b'], $mapping->getIdentifierFieldNames('bar'));
}
public function testGetIdentifierFieldNamesWithEmptyMapping(): void
diff --git a/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php b/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php
index 19cedf31..7f55e8f6 100644
--- a/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php
+++ b/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php
@@ -11,7 +11,7 @@ final class GlobalObjectMemoryTest extends TestCase
{
public function testCreateDefault(): void
{
- $this->assertSame(GlobalObjectMemory::createDefault(), GlobalObjectMemory::createDefault());
+ self::assertSame(GlobalObjectMemory::createDefault(), GlobalObjectMemory::createDefault());
}
public function testAll(): void
@@ -23,7 +23,7 @@ public function testAll(): void
foreach ([$std1 = new \stdClass(), $std2 = new \stdClass(), $anonymous] as $object) {
$memory->persist($object);
- $objects[get_class($object)] = [];
+ $objects[\get_class($object)] = [];
}
$memory->persist($std1);
@@ -33,33 +33,33 @@ public function testAll(): void
}
}
- $this->assertSame([$std1, $std2], $objects[\stdClass::class]);
- $this->assertSame([$anonymous], $objects[get_class($anonymous)]);
+ self::assertSame([$std1, $std2], $objects[\stdClass::class]);
+ self::assertSame([$anonymous], $objects[\get_class($anonymous)]);
$memory->remove($anonymous);
- $class = get_class($anonymous);
+ $class = \get_class($anonymous);
$objects[$class] = [];
foreach ($memory->all($class) as $i => $object) {
$objects[$class][$i] = $object;
}
- $this->assertSame([], $objects[$class]);
+ self::assertSame([], $objects[$class]);
}
public function testContains(): void
{
$memory = new GlobalObjectMemory();
- $this->assertFalse($memory->contains($object = new \stdClass()));
+ self::assertFalse($memory->contains($object = new \stdClass()));
$memory->persist($object);
- $this->assertTrue($memory->contains($object));
+ self::assertTrue($memory->contains($object));
$memory->remove($object);
- $this->assertFalse($memory->contains($object));
+ self::assertFalse($memory->contains($object));
}
}
diff --git a/src/Domain/Tests/Infra/InMemory/ObjectFieldAccessorTest.php b/src/Domain/Tests/Infra/InMemory/ObjectFieldAccessorTest.php
index 74929084..2e43bb83 100644
--- a/src/Domain/Tests/Infra/InMemory/ObjectFieldAccessorTest.php
+++ b/src/Domain/Tests/Infra/InMemory/ObjectFieldAccessorTest.php
@@ -40,13 +40,13 @@ public function otherField()
}
};
- $this->assertNull($accessor->getValue($object, 'field'));
- $this->assertSame('foo', $accessor->getValue($object, 'field2'));
- $this->assertSame(1, $accessor->getValue($object2, 'fieldA'));
- $this->assertTrue($accessor->getValue($object2, 'fieldB'));
- $this->assertFalse($accessor->getValue($object2, 'fieldC'));
- $this->assertSame('bar', $accessor->getValue($object2, 'otherField'));
- $this->assertSame(1, $accessor->getValue($object2, 'getFieldA'));
+ self::assertNull($accessor->getValue($object, 'field'));
+ self::assertSame('foo', $accessor->getValue($object, 'field2'));
+ self::assertSame(1, $accessor->getValue($object2, 'fieldA'));
+ self::assertTrue($accessor->getValue($object2, 'fieldB'));
+ self::assertFalse($accessor->getValue($object2, 'fieldC'));
+ self::assertSame('bar', $accessor->getValue($object2, 'otherField'));
+ self::assertSame(1, $accessor->getValue($object2, 'getFieldA'));
}
/**
diff --git a/src/Domain/Tests/Infra/Messenger/DomainMessageBusTest.php b/src/Domain/Tests/Infra/Messenger/DomainMessageBusTest.php
new file mode 100644
index 00000000..82ab8bbd
--- /dev/null
+++ b/src/Domain/Tests/Infra/Messenger/DomainMessageBusTest.php
@@ -0,0 +1,24 @@
+createMock(MessageBusInterface::class);
+ $bus->expects(self::once())
+ ->method('dispatch')
+ ->with($message = new \stdClass())
+ ->willReturn(new Envelope($message));
+
+ (new DomainMessageBus($bus))->dispatch($message);
+ }
+}
diff --git a/src/Domain/Tests/Infra/SimpleBus/DomainMessageBusTest.php b/src/Domain/Tests/Infra/SimpleBus/DomainMessageBusTest.php
index f232aca3..d607f5c5 100644
--- a/src/Domain/Tests/Infra/SimpleBus/DomainMessageBusTest.php
+++ b/src/Domain/Tests/Infra/SimpleBus/DomainMessageBusTest.php
@@ -13,11 +13,10 @@ final class DomainMessageBusTest extends TestCase
public function testDispatch(): void
{
$bus = $this->createMock(MessageBus::class);
- $bus->expects($this->once())
+ $bus->expects(self::once())
->method('handle')
- ->with($message = new \stdClass())
- ->willReturn('foo');
+ ->with($message = new \stdClass());
- $this->assertNull((new DomainMessageBus($bus))->dispatch($message));
+ (new DomainMessageBus($bus))->dispatch($message);
}
}
diff --git a/src/Domain/Tests/Infra/Uuid/DomainIdTest.php b/src/Domain/Tests/Infra/Uuid/DomainIdTest.php
index 97084344..9607540d 100644
--- a/src/Domain/Tests/Infra/Uuid/DomainIdTest.php
+++ b/src/Domain/Tests/Infra/Uuid/DomainIdTest.php
@@ -16,13 +16,13 @@ public function testFromValue(): void
{
$uuid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
- $this->assertInstanceOf(DomainId::class, DomainId::fromValue(null));
- $this->assertInstanceOf(DomainId::class, DomainId::fromValue('00000000-0000-0000-0000-000000000000'));
- $this->assertNotSame((array) new DomainId(), (array) DomainId::fromValue(null));
- $this->assertNotSame((array) new DomainId(null), (array) DomainId::fromValue(null));
- $this->assertNotSame((array) new DomainId($uuid), (array) DomainId::fromValue(null));
- $this->assertSame((array) new DomainId($uuid), (array) DomainId::fromValue($uuid));
- $this->assertInstanceOf(OtherTestDomainId::class, OtherTestDomainId::fromValue(null));
+ self::assertInstanceOf(DomainId::class, DomainId::fromValue(null));
+ self::assertInstanceOf(DomainId::class, DomainId::fromValue('00000000-0000-0000-0000-000000000000'));
+ self::assertNotSame((array) new DomainId(), (array) DomainId::fromValue(null));
+ self::assertNotSame((array) new DomainId(null), (array) DomainId::fromValue(null));
+ self::assertNotSame((array) new DomainId($uuid), (array) DomainId::fromValue(null));
+ self::assertSame((array) new DomainId($uuid), (array) DomainId::fromValue($uuid));
+ self::assertInstanceOf(OtherTestDomainId::class, OtherTestDomainId::fromValue(null));
}
public function testFromValueWithInvalidUuid(): void
@@ -34,42 +34,42 @@ public function testFromValueWithInvalidUuid(): void
public function testIsEmpty(): void
{
- $this->assertFalse((new DomainId())->isEmpty());
- $this->assertFalse((new DomainId($this->createMock(UuidInterface::class)))->isEmpty());
+ self::assertFalse((new DomainId())->isEmpty());
+ self::assertFalse((new DomainId($this->createMock(UuidInterface::class)))->isEmpty());
}
public function testEquals(): void
{
$id = new DomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'));
- $this->assertTrue($id->equals($id));
- $this->assertTrue($id->equals(new DomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'))));
- $this->assertFalse($id->equals(new DomainId()));
- $this->assertFalse($id->equals(new OtherTestDomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'))));
+ self::assertTrue($id->equals($id));
+ self::assertTrue($id->equals(new DomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'))));
+ self::assertFalse($id->equals(new DomainId()));
+ self::assertFalse($id->equals(new OtherTestDomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'))));
}
public function testToString(): void
{
$id = new DomainId(Uuid::fromString($uuid = '00000000-0000-0000-0000-000000000000'));
- $this->assertSame($uuid, $id->toString());
- $this->assertSame($uuid, (string) $id);
- $this->assertNotSame((new DomainId())->toString(), (new DomainId())->toString());
- $this->assertNotSame((string) new DomainId(), (string) new DomainId());
+ self::assertSame($uuid, $id->toString());
+ self::assertSame($uuid, (string) $id);
+ self::assertNotSame((new DomainId())->toString(), (new DomainId())->toString());
+ self::assertNotSame((string) new DomainId(), (string) new DomainId());
}
public function testSerialize(): void
{
$id = new DomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'));
- $this->assertEquals($id->toString(), (string) unserialize(serialize($id)));
+ self::assertEquals($id->toString(), (string) unserialize(serialize($id)));
}
public function testJsonSerialize(): void
{
$id = new DomainId(Uuid::fromString('00000000-0000-0000-0000-000000000000'));
- $this->assertSame($id->toString(), json_decode(json_encode($id)));
+ self::assertSame($id->toString(), json_decode((string) json_encode($id)));
}
}
diff --git a/src/Domain/Tests/Message/MessageDispatchingTraitTest.php b/src/Domain/Tests/Message/MessageDispatchingTraitTest.php
index a4acbaf7..5f5248ab 100644
--- a/src/Domain/Tests/Message/MessageDispatchingTraitTest.php
+++ b/src/Domain/Tests/Message/MessageDispatchingTraitTest.php
@@ -13,21 +13,16 @@ final class MessageDispatchingTraitTest extends TestCase
public function testDispatch(): void
{
$factory = $this->createMock(DomainObjectFactoryInterface::class);
- $factory->expects($this->once())
+ $factory->expects(self::once())
->method('create')
- ->willReturnCallback(function ($class, $context) {
- $o = new \stdClass();
- $o->class = $class;
- $o->context = $context;
-
- return $o;
- });
+ ->with('class', ['context'])
+ ->willReturn($message = new \stdClass());
$bus = $this->createMock(DomainMessageBusInterface::class);
- $bus->expects($this->once())
+ $bus->expects(self::once())
->method('dispatch')
- ->willReturnArgument(0);
+ ->with($message);
- $this->assertSame(['class' => 'class', 'context' => ['argument' => 'value', 1]], (array) $this->getObject($factory, $bus)->dispatch('class', ['argument' => 'value', 1]));
+ self::assertNull($this->getObject($factory, $bus)->dispatch('class', ['context']));
}
private function getObject(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus)
diff --git a/src/Domain/Tests/PaginatedDomainCollectionTest.php b/src/Domain/Tests/PaginatedDomainCollectionTest.php
new file mode 100644
index 00000000..4e9363c3
--- /dev/null
+++ b/src/Domain/Tests/PaginatedDomainCollectionTest.php
@@ -0,0 +1,40 @@
+getOffset());
+ self::assertSame(0., $collection->getLimit());
+ self::assertSame(1., $collection->getCurrentPage());
+ self::assertSame(1., $collection->getLastPage());
+ self::assertSame(4., $collection->getTotalCount());
+ self::assertCount(4, $collection);
+ }
+
+ public function testPagination(): void
+ {
+ $collection = new PaginatedDomainCollection([], 8., 2., 2., 12.);
+
+ self::assertSame(8., $collection->getOffset());
+ self::assertSame(2., $collection->getLimit());
+ self::assertSame(5., $collection->getCurrentPage());
+ self::assertSame(6., $collection->getLastPage());
+ self::assertSame(12., $collection->getTotalCount());
+ self::assertCount(2, $collection);
+ }
+
+ protected static function createCollection(array $elements): DomainCollectionInterface
+ {
+ return new PaginatedDomainCollection($elements);
+ }
+}
diff --git a/src/Domain/composer.json b/src/Domain/composer.json
index 1022f641..e59c9886 100644
--- a/src/Domain/composer.json
+++ b/src/Domain/composer.json
@@ -2,8 +2,8 @@
"name": "msgphp/domain",
"type": "library",
"license": "MIT",
- "description": "Base domain layer",
- "keywords": ["entity"],
+ "description": "A base domain layer to rapidly built other domain layers",
+ "keywords": ["domain-driven-design", "ddd", "domain model", "rich domain model", "entity", "event sourcing", "es", "command query responsibility segregation", "cqrs"],
"authors": [
{
"name": "Roland Franssen",
@@ -30,9 +30,10 @@
"ramsey/uuid-doctrine": "^1.4",
"simple-bus/message-bus": "^3.0",
"symfony/config": "^3.4|^4.0",
- "symfony/dependency-injection": "^3.4|^4.0",
+ "symfony/dependency-injection": "^3.4.16|^4.0.16|^4.1.5",
"symfony/http-kernel": "^3.4|^4.0",
- "symfony/phpunit-bridge": "^3.4|^4.0",
+ "symfony/messenger": "^4.2",
+ "symfony/phpunit-bridge": "^3.4.9|^4.0.9",
"symfony/property-access": "^3.4|^4.0",
"symfony/var-dumper": "^3.4|^4.0"
},
@@ -44,7 +45,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "0.8-dev"
}
}
}
diff --git a/src/Eav/Command/CreateAttributeCommand.php b/src/Eav/Command/CreateAttributeCommand.php
new file mode 100644
index 00000000..e2d8a69f
--- /dev/null
+++ b/src/Eav/Command/CreateAttributeCommand.php
@@ -0,0 +1,18 @@
+
+ */
+class CreateAttributeCommand
+{
+ public $context;
+
+ final public function __construct(array $context)
+ {
+ $this->context = $context;
+ }
+}
diff --git a/src/Eav/Command/DeleteAttributeCommand.php b/src/Eav/Command/DeleteAttributeCommand.php
new file mode 100644
index 00000000..24e5ee73
--- /dev/null
+++ b/src/Eav/Command/DeleteAttributeCommand.php
@@ -0,0 +1,18 @@
+
+ */
+class DeleteAttributeCommand
+{
+ public $attributeId;
+
+ final public function __construct($attributeId)
+ {
+ $this->attributeId = $attributeId;
+ }
+}
diff --git a/src/Eav/Command/Handler/CreateAttributeHandler.php b/src/Eav/Command/Handler/CreateAttributeHandler.php
new file mode 100644
index 00000000..e499ef6d
--- /dev/null
+++ b/src/Eav/Command/Handler/CreateAttributeHandler.php
@@ -0,0 +1,37 @@
+
+ */
+final class CreateAttributeHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, AttributeRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(CreateAttributeCommand $command): void
+ {
+ $attribute = $this->factory->create(Attribute::class, $command->context + ['id' => $this->factory->nextIdentifier(Attribute::class)]);
+
+ $this->repository->save($attribute);
+ $this->dispatch(AttributeCreatedEvent::class, [$attribute]);
+ }
+}
diff --git a/src/Eav/Command/Handler/DeleteAttributeHandler.php b/src/Eav/Command/Handler/DeleteAttributeHandler.php
new file mode 100644
index 00000000..0309a96a
--- /dev/null
+++ b/src/Eav/Command/Handler/DeleteAttributeHandler.php
@@ -0,0 +1,42 @@
+
+ */
+final class DeleteAttributeHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, AttributeRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(DeleteAttributeCommand $command): void
+ {
+ try {
+ $attribute = $this->repository->find($this->factory->identify(Attribute::class, $command->attributeId));
+ } catch (EntityNotFoundException $e) {
+ return;
+ }
+
+ $this->repository->delete($attribute);
+ $this->dispatch(AttributeDeletedEvent::class, [$attribute]);
+ }
+}
diff --git a/src/Eav/Entity/AttributeValue.php b/src/Eav/Entity/AttributeValue.php
index cf6b9c25..025d9160 100644
--- a/src/Eav/Entity/AttributeValue.php
+++ b/src/Eav/Entity/AttributeValue.php
@@ -21,6 +21,11 @@ abstract class AttributeValue
private $value;
private $isNull;
+ public static function getChecksum($value): string
+ {
+ return md5(serialize([\gettype($value), static::prepareChecksumValue($value)]));
+ }
+
public function __construct(Attribute $attribute, $value)
{
$this->attribute = $attribute;
@@ -40,9 +45,13 @@ public function getAttributeId(): AttributeIdInterface
return $this->attribute->getId();
}
- public function getValue()
+ final public function getValue()
{
- if ($this->isNull || null !== $this->value) {
+ if ($this->isNull) {
+ return null;
+ }
+
+ if (null !== $this->value) {
return $this->value;
}
@@ -53,39 +62,43 @@ public function getValue()
return $this->value = $value;
}
- public function getChecksum(): string
- {
- return $this->checksum;
- }
-
- public function changeValue($value): void
+ final public function changeValue($value): void
{
- $this->isNull = true;
- $this->boolValue = $this->intValue = $this->floatValue = $this->stringValue = $this->dateTimeValue = $this->value = null;
+ $this->doClearValue();
+ $this->isNull = null === $value;
- if (null !== $value) {
+ if (!$this->isNull) {
$this->doSetValue($value);
-
- $this->value = $value;
}
- $this->checksum = md5(serialize($value));
+ $this->value = $value;
+ $this->checksum = static::getChecksum($value);
+ }
+
+ protected static function prepareChecksumValue($value)
+ {
+ return $value;
+ }
+
+ protected function doClearValue(): void
+ {
+ $this->boolValue = $this->intValue = $this->floatValue = $this->stringValue = $this->dateTimeValue = null;
}
protected function doSetValue($value): void
{
- if (is_bool($value)) {
+ if (\is_bool($value)) {
$this->boolValue = $value;
- } elseif (is_int($value)) {
+ } elseif (\is_int($value)) {
$this->intValue = $value;
- } elseif (is_float($value)) {
+ } elseif (\is_float($value)) {
$this->floatValue = $value;
- } elseif (is_string($value)) {
+ } elseif (\is_string($value)) {
$this->stringValue = $value;
} elseif ($value instanceof \DateTimeInterface) {
$this->dateTimeValue = $value;
} else {
- throw new \LogicException(sprintf('Unsupported attribute value type "%s".', gettype($value)));
+ throw new \LogicException(sprintf('Unsupported attribute value type "%s".', \gettype($value)));
}
}
diff --git a/src/Eav/Entity/Fields/AttributeValueField.php b/src/Eav/Entity/Features/EntityAttributeValue.php
similarity index 75%
rename from src/Eav/Entity/Fields/AttributeValueField.php
rename to src/Eav/Entity/Features/EntityAttributeValue.php
index 88fd814b..c2837bd1 100644
--- a/src/Eav/Entity/Fields/AttributeValueField.php
+++ b/src/Eav/Entity/Features/EntityAttributeValue.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace MsgPhp\Eav\Entity\Fields;
+namespace MsgPhp\Eav\Entity\Features;
use MsgPhp\Eav\{AttributeIdInterface, AttributeValueIdInterface};
use MsgPhp\Eav\Entity\{Attribute, AttributeValue};
@@ -10,17 +10,12 @@
/**
* @author Roland Franssen
*/
-trait AttributeValueField
+trait EntityAttributeValue
{
/** @var AttributeValue */
private $attributeValue;
- public function getAttributeValue(): AttributeValue
- {
- return $this->attributeValue;
- }
-
- public function getAttributeValueId(): AttributeValueIdInterface
+ public function getId(): AttributeValueIdInterface
{
return $this->attributeValue->getId();
}
@@ -39,4 +34,9 @@ public function getValue()
{
return $this->attributeValue->getValue();
}
+
+ public function changeValue($value): void
+ {
+ $this->attributeValue->changeValue($value);
+ }
}
diff --git a/src/Eav/Event/AttributeCreatedEvent.php b/src/Eav/Event/AttributeCreatedEvent.php
new file mode 100644
index 00000000..9302d9bc
--- /dev/null
+++ b/src/Eav/Event/AttributeCreatedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class AttributeCreatedEvent
+{
+ public $attribute;
+
+ final public function __construct(Attribute $attribute)
+ {
+ $this->attribute = $attribute;
+ }
+}
diff --git a/src/Eav/Event/AttributeDeletedEvent.php b/src/Eav/Event/AttributeDeletedEvent.php
new file mode 100644
index 00000000..cb041690
--- /dev/null
+++ b/src/Eav/Event/AttributeDeletedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class AttributeDeletedEvent
+{
+ public $attribute;
+
+ final public function __construct(Attribute $attribute)
+ {
+ $this->attribute = $attribute;
+ }
+}
diff --git a/src/Eav/Infra/Doctrine/EntityFieldsMapping.php b/src/Eav/Infra/Doctrine/EntityFieldsMapping.php
deleted file mode 100644
index bffdabe6..00000000
--- a/src/Eav/Infra/Doctrine/EntityFieldsMapping.php
+++ /dev/null
@@ -1,31 +0,0 @@
-
- *
- * @internal
- */
-final class EntityFieldsMapping implements ObjectFieldMappingProviderInterface
-{
- public static function getObjectFieldMapping(): array
- {
- return [
- Fields\AttributeValueField::class => [
- 'attributeValue' => [
- 'type' => self::TYPE_MANY_TO_ONE,
- 'targetEntity' => AttributeValue::class,
- 'joinColumns' => [
- ['nullable' => false],
- ],
- ],
- ],
- ];
- }
-}
diff --git a/src/Eav/Infra/Doctrine/ObjectFieldMappings.php b/src/Eav/Infra/Doctrine/ObjectFieldMappings.php
new file mode 100644
index 00000000..f3435698
--- /dev/null
+++ b/src/Eav/Infra/Doctrine/ObjectFieldMappings.php
@@ -0,0 +1,30 @@
+
+ *
+ * @internal
+ */
+final class ObjectFieldMappings implements ObjectFieldMappingsProviderInterface
+{
+ public static function provideObjectFieldMappings(): iterable
+ {
+ yield Features\EntityAttributeValue::class => [
+ 'attributeValue' => [
+ 'type' => self::TYPE_ONE_TO_ONE,
+ 'targetEntity' => AttributeValue::class,
+ 'cascade' => ['all'],
+ 'joinColumns' => [
+ ['nullable' => false, 'onDelete' => 'CASCADE'],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/src/Eav/Infra/Doctrine/Repository/EntityAttributeValueRepositoryTrait.php b/src/Eav/Infra/Doctrine/Repository/EntityAttributeValueRepositoryTrait.php
new file mode 100644
index 00000000..8555b5a5
--- /dev/null
+++ b/src/Eav/Infra/Doctrine/Repository/EntityAttributeValueRepositoryTrait.php
@@ -0,0 +1,37 @@
+
+ */
+trait EntityAttributeValueRepositoryTrait
+{
+ use DomainEntityRepositoryTrait;
+
+ private function addAttributeCriteria(QueryBuilder $qb, AttributeIdInterface $attributeId, $value = null): void
+ {
+ $field = $this->alias.'.'.$this->attributeValueField;
+ $targetClass = $this->em->getClassMetadata($this->class)->getAssociationMapping($this->attributeValueField)['targetEntity'];
+
+ if (!is_subclass_of($targetClass, AttributeValue::class)) {
+ throw new \LogicException(sprintf('The field "%s" is expected to be an association mapping for "%s", got "%s".', $this->class.'.'.$this->attributeValueField, AttributeValue::class, $targetClass));
+ }
+
+ if (3 === \func_num_args()) {
+ $qb->join($field, 'attribute_value', Join::WITH, 'attribute_value.checksum = '.$this->addFieldParameter($qb, $this->attributeValueField, $targetClass::getChecksum($value)));
+ } else {
+ $qb->join($field, 'attribute_value');
+ }
+
+ $qb->join('attribute_value.attribute', 'attribute', Join::WITH, 'attribute.id = '.$this->addFieldParameter($qb, 'attribute', $attributeId));
+ }
+}
diff --git a/src/Eav/README.md b/src/Eav/README.md
index 1ba56165..998ad0c9 100644
--- a/src/Eav/README.md
+++ b/src/Eav/README.md
@@ -1,4 +1,4 @@
-# Entity-Attribute-Value domain layer
+# Entity-Attribute-Value Domain
A domain layer providing basic [EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model)
management.
@@ -17,8 +17,9 @@ composer require msgphp/eav
## Features
+- Symfony messenger commands & events
- Doctrine persistence
-- Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null`
+- Default attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null`
## Documentation
diff --git a/src/Eav/Tests/Entity/AttributeValueTest.php b/src/Eav/Tests/Entity/AttributeValueTest.php
index 1e3dfac5..a2976dfb 100644
--- a/src/Eav/Tests/Entity/AttributeValueTest.php
+++ b/src/Eav/Tests/Entity/AttributeValueTest.php
@@ -13,15 +13,14 @@ final class AttributeValueTest extends TestCase
public function testCreate(): void
{
$attribute = $this->createMock(Attribute::class);
- $attribute->expects($this->any())
+ $attribute->expects(self::any())
->method('getId')
->willReturn($this->createMock(AttributeIdInterface::class));
$attributeValue = $this->createEntity($this->createMock(AttributeValueIdInterface::class), $attribute, 'value');
- $this->assertSame($attribute, $attributeValue->getAttribute());
- $this->assertSame($attribute->getId(), $attributeValue->getAttributeId());
- $this->assertSame('value', $attributeValue->getValue());
- $this->assertSame(md5(serialize('value')), $attributeValue->getChecksum());
+ self::assertSame($attribute, $attributeValue->getAttribute());
+ self::assertSame($attribute->getId(), $attributeValue->getAttributeId());
+ self::assertSame('value', $attributeValue->getValue());
}
/**
@@ -30,14 +29,20 @@ public function testCreate(): void
public function testChangeValue($initialValue, $newValue): void
{
$attributeValue = $this->createEntity($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), $initialValue);
- $checksum = $attributeValue->getChecksum();
- $this->assertSame($initialValue, $attributeValue->getValue());
+ self::assertSame($initialValue, $attributeValue->getValue());
$attributeValue->changeValue($newValue);
- $this->assertNotSame($checksum, $attributeValue->getChecksum());
- $this->assertSame($newValue, $attributeValue->getValue());
+ self::assertSame($newValue, $attributeValue->getValue());
+ }
+
+ public function testGetChecksum(): void
+ {
+ self::assertSame(AttributeValue::getChecksum('foo'), AttributeValue::getChecksum('foo'));
+ self::assertSame(AttributeValue::getChecksum(1), AttributeValue::getChecksum(1));
+ self::assertNotSame(AttributeValue::getChecksum('foo'), AttributeValue::getChecksum('bar'));
+ self::assertNotSame(AttributeValue::getChecksum(1), AttributeValue::getChecksum('1'));
}
public function provideAttributeValues(): iterable
@@ -65,7 +70,7 @@ public function testLazyGetValue($value, string $type): void
->enableProxyingToOriginalMethods()
->getMockForAbstractClass();
- $this->assertNull($attributeValue->getValue());
+ self::assertNull($attributeValue->getValue());
$propertyValueRefl = new \ReflectionProperty(AttributeValue::class, $type.'Value');
$propertyValueRefl->setAccessible(true);
@@ -75,7 +80,7 @@ public function testLazyGetValue($value, string $type): void
$propertyValueRefl->setAccessible(true);
$propertyValueRefl->setValue($attributeValue, false);
- $this->assertSame($value, $attributeValue->getValue());
+ self::assertSame($value, $attributeValue->getValue());
}
public function provideLazyAttributeValues(): iterable
diff --git a/src/Eav/Tests/Entity/Features/EntityAttributeValueTest.php b/src/Eav/Tests/Entity/Features/EntityAttributeValueTest.php
new file mode 100644
index 00000000..d9593793
--- /dev/null
+++ b/src/Eav/Tests/Entity/Features/EntityAttributeValueTest.php
@@ -0,0 +1,60 @@
+createMock(Attribute::class);
+ $attribute->expects(self::once())
+ ->method('getId')
+ ->willReturn($attributeId = $this->createMock(AttributeIdInterface::class));
+ /** @var AttributeValue $attributeValue */
+ $object = $this->getObject('value', $attribute, $attributeValue);
+
+ self::assertSame($attribute, $object->getAttribute());
+ self::assertSame($attributeId, $object->getAttributeId());
+ self::assertSame('value', $object->getValue());
+ self::assertSame('value', $attributeValue->getValue());
+ self::assertNull($object->changeValue('other'));
+ self::assertSame('other', $object->getValue());
+ self::assertSame('other', $attributeValue->getValue());
+ }
+
+ private function getObject($value, $attribute, &$attributeValue = null)
+ {
+ $attributeValueId = $this->createMock(AttributeValueIdInterface::class);
+ $attributeValue = new class($attributeValueId, $attribute, $value) extends AttributeValue {
+ private $id;
+
+ public function __construct($id, $attribute, $value)
+ {
+ parent::__construct($attribute, $value);
+
+ $this->id = $id;
+ }
+
+ public function getId(): AttributeValueIdInterface
+ {
+ return $this->id;
+ }
+ };
+
+ return new class($attributeValue) {
+ use EntityAttributeValue;
+
+ public function __construct($attributeValue)
+ {
+ $this->attributeValue = $attributeValue;
+ }
+ };
+ }
+}
diff --git a/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php b/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php
deleted file mode 100644
index e79df84d..00000000
--- a/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-createMock(AttributeValue::class);
- $value->expects($this->any())
- ->method('getId')
- ->willReturn($this->createMock(AttributeValueIdInterface::class));
- $value->expects($this->any())
- ->method('getAttribute')
- ->willReturn($this->createMock(Attribute::class));
- $value->expects($this->any())
- ->method('getAttributeId')
- ->willReturn($this->createMock(AttributeIdInterface::class));
- $value->expects($this->any())
- ->method('getValue')
- ->willReturn('value');
- $object = $this->getObject($value);
-
- $this->assertSame($value, $object->getAttributeValue());
- $this->assertSame($value->getId(), $object->getAttributeValueId());
- $this->assertSame($value->getAttribute(), $object->getAttribute());
- $this->assertSame($value->getAttributeId(), $object->getAttributeId());
- $this->assertSame($value->getValue(), $object->getValue());
- }
-
- private function getObject($value)
- {
- return new class($value) {
- use AttributeValueField;
-
- public function __construct($value)
- {
- $this->attributeValue = $value;
- }
- };
- }
-}
diff --git a/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php b/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php
index d7de1740..cefb1bd7 100644
--- a/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php
+++ b/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php
@@ -13,25 +13,26 @@
final class AttributesFieldTest extends TestCase
{
- public function testGetAttributes(): void
+ public function testField(): void
{
$attributeValue1 = $this->createMock(AttributeValue::class);
- $attributeValue1->expects($this->any())
+ $attributeValue1->expects(self::any())
->method('getAttribute')
->willReturn($attribute1 = $this->createMock(Attribute::class));
$attributeValue2 = $this->createMock(AttributeValue::class);
- $attributeValue2->expects($this->any())
+ $attributeValue2->expects(self::any())
->method('getAttribute')
->willReturn($attribute2 = $this->createMock(Attribute::class));
- $attribute1->expects($this->any())
+ $attribute1->expects(self::any())
->method('getId')
->willReturn(new AttributeId('attr1'));
- $attribute2->expects($this->any())
+ $attribute2->expects(self::any())
->method('getId')
->willReturn(new AttributeId('attr2'));
$object = $this->getObject(new DomainCollection([$attributeValue1, $attributeValue2, $attributeValue1]));
- $this->assertSame([$attribute1, $attribute2], iterator_to_array($object->getAttributes()));
+ self::assertInstanceOf(DomainCollectionInterface::class, $collection = $object->getAttributes());
+ self::assertSame([$attribute1, $attribute2], iterator_to_array($collection));
}
private function getObject($value)
diff --git a/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php b/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
deleted file mode 100644
index c5c8b1cb..00000000
--- a/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-assertSame(array_keys($available), $mapping);
- }
-}
diff --git a/src/Eav/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php b/src/Eav/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
new file mode 100644
index 00000000..3e1b827e
--- /dev/null
+++ b/src/Eav/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
@@ -0,0 +1,26 @@
+
+ *
+ * @internal
+ */
+final class CleanupPass implements CompilerPassInterface
+{
+ public function process(ContainerBuilder $container): void
+ {
+ ContainerHelper::removeIf($container, !$container->has(Repository\AttributeRepositoryInterface::class), [
+ Command\Handler\CreateAttributeHandler::class,
+ Command\Handler\DeleteAttributeHandler::class,
+ ]);
+ }
+}
diff --git a/src/EavBundle/DependencyInjection/Configuration.php b/src/EavBundle/DependencyInjection/Configuration.php
index 2e3b283f..69d183c5 100644
--- a/src/EavBundle/DependencyInjection/Configuration.php
+++ b/src/EavBundle/DependencyInjection/Configuration.php
@@ -7,8 +7,8 @@
use MsgPhp\Domain\DomainIdInterface;
use MsgPhp\Domain\Infra\Config\{NodeBuilder, TreeBuilder};
use MsgPhp\Domain\Infra\DependencyInjection\ConfigHelper;
-use MsgPhp\Eav\{AttributeId, AttributeIdInterface, AttributeValueId, AttributeValueIdInterface, Entity};
-use MsgPhp\Eav\Infra\Uuid as UuidInfra;
+use MsgPhp\Eav\{AttributeId, AttributeIdInterface, AttributeValueId, AttributeValueIdInterface, Command, Entity};
+use MsgPhp\Eav\Infra\{Doctrine as DoctrineInfra, Uuid as UuidInfra};
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
@@ -34,7 +34,27 @@ final class Configuration implements ConfigurationInterface
AttributeIdInterface::class => UuidInfra\AttributeId::class,
AttributeValueIdInterface::class => UuidInfra\AttributeValueId::class,
];
- private const COMMAND_MAPPING = [];
+ public const DOCTRINE_TYPE_MAPPING = [
+ AttributeIdInterface::class => DoctrineInfra\Type\AttributeIdType::class,
+ AttributeValueIdInterface::class => DoctrineInfra\Type\AttributeValueIdType::class,
+ ];
+ public const DOCTRINE_REPOSITORY_MAPPING = [
+ Entity\Attribute::class => DoctrineInfra\Repository\AttributeRepository::class,
+ ];
+ public const CONSOLE_COMMAND_MAPPING = [];
+ private const COMMAND_MAPPING = [
+ Entity\Attribute::class => [
+ Command\CreateAttributeCommand::class => true,
+ Command\DeleteAttributeCommand::class => true,
+ ],
+ ];
+
+ private static $packageDir;
+
+ public static function getPackageDir(): string
+ {
+ return self::$packageDir ?? (self::$packageDir = \dirname((string) (new \ReflectionClass(AttributeIdInterface::class))->getFileName()));
+ }
public function getConfigTreeBuilder(): TreeBuilder
{
diff --git a/src/EavBundle/DependencyInjection/Extension.php b/src/EavBundle/DependencyInjection/Extension.php
index 19f0ddef..e7d5ce4e 100644
--- a/src/EavBundle/DependencyInjection/Extension.php
+++ b/src/EavBundle/DependencyInjection/Extension.php
@@ -4,14 +4,11 @@
namespace MsgPhp\EavBundle\DependencyInjection;
-use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
-use Doctrine\ORM\Version as DoctrineOrmVersion;
-use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
-use MsgPhp\Eav\{AttributeIdInterface, AttributeValueIdInterface, Entity};
-use MsgPhp\Eav\Infra\Doctrine as DoctrineInfra;
+use MsgPhp\Domain\Infra\DependencyInjection\{ExtensionHelper, FeatureDetection};
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension as BaseExtension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -22,7 +19,7 @@
*
* @internal
*/
-final class Extension extends BaseExtension implements PrependExtensionInterface
+final class Extension extends BaseExtension implements PrependExtensionInterface, CompilerPassInterface
{
public const ALIAS = 'msgphp_eav';
@@ -38,16 +35,20 @@ public function getConfiguration(array $config, ContainerBuilder $container): Co
public function load(array $configs, ContainerBuilder $container): void
{
- $loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
+ $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
$config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);
- ContainerHelper::configureIdentityMapping($container, $config['class_mapping'], Configuration::IDENTITY_MAPPING);
- ContainerHelper::configureEntityFactory($container, $config['class_mapping'], Configuration::AGGREGATE_ROOTS);
- ContainerHelper::configureDoctrineOrmMapping($container, self::getDoctrineMappingFiles($config, $container), [DoctrineInfra\EntityFieldsMapping::class]);
+ ExtensionHelper::configureDomain($container, $config['class_mapping'], Configuration::AGGREGATE_ROOTS, Configuration::IDENTITY_MAPPING);
+
+ // message infra
+ $loader->load('message.php');
+ ExtensionHelper::finalizeCommandHandlers($container, $config['class_mapping'], $config['commands'], array_map(function (string $file): string {
+ return 'MsgPhp\\Eav\\Event\\'.basename($file, '.php');
+ }, glob(Configuration::getPackageDir().'/Event/*Event.php')));
// persistence infra
- if (class_exists(DoctrineOrmVersion::class) && ContainerHelper::hasBundle($container, DoctrineBundle::class)) {
- $this->prepareDoctrineOrm($config, $loader, $container);
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ $this->loadDoctrineOrm($config, $loader, $container);
}
}
@@ -55,26 +56,32 @@ public function prepend(ContainerBuilder $container): void
{
$config = $this->processConfiguration($this->getConfiguration($configs = $container->getExtensionConfig($this->getAlias()), $container), $configs);
- ContainerHelper::configureDoctrineTypes($container, $config['class_mapping'], $config['id_type_mapping'], [
- AttributeIdInterface::class => DoctrineInfra\Type\AttributeIdType::class,
- AttributeValueIdInterface::class => DoctrineInfra\Type\AttributeValueIdType::class,
- ]);
- ContainerHelper::configureDoctrineOrmTargetEntities($container, $config['class_mapping']);
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ ExtensionHelper::configureDoctrineOrm(
+ $container,
+ $config['class_mapping'],
+ $config['id_type_mapping'],
+ Configuration::DOCTRINE_TYPE_MAPPING,
+ self::getDoctrineMappingFiles($config, $container)
+ );
+ }
}
- private function prepareDoctrineOrm(array $config, LoaderInterface $loader, ContainerBuilder $container): void
+ public function process(ContainerBuilder $container): void
{
- $loader->load('doctrine.php');
-
- ContainerHelper::configureDoctrineOrmRepositories($container, $config['class_mapping'], [
- DoctrineInfra\Repository\AttributeRepository::class => Entity\Attribute::class,
- ]);
}
private static function getDoctrineMappingFiles(array $config, ContainerBuilder $container): array
{
- $baseDir = dirname(ContainerHelper::getClassReflection($container, AttributeIdInterface::class)->getFileName()).'/Infra/Doctrine/Resources/dist-mapping';
+ $baseDir = Configuration::getPackageDir().'/Infra/Doctrine/Resources/dist-mapping';
return glob($baseDir.'/*.orm.xml');
}
+
+ private function loadDoctrineOrm(array $config, LoaderInterface $loader, ContainerBuilder $container): void
+ {
+ $loader->load('doctrine.php');
+
+ ExtensionHelper::finalizeDoctrineOrmRepositories($container, $config['class_mapping'], Configuration::DOCTRINE_REPOSITORY_MAPPING);
+ }
}
diff --git a/src/EavBundle/MsgPhpEavBundle.php b/src/EavBundle/MsgPhpEavBundle.php
index f11141ae..fb39e471 100644
--- a/src/EavBundle/MsgPhpEavBundle.php
+++ b/src/EavBundle/MsgPhpEavBundle.php
@@ -5,7 +5,9 @@
namespace MsgPhp\EavBundle;
use MsgPhp\Domain\Infra\DependencyInjection\BundleHelper;
+use MsgPhp\EavBundle\DependencyInjection\Compiler\CleanupPass;
use MsgPhp\EavBundle\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -17,12 +19,14 @@ final class MsgPhpEavBundle extends Bundle
{
public function boot(): void
{
- BundleHelper::initDoctrineTypes($this->container);
+ BundleHelper::boot($this->container);
}
public function build(ContainerBuilder $container): void
{
- BundleHelper::initDomain($container);
+ $container->addCompilerPass(new CleanupPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
+
+ BundleHelper::build($container);
}
public function getContainerExtension(): ?ExtensionInterface
diff --git a/src/EavBundle/README.md b/src/EavBundle/README.md
index 94ac3f67..05e8deaa 100644
--- a/src/EavBundle/README.md
+++ b/src/EavBundle/README.md
@@ -1,4 +1,4 @@
-# Entity-Attribute-Value bundle
+# Entity-Attribute-Value Bundle
A Symfony bundle for basic [EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) management.
@@ -17,14 +17,15 @@ composer require msgphp/eav-bundle
## Features
- Symfony 3.4 / 4.0 ready
+- Symfony messenger commands & events
- Doctrine persistence
-- Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null`
+- Default attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null`
## Configuration
```php
getFileName());
- $services = $container->services()
+return function (ContainerConfigurator $container): void {
+ $container->services()
->defaults()
->autowire()
->private()
- ->load($ns = 'MsgPhp\\Eav\\Infra\\Doctrine\\Repository\\', $repositories = $baseDir.'/Infra/Doctrine/Repository/*Repository.php')
- ;
+ ->load('MsgPhp\\Eav\\Infra\\Doctrine\\Repository\\', Configuration::getPackageDir().'/Infra/Doctrine/Repository/*Repository.php')
+ ->bind(EntityManagerInterface::class, ref('msgphp.doctrine.entity_manager'))
- foreach (glob($repositories) as $file) {
- foreach ($reflector($repository = $ns.basename($file, '.php'))->getInterfaceNames() as $interface) {
- $services->alias($interface, $repository);
- }
- }
+ ->set(Doctrine\ObjectFieldMappings::class)
+ ->tag('msgphp.doctrine.object_field_mappings', ['priority' => -100])
+ ;
};
diff --git a/src/EavBundle/Resources/config/message.php b/src/EavBundle/Resources/config/message.php
new file mode 100644
index 00000000..f94385fb
--- /dev/null
+++ b/src/EavBundle/Resources/config/message.php
@@ -0,0 +1,17 @@
+services()
+ ->defaults()
+ ->autowire()
+ ->private()
+
+ ->load('MsgPhp\\Eav\\Command\\Handler\\', Configuration::getPackageDir().'/Command/Handler/*Handler.php')
+ ->tag('msgphp.domain.command_handler')
+ ;
+};
diff --git a/src/EavBundle/composer.json b/src/EavBundle/composer.json
index ecc997a4..ae0725fe 100644
--- a/src/EavBundle/composer.json
+++ b/src/EavBundle/composer.json
@@ -2,7 +2,7 @@
"name": "msgphp/eav-bundle",
"type": "symfony-bundle",
"license": "MIT",
- "description": "Entity-Attribute-Value domain layer",
+ "description": "Basic entity-attribute-value management (the `EAV` domain)",
"keywords": ["entity-attribute-value", "eav"],
"authors": [
{
@@ -18,7 +18,7 @@
"exclude-from-classmap": ["/Tests/"]
},
"require": {
- "msgphp/eav": "^0.2|^1.0",
+ "msgphp/eav": "^0.8",
"symfony/config": "^3.4|^4.0",
"symfony/dependency-injection": "^3.4|^4.0",
"symfony/http-kernel": "^3.4|^4.0"
@@ -26,7 +26,7 @@
"require-dev": {
"doctrine/doctrine-bundle": "^1.8",
"doctrine/orm": "^2.6",
- "symfony/phpunit-bridge": "^3.4|^4.0",
+ "symfony/phpunit-bridge": "^3.4.9|^4.0.9",
"symfony/var-dumper": "^3.4|^4.0"
},
"config": {
@@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "0.8-dev"
}
}
}
diff --git a/src/User/Command/ChangeUserAttributeValueCommand.php b/src/User/Command/ChangeUserAttributeValueCommand.php
index 44d59002..a90224af 100644
--- a/src/User/Command/ChangeUserAttributeValueCommand.php
+++ b/src/User/Command/ChangeUserAttributeValueCommand.php
@@ -9,12 +9,12 @@
*/
class ChangeUserAttributeValueCommand
{
- public $attributeValueId;
+ public $id;
public $value;
- final public function __construct($attributeValueId, $value)
+ final public function __construct($id, $value)
{
- $this->attributeValueId = $attributeValueId;
+ $this->id = $id;
$this->value = $value;
}
}
diff --git a/src/User/Command/CreateRoleCommand.php b/src/User/Command/CreateRoleCommand.php
new file mode 100644
index 00000000..8de9be89
--- /dev/null
+++ b/src/User/Command/CreateRoleCommand.php
@@ -0,0 +1,18 @@
+
+ */
+class CreateRoleCommand
+{
+ public $context;
+
+ final public function __construct(array $context)
+ {
+ $this->context = $context;
+ }
+}
diff --git a/src/User/Command/DeleteRoleCommand.php b/src/User/Command/DeleteRoleCommand.php
new file mode 100644
index 00000000..846c6405
--- /dev/null
+++ b/src/User/Command/DeleteRoleCommand.php
@@ -0,0 +1,18 @@
+
+ */
+class DeleteRoleCommand
+{
+ public $roleName;
+
+ final public function __construct(string $roleName)
+ {
+ $this->roleName = $roleName;
+ }
+}
diff --git a/src/User/Command/DeleteUserAttributeValueCommand.php b/src/User/Command/DeleteUserAttributeValueCommand.php
index 93539aa7..f5151951 100644
--- a/src/User/Command/DeleteUserAttributeValueCommand.php
+++ b/src/User/Command/DeleteUserAttributeValueCommand.php
@@ -9,10 +9,10 @@
*/
class DeleteUserAttributeValueCommand
{
- public $attributeValueId;
+ public $id;
- final public function __construct($attributeValueId)
+ final public function __construct($id)
{
- $this->attributeValueId = $attributeValueId;
+ $this->id = $id;
}
}
diff --git a/src/User/Command/Handler/AddUserAttributeValueHandler.php b/src/User/Command/Handler/AddUserAttributeValueHandler.php
index 91d0e360..49f7d18a 100644
--- a/src/User/Command/Handler/AddUserAttributeValueHandler.php
+++ b/src/User/Command/Handler/AddUserAttributeValueHandler.php
@@ -31,10 +31,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB
public function __invoke(AddUserAttributeValueCommand $command): void
{
$attributeValueContext = $command->context['attributeValue'] ?? $command->context['attribute_value'] ?? [];
- if (!isset($attributeValueContext['id'])) {
- $attributeValueContext['id'] = $this->factory->nextIdentifier(AttributeValue::class);
- }
-
+ $attributeValueContext += ['id' => $this->factory->nextIdentifier(AttributeValue::class)];
$userAttributeValue = $this->factory->create(UserAttributeValue::class, [
'user' => $this->factory->reference(User::class, $this->factory->identify(User::class, $command->userId)),
'attributeValue' => [
diff --git a/src/User/Command/Handler/ChangeUserAttributeValueHandler.php b/src/User/Command/Handler/ChangeUserAttributeValueHandler.php
index 88296899..65b322a6 100644
--- a/src/User/Command/Handler/ChangeUserAttributeValueHandler.php
+++ b/src/User/Command/Handler/ChangeUserAttributeValueHandler.php
@@ -29,13 +29,14 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB
public function __invoke(ChangeUserAttributeValueCommand $command): void
{
- $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->attributeValueId));
+ // @fixme configure factory to handle `UserAttributeValue::class` as well
+ $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->id));
if ($command->value === $oldValue = $userAttributeValue->getValue()) {
return;
}
- $userAttributeValue->getAttributeValue()->changeValue($command->value);
+ $userAttributeValue->changeValue($command->value);
$this->repository->save($userAttributeValue);
$this->dispatch(UserAttributeValueChangedEvent::class, [$userAttributeValue, $oldValue, $command->value]);
diff --git a/src/User/Command/Handler/CreateRoleHandler.php b/src/User/Command/Handler/CreateRoleHandler.php
new file mode 100644
index 00000000..3c561589
--- /dev/null
+++ b/src/User/Command/Handler/CreateRoleHandler.php
@@ -0,0 +1,36 @@
+
+ */
+final class CreateRoleHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, RoleRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(CreateRoleCommand $command): void
+ {
+ $role = $this->factory->create(Role::class, $command->context);
+ $this->repository->save($role);
+ $this->dispatch(RoleCreatedEvent::class, [$role]);
+ }
+}
diff --git a/src/User/Command/Handler/CreateUserHandler.php b/src/User/Command/Handler/CreateUserHandler.php
index 5f55a320..54bc051e 100644
--- a/src/User/Command/Handler/CreateUserHandler.php
+++ b/src/User/Command/Handler/CreateUserHandler.php
@@ -29,11 +29,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB
public function __invoke(CreateUserCommand $command): void
{
- $context = $command->context;
- if (!isset($context['id'])) {
- $context['id'] = $this->factory->nextIdentifier(User::class);
- }
- $user = $this->factory->create(User::class, $context);
+ $user = $this->factory->create(User::class, $command->context + ['id' => $this->factory->nextIdentifier(User::class)]);
$this->repository->save($user);
$this->dispatch(UserCreatedEvent::class, [$user]);
diff --git a/src/User/Command/Handler/DeleteRoleHandler.php b/src/User/Command/Handler/DeleteRoleHandler.php
new file mode 100644
index 00000000..48dd27dc
--- /dev/null
+++ b/src/User/Command/Handler/DeleteRoleHandler.php
@@ -0,0 +1,41 @@
+
+ */
+final class DeleteRoleHandler
+{
+ use MessageDispatchingTrait;
+
+ private $repository;
+
+ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, RoleRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+ }
+
+ public function __invoke(DeleteRoleCommand $command): void
+ {
+ try {
+ $role = $this->repository->find($command->roleName);
+ } catch (EntityNotFoundException $e) {
+ return;
+ }
+
+ $this->repository->delete($role);
+ $this->dispatch(RoleDeletedEvent::class, [$role]);
+ }
+}
diff --git a/src/User/Command/Handler/DeleteUserAttributeValueHandler.php b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php
index dfdd6240..b77def00 100644
--- a/src/User/Command/Handler/DeleteUserAttributeValueHandler.php
+++ b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php
@@ -31,7 +31,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB
public function __invoke(DeleteUserAttributeValueCommand $command): void
{
try {
- $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->attributeValueId));
+ $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->id));
} catch (EntityNotFoundException $e) {
return;
}
diff --git a/src/User/Entity/Credential/Anonymous.php b/src/User/Entity/Credential/Anonymous.php
index de67e387..0f8d7da4 100644
--- a/src/User/Entity/Credential/Anonymous.php
+++ b/src/User/Entity/Credential/Anonymous.php
@@ -18,6 +18,6 @@ public static function getUsernameField(): string
public function getUsername(): string
{
- return 'anonymous_'.bin2hex(random_bytes(8));
+ throw new \LogicException('An anonymous credential has no username.');
}
}
diff --git a/src/User/Entity/Features/AbstractCredential.php b/src/User/Entity/Features/AbstractCredential.php
index 0dae3abe..2da28478 100644
--- a/src/User/Entity/Features/AbstractCredential.php
+++ b/src/User/Entity/Features/AbstractCredential.php
@@ -30,11 +30,11 @@ private function handleChangeCredentialEvent(ChangeCredentialEvent $event): bool
$readIsProp = $writeIsProp = false;
if (!method_exists($this, $writeMethod = 'change'.($ucField = ucfirst($field))) && !method_exists($this, $writeMethod = 'set'.$ucField) && !($writeIsProp = property_exists($this, $field))) {
- throw new \LogicException(sprintf('Unable to handle event "%s", method "%s::change/set%s()" nor property "$%s" does not exists.', get_class($event), get_class($this), $ucField, $field));
+ throw new \LogicException(sprintf('Unable to handle event "%s", method "%s::change/set%s()" nor property "$%s" does not exists.', \get_class($event), \get_class($this), $ucField, $field));
}
if (!method_exists($this, $readMethod = 'get'.$ucField) && !method_exists($this, $readMethod = 'is'.$ucField) && !method_exists($this, $readMethod = 'has'.$ucField) && !($readIsProp = property_exists($this, $field))) {
- throw new \LogicException(sprintf('Unable to handle event "%s", method "%s::get/is/has%s()" nor property "$%s" does not exists.', get_class($event), get_class($this), $ucField, $field));
+ throw new \LogicException(sprintf('Unable to handle event "%s", method "%s::get/is/has%s()" nor property "$%s" does not exists.', \get_class($event), \get_class($this), $ucField, $field));
}
if ($value !== ($readIsProp ? $this->$field : $this->$readMethod())) {
diff --git a/src/User/Entity/Fields/AttributeValuesField.php b/src/User/Entity/Fields/AttributeValuesField.php
index eeccb6cd..fec36403 100644
--- a/src/User/Entity/Fields/AttributeValuesField.php
+++ b/src/User/Entity/Fields/AttributeValuesField.php
@@ -16,7 +16,7 @@ trait AttributeValuesField
{
use AttributesField;
- /** @var UserAttributeValue[] */
+ /** @var iterable|UserAttributeValue[] */
private $attributeValues = [];
/**
diff --git a/src/User/Entity/Fields/EmailsField.php b/src/User/Entity/Fields/EmailsField.php
index 187f5e81..4bc70452 100644
--- a/src/User/Entity/Fields/EmailsField.php
+++ b/src/User/Entity/Fields/EmailsField.php
@@ -13,7 +13,7 @@
*/
trait EmailsField
{
- /** @var UserEmail[] */
+ /** @var iterable|UserEmail[] */
private $emails = [];
/**
diff --git a/src/User/Entity/Fields/RolesField.php b/src/User/Entity/Fields/RolesField.php
index 855d85ff..7bfe709d 100644
--- a/src/User/Entity/Fields/RolesField.php
+++ b/src/User/Entity/Fields/RolesField.php
@@ -13,8 +13,8 @@
*/
trait RolesField
{
- /** @var UserRole[] */
- private $roles;
+ /** @var iterable|UserRole[] */
+ private $roles = [];
/**
* @return DomainCollectionInterface|UserRole[]
diff --git a/src/User/Entity/UserAttributeValue.php b/src/User/Entity/UserAttributeValue.php
index 6de0dc5e..46250271 100644
--- a/src/User/Entity/UserAttributeValue.php
+++ b/src/User/Entity/UserAttributeValue.php
@@ -5,7 +5,7 @@
namespace MsgPhp\User\Entity;
use MsgPhp\Eav\Entity\AttributeValue;
-use MsgPhp\Eav\Entity\Fields\AttributeValueField;
+use MsgPhp\Eav\Entity\Features\EntityAttributeValue;
use MsgPhp\User\Entity\Fields\UserField;
/**
@@ -14,7 +14,7 @@
abstract class UserAttributeValue
{
use UserField;
- use AttributeValueField;
+ use EntityAttributeValue;
public function __construct(User $user, AttributeValue $attributeValue)
{
diff --git a/src/User/Entity/Username.php b/src/User/Entity/Username.php
index 4006567f..94d2a8e4 100644
--- a/src/User/Entity/Username.php
+++ b/src/User/Entity/Username.php
@@ -8,10 +8,8 @@
/**
* @author Roland Franssen
- *
- * @final
*/
-class Username
+abstract class Username
{
use UserField;
diff --git a/src/User/Event/RoleCreatedEvent.php b/src/User/Event/RoleCreatedEvent.php
new file mode 100644
index 00000000..8f2de53a
--- /dev/null
+++ b/src/User/Event/RoleCreatedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class RoleCreatedEvent
+{
+ public $role;
+
+ final public function __construct(Role $role)
+ {
+ $this->role = $role;
+ }
+}
diff --git a/src/User/Event/RoleDeletedEvent.php b/src/User/Event/RoleDeletedEvent.php
new file mode 100644
index 00000000..b47c6167
--- /dev/null
+++ b/src/User/Event/RoleDeletedEvent.php
@@ -0,0 +1,20 @@
+
+ */
+class RoleDeletedEvent
+{
+ public $role;
+
+ final public function __construct(Role $role)
+ {
+ $this->role = $role;
+ }
+}
diff --git a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php
index d52fa574..5b7ce1ed 100644
--- a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php
+++ b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php
@@ -38,13 +38,18 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB
public function onMessageReceived($message): void
{
if ($message instanceof UserCredentialChangedEvent) {
- $messages = ['Changed user credential for '.$message->user->getCredential()->getUsername()];
+ $this->io->success('Changed user credential for '.$message->user->getCredential()->getUsername());
+
+ $rows = [];
foreach (array_diff((array) $message->newCredential, $oldValues = (array) $message->oldCredential) as $key => $value) {
$field = false === ($i = strrpos($key, "\00")) ? $key : substr($key, $i + 1);
- $messages[] = sprintf('> Field "%s" changed from %s to %s', $field, json_encode($oldValues[$key] ?? null), json_encode($value));
+ // @todo use VarDumper
+ $rows[] = [$field, json_encode($oldValues[$key] ?? null), json_encode($value)];
}
- $this->io->success($messages);
+ if ($rows) {
+ $this->io->table(['Field', 'Old Value', 'New Value'], $rows);
+ }
}
}
diff --git a/src/User/Infra/Console/Command/CreateRoleCommand.php b/src/User/Infra/Console/Command/CreateRoleCommand.php
new file mode 100644
index 00000000..c5828700
--- /dev/null
+++ b/src/User/Infra/Console/Command/CreateRoleCommand.php
@@ -0,0 +1,66 @@
+
+ */
+final class CreateRoleCommand extends Command implements MessageReceivingInterface
+{
+ use MessageDispatchingTrait;
+
+ protected static $defaultName = 'role:create';
+
+ private $contextFactory;
+
+ /** @var StyleInterface */
+ private $io;
+
+ public function __construct(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus, ContextFactoryInterface $contextFactory)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->contextFactory = $contextFactory;
+
+ parent::__construct();
+ }
+
+ /**
+ * @internal
+ */
+ public function onMessageReceived($message): void
+ {
+ if ($message instanceof RoleCreatedEvent) {
+ $this->io->success('Created role '.$message->role->getName());
+ }
+ }
+
+ protected function configure(): void
+ {
+ $this->setDescription('Create a role');
+ $this->contextFactory->configure($this->getDefinition());
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io = new SymfonyStyle($input, $output);
+ $context = $this->contextFactory->getContext($input, $this->io);
+
+ $this->dispatch(CreateRoleDomainCommand::class, [$context]);
+
+ return 0;
+ }
+}
diff --git a/src/User/Infra/Console/Command/DeleteRoleCommand.php b/src/User/Infra/Console/Command/DeleteRoleCommand.php
new file mode 100644
index 00000000..1317daaa
--- /dev/null
+++ b/src/User/Infra/Console/Command/DeleteRoleCommand.php
@@ -0,0 +1,55 @@
+
+ */
+final class DeleteRoleCommand extends RoleCommand
+{
+ protected static $defaultName = 'role:delete';
+
+ /** @var StyleInterface */
+ private $io;
+
+ public function onMessageReceived($message): void
+ {
+ if ($message instanceof RoleDeletedEvent) {
+ $this->io->success('Deleted role '.$message->role->getName());
+ }
+ }
+
+ protected function configure(): void
+ {
+ parent::configure();
+
+ $this->setDescription('Delete a role');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io = new SymfonyStyle($input, $output);
+ $role = $this->getRole($input, $this->io);
+
+ if ($input->isInteractive()) {
+ $this->io->note('Deleting role '.$role->getName());
+
+ if (!$this->io->confirm('Are you sure?')) {
+ return 0;
+ }
+ }
+
+ $this->dispatch(DeleteRoleDomainCommand::class, [$role->getName()]);
+
+ return 0;
+ }
+}
diff --git a/src/User/Infra/Console/Command/RoleAwareTrait.php b/src/User/Infra/Console/Command/RoleAwareTrait.php
new file mode 100644
index 00000000..a63b656c
--- /dev/null
+++ b/src/User/Infra/Console/Command/RoleAwareTrait.php
@@ -0,0 +1,34 @@
+
+ */
+trait RoleAwareTrait
+{
+ /** @var RoleRepositoryInterface */
+ private $repository;
+
+ protected function getRole(InputInterface $input, StyleInterface $io): Role
+ {
+ if (null === $value = $input->getArgument('role')) {
+ if (!$input->isInteractive()) {
+ throw new \LogicException('No value provided for "role".');
+ }
+
+ do {
+ $value = $io->ask('Role name');
+ } while (null === $value);
+ }
+
+ return $this->repository->find($value);
+ }
+}
diff --git a/src/User/Infra/Console/Command/RoleCommand.php b/src/User/Infra/Console/Command/RoleCommand.php
new file mode 100644
index 00000000..144c491f
--- /dev/null
+++ b/src/User/Infra/Console/Command/RoleCommand.php
@@ -0,0 +1,45 @@
+
+ */
+abstract class RoleCommand extends Command implements MessageReceivingInterface
+{
+ use RoleAwareTrait;
+ use MessageDispatchingTrait {
+ dispatch as protected;
+ }
+
+ private $repository;
+
+ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, RoleRepositoryInterface $repository)
+ {
+ $this->factory = $factory;
+ $this->bus = $bus;
+ $this->repository = $repository;
+
+ parent::__construct();
+ }
+
+ /**
+ * @internal
+ */
+ public function onMessageReceived($message): void
+ {
+ }
+
+ protected function configure(): void
+ {
+ $this->addArgument('role', InputArgument::OPTIONAL, 'The role name');
+ }
+}
diff --git a/src/User/Infra/Console/Command/SynchronizeUsernamesCommand.php b/src/User/Infra/Console/Command/SynchronizeUsernamesCommand.php
index 83fad18c..5c9a9913 100644
--- a/src/User/Infra/Console/Command/SynchronizeUsernamesCommand.php
+++ b/src/User/Infra/Console/Command/SynchronizeUsernamesCommand.php
@@ -17,7 +17,7 @@
*/
final class SynchronizeUsernamesCommand extends Command
{
- protected static $defaultName = 'user:synchronize:usernames';
+ protected static $defaultName = 'user:synchronize-usernames';
private $repository;
diff --git a/src/User/Infra/Console/Command/UserRoleCommand.php b/src/User/Infra/Console/Command/UserRoleCommand.php
index d71017a5..012a2107 100644
--- a/src/User/Infra/Console/Command/UserRoleCommand.php
+++ b/src/User/Infra/Console/Command/UserRoleCommand.php
@@ -6,18 +6,15 @@
use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
use MsgPhp\Domain\Message\DomainMessageBusInterface;
-use MsgPhp\User\Entity\Role;
use MsgPhp\User\Repository\{RoleRepositoryInterface, UserRepositoryInterface};
use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Style\StyleInterface;
/**
* @author Roland Franssen
*/
abstract class UserRoleCommand extends UserCommand
{
- private $repository;
+ use RoleAwareTrait;
public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $userRepository, RoleRepositoryInterface $roleRepository)
{
@@ -32,19 +29,4 @@ protected function configure(): void
$this->addArgument('role', InputArgument::OPTIONAL, 'The role name');
}
-
- protected function getRole(InputInterface $input, StyleInterface $io): Role
- {
- if (null === $value = $input->getArgument('role')) {
- if (!$input->isInteractive()) {
- throw new \LogicException('No value provided for "role".');
- }
-
- do {
- $value = $io->ask('Role name');
- } while (null === $value);
- }
-
- return $this->repository->find($value);
- }
}
diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php
deleted file mode 100644
index a4129f49..00000000
--- a/src/User/Infra/Doctrine/EntityFieldsMapping.php
+++ /dev/null
@@ -1,93 +0,0 @@
-
- *
- * @internal
- */
-final class EntityFieldsMapping implements ObjectFieldMappingProviderInterface
-{
- private const CREDENTIALS = [
- Features\EmailCredential::class => Credential\Email::class,
- Features\EmailPasswordCredential::class => Credential\EmailPassword::class,
- Features\EmailSaltedPasswordCredential::class => Credential\EmailSaltedPassword::class,
- Features\NicknameCredential::class => Credential\Nickname::class,
- Features\NicknamePasswordCredential::class => Credential\NicknamePassword::class,
- Features\NicknameSaltedPasswordCredential::class => Credential\NicknameSaltedPassword::class,
- Features\TokenCredential::class => Credential\Token::class,
- ];
-
- public static function getObjectFieldMapping(): array
- {
- $credentials = self::CREDENTIALS;
- array_walk($credentials, function (string &$credential): void {
- $credential = ['credential' => [
- 'type' => self::TYPE_EMBEDDED,
- 'class' => $credential,
- 'columnPrefix' => null,
- ]];
- unset($credential);
- });
-
- return $credentials + [
- Features\ResettablePassword::class => [
- 'passwordResetToken' => [
- 'type' => 'string',
- 'unique' => true,
- 'nullable' => true,
- ],
- 'passwordRequestedAt' => [
- 'type' => 'datetime',
- 'nullable' => true,
- ],
- ],
- Fields\AttributeValuesField::class => [
- 'attributeValues' => [
- 'type' => self::TYPE_ONE_TO_MANY,
- 'targetEntity' => UserAttributeValue::class,
- 'mappedBy' => 'user',
- ],
- ],
- Fields\EmailsField::class => [
- 'emails' => [
- 'type' => self::TYPE_ONE_TO_MANY,
- 'targetEntity' => UserEmail::class,
- 'mappedBy' => 'user',
- 'indexBy' => 'email',
- ],
- ],
- Fields\RoleField::class => [
- 'role' => [
- 'type' => self::TYPE_MANY_TO_ONE,
- 'targetEntity' => Role::class,
- 'joinColumns' => [
- ['referencedColumnName' => 'name', 'nullable' => false],
- ],
- ],
- ],
- Fields\RolesField::class => [
- 'roles' => [
- 'type' => self::TYPE_ONE_TO_MANY,
- 'targetEntity' => UserRole::class,
- 'mappedBy' => 'user',
- ],
- ],
- Fields\UserField::class => [
- 'user' => [
- 'type' => self::TYPE_MANY_TO_ONE,
- 'targetEntity' => User::class,
- 'joinColumns' => [
- ['nullable' => false],
- ],
- ],
- ],
- ];
- }
-}
diff --git a/src/User/Infra/Doctrine/Event/UsernameListener.php b/src/User/Infra/Doctrine/Event/UsernameListener.php
index 827b3b6d..c3cb679d 100644
--- a/src/User/Infra/Doctrine/Event/UsernameListener.php
+++ b/src/User/Infra/Doctrine/Event/UsernameListener.php
@@ -10,30 +10,40 @@
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
-use MsgPhp\User\Entity\Username;
+use Doctrine\ORM\Mapping\MappingException;
+use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
+use MsgPhp\User\Entity\{User, Username};
/**
* @author Roland Franssen
*/
final class UsernameListener
{
- private $mapping;
+ private $factory;
+ private $targetMappings;
private $updateUsernames = [];
- public function __construct(array $mapping)
+ public function __construct(EntityAwareFactoryInterface $factory, array $targetMappings)
{
- $this->mapping = $mapping;
+ $this->factory = $factory;
+ $this->targetMappings = $targetMappings;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
{
- if (!isset($this->mapping[($metadata = $event->getClassMetadata())->getName()])) {
+ $metadata = $event->getClassMetadata();
+
+ if (!isset($this->targetMappings[$metadata->getName()])) {
return;
}
- $metadata->addEntityListener('prePersist', self::class, 'add');
- $metadata->addEntityListener('preUpdate', self::class, 'update');
- $metadata->addEntityListener('preRemove', self::class, 'remove');
+ try {
+ $metadata->addEntityListener('prePersist', self::class, 'add');
+ $metadata->addEntityListener('preUpdate', self::class, 'update');
+ $metadata->addEntityListener('preRemove', self::class, 'remove');
+ } catch (MappingException $e) {
+ // duplicate
+ }
}
/**
@@ -53,12 +63,24 @@ public function add($entity, LifecycleEventArgs $event): void
*/
public function update($entity, PreUpdateEventArgs $event): void
{
- foreach ($this->getMapping($entity, $event->getEntityManager()) as $mapping) {
- if (!$event->hasChangedField($mapping['field'])) {
+ $em = $event->getEntityManager();
+
+ foreach ($this->getTargetMapping($entity, $event->getEntityManager()) as $field => $mappedBy) {
+ if (!$event->hasChangedField($field)) {
continue;
}
- $this->updateUsernames[$event->getOldValue($mapping['field'])] = $event->getNewValue($mapping['field']);
+ if (null !== $username = $event->getOldValue($field)) {
+ $this->updateUsernames[$username] = $event->getNewValue($field);
+ } elseif (null !== $username = $event->getNewValue($field)) {
+ $user = null === $mappedBy ? $entity : $em->getClassMetadata(\get_class($entity))->getFieldValue($entity, $mappedBy);
+
+ if (null === $user) {
+ continue;
+ }
+
+ $em->persist($this->createUsername($user, $username));
+ }
}
}
@@ -68,14 +90,14 @@ public function update($entity, PreUpdateEventArgs $event): void
public function remove($entity, LifecycleEventArgs $event): void
{
$em = $event->getEntityManager();
- $metadata = $em->getClassMetadata(get_class($entity));
+ $metadata = $em->getClassMetadata(\get_class($entity));
- foreach ($this->getMapping($entity, $em) as $mapping) {
- if (!isset($mapping['mapped_by'])) {
+ foreach (array_keys($this->getTargetMapping($entity, $em)) as $field) {
+ if (null === $username = $metadata->getFieldValue($entity, $field)) {
continue;
}
- $this->updateUsernames[$metadata->getFieldValue($entity, $mapping['field'])] = null;
+ $this->updateUsernames[$username] = null;
}
}
@@ -87,14 +109,18 @@ public function postFlush(PostFlushEventArgs $event): void
$em = $event->getEntityManager();
- /** @var Username[] $usernames */
- $usernames = $em->getRepository(Username::class)->findBy(['username' => array_keys($this->updateUsernames)]);
+ $qb = $em->createQueryBuilder();
+ $qb->select('u');
+ $qb->from(Username::class, 'u');
+ $qb->where($qb->expr()->in('u.username', ':usernames'));
+ $qb->setParameter('usernames', array_keys($this->updateUsernames));
- foreach ($usernames as $username) {
+ foreach ($qb->getQuery()->getResult() as $username) {
+ /* @var Username $username */
$em->remove($username);
if (isset($this->updateUsernames[$usernameValue = (string) $username])) {
- $em->persist(new Username($username->getUser(), $this->updateUsernames[$usernameValue]));
+ $em->persist($this->createUsername($username->getUser(), $this->updateUsernames[$usernameValue]));
}
}
@@ -110,24 +136,33 @@ public function postFlush(PostFlushEventArgs $event): void
*/
private function getUsernames($entity, EntityManagerInterface $em): iterable
{
- $metadata = $em->getClassMetadata(get_class($entity));
+ $metadata = $em->getClassMetadata(\get_class($entity));
- foreach ($this->getMapping($entity, $em) as $mapping) {
- $user = isset($mapping['mapped_by']) ? $metadata->getFieldValue($entity, $mapping['mapped_by']) : $entity;
+ foreach ($this->getTargetMapping($entity, $em) as $field => $mappedBy) {
+ $user = null === $mappedBy ? $entity : $metadata->getFieldValue($entity, $mappedBy);
- yield new Username($user, $metadata->getFieldValue($entity, $mapping['field']));
+ if (null === $user || null === $username = $metadata->getFieldValue($entity, $field)) {
+ continue;
+ }
+
+ yield $this->createUsername($user, $username);
}
}
- private function getMapping($entity, EntityManagerInterface $em): array
+ private function createUsername(User $user, string $username): Username
+ {
+ return $this->factory->create(Username::class, [$user, $username]);
+ }
+
+ private function getTargetMapping($entity, EntityManagerInterface $em): array
{
- if (isset($this->mapping[$class = ClassUtils::getClass($entity)])) {
- return $this->mapping[$class];
+ if (isset($this->targetMappings[$class = ClassUtils::getClass($entity)])) {
+ return $this->targetMappings[$class];
}
foreach ($em->getClassMetadata($class)->parentClasses as $parent) {
- if (isset($this->mapping[$parent])) {
- return $this->mapping[$parent];
+ if (isset($this->targetMappings[$parent])) {
+ return $this->targetMappings[$parent];
}
}
diff --git a/src/User/Infra/Doctrine/ObjectFieldMappings.php b/src/User/Infra/Doctrine/ObjectFieldMappings.php
new file mode 100644
index 00000000..ce2b6b7b
--- /dev/null
+++ b/src/User/Infra/Doctrine/ObjectFieldMappings.php
@@ -0,0 +1,91 @@
+
+ *
+ * @internal
+ */
+final class ObjectFieldMappings implements ObjectFieldMappingsProviderInterface
+{
+ private const CREDENTIALS = [
+ Features\EmailCredential::class => Credential\Email::class,
+ Features\EmailPasswordCredential::class => Credential\EmailPassword::class,
+ Features\EmailSaltedPasswordCredential::class => Credential\EmailSaltedPassword::class,
+ Features\NicknameCredential::class => Credential\Nickname::class,
+ Features\NicknamePasswordCredential::class => Credential\NicknamePassword::class,
+ Features\NicknameSaltedPasswordCredential::class => Credential\NicknameSaltedPassword::class,
+ Features\TokenCredential::class => Credential\Token::class,
+ ];
+
+ public static function provideObjectFieldMappings(): iterable
+ {
+ foreach (self::CREDENTIALS as $object => $credential) {
+ yield $object => [
+ 'credential' => [
+ 'type' => self::TYPE_EMBEDDED,
+ 'class' => $credential,
+ 'columnPrefix' => null,
+ ],
+ ];
+ }
+
+ yield Features\ResettablePassword::class => [
+ 'passwordResetToken' => [
+ 'type' => 'string',
+ 'unique' => true,
+ 'nullable' => true,
+ ],
+ 'passwordRequestedAt' => [
+ 'type' => 'datetime',
+ 'nullable' => true,
+ ],
+ ];
+ yield Fields\AttributeValuesField::class => [
+ 'attributeValues' => [
+ 'type' => self::TYPE_ONE_TO_MANY,
+ 'targetEntity' => UserAttributeValue::class,
+ 'mappedBy' => 'user',
+ ],
+ ];
+ yield Fields\EmailsField::class => [
+ 'emails' => [
+ 'type' => self::TYPE_ONE_TO_MANY,
+ 'targetEntity' => UserEmail::class,
+ 'mappedBy' => 'user',
+ 'indexBy' => 'email',
+ ],
+ ];
+ yield Fields\RoleField::class => [
+ 'role' => [
+ 'type' => self::TYPE_MANY_TO_ONE,
+ 'targetEntity' => Role::class,
+ 'joinColumns' => [
+ ['referencedColumnName' => 'name', 'nullable' => false],
+ ],
+ ],
+ ];
+ yield Fields\RolesField::class => [
+ 'roles' => [
+ 'type' => self::TYPE_ONE_TO_MANY,
+ 'targetEntity' => UserRole::class,
+ 'mappedBy' => 'user',
+ ],
+ ];
+ yield Fields\UserField::class => [
+ 'user' => [
+ 'type' => self::TYPE_MANY_TO_ONE,
+ 'targetEntity' => User::class,
+ 'joinColumns' => [
+ ['nullable' => false],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php b/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php
index d80b773e..efa84d23 100644
--- a/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php
+++ b/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php
@@ -5,22 +5,21 @@
namespace MsgPhp\User\Infra\Doctrine\Repository;
use MsgPhp\Domain\DomainCollectionInterface;
-use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait;
use MsgPhp\Eav\{AttributeIdInterface, AttributeValueIdInterface};
+use MsgPhp\Eav\Infra\Doctrine\Repository\EntityAttributeValueRepositoryTrait;
use MsgPhp\User\Entity\UserAttributeValue;
use MsgPhp\User\Repository\UserAttributeValueRepositoryInterface;
use MsgPhp\User\UserIdInterface;
-use Doctrine\ORM\Query\Expr\Join;
-use Doctrine\ORM\QueryBuilder;
/**
* @author Roland Franssen
*/
final class UserAttributeValueRepository implements UserAttributeValueRepositoryInterface
{
- use DomainEntityRepositoryTrait;
+ use EntityAttributeValueRepositoryTrait;
private $alias = 'user_attribute_value';
+ private $attributeValueField = 'attributeValue';
/**
* @return DomainCollectionInterface|UserAttributeValue[]
@@ -83,19 +82,4 @@ public function delete(UserAttributeValue $userAttributeValue): void
{
$this->doDelete($userAttributeValue);
}
-
- private function addAttributeCriteria(QueryBuilder $qb, AttributeIdInterface $attributeId, $value = null): void
- {
- if (3 === func_num_args()) {
- $param = $this->addFieldParameter($qb, 'attributeValue', md5(serialize($value)));
-
- $qb->join($this->alias.'.attributeValue', 'attribute_value', Join::WITH, 'attribute_value.checksum = '.$param);
- } else {
- $qb->join($this->alias.'.attributeValue', 'attribute_value');
- }
-
- $param = $this->addFieldParameter($qb, 'attribute', $attributeId);
-
- $qb->join('attribute_value.attribute', 'attribute', Join::WITH, 'attribute.id = '.$param);
- }
}
diff --git a/src/User/Infra/Doctrine/Repository/UserRepository.php b/src/User/Infra/Doctrine/Repository/UserRepository.php
index 8a82e395..b7c7f7a3 100644
--- a/src/User/Infra/Doctrine/Repository/UserRepository.php
+++ b/src/User/Infra/Doctrine/Repository/UserRepository.php
@@ -11,7 +11,7 @@
use MsgPhp\Domain\{DomainCollectionInterface, DomainIdentityHelper};
use MsgPhp\Domain\Exception\EntityNotFoundException;
use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait;
-use MsgPhp\User\Entity\{User, Username};
+use MsgPhp\User\Entity\User;
use MsgPhp\User\Repository\UserRepositoryInterface;
use MsgPhp\User\UserIdInterface;
@@ -26,12 +26,14 @@ final class UserRepository implements UserRepositoryInterface
private $alias = 'user';
private $usernameField;
+ private $usernameClass;
- public function __construct(string $class, EntityManagerInterface $em, string $usernameField = null, DomainIdentityHelper $identityHelper = null)
+ public function __construct(string $class, ?string $usernameField, ?string $usernameClass, EntityManagerInterface $em, DomainIdentityHelper $identityHelper = null)
{
$this->__parent_construct($class, $em, $identityHelper);
$this->usernameField = $usernameField;
+ $this->usernameClass = $usernameClass;
}
/**
@@ -49,22 +51,17 @@ public function find(UserIdInterface $id): User
public function findByUsername(string $username): User
{
- if (null === $this->usernameField) {
- throw new \LogicException('Cannot find users by username without a username field name.');
- }
-
- $qb = $this->createUsernameQueryBuilder($username);
- $criteria = [$this->usernameField => $username];
-
- if (null === $qb) {
- return $this->doFindByFields($criteria);
+ $qb = $this->createQueryBuilderForUsername($username);
+
+ if (null !== $qb) {
+ try {
+ return $qb->getQuery()->getSingleResult();
+ } catch (NoResultException $e) {
+ throw EntityNotFoundException::createForFields($this->class, ['username' => $username]);
+ }
}
- try {
- return $qb->getQuery()->getSingleResult();
- } catch (NoResultException $e) {
- throw EntityNotFoundException::createForFields($this->class, $criteria);
- }
+ return $this->doFindByFields($this->getUsernameCriteria($username));
}
public function exists(UserIdInterface $id): bool
@@ -74,15 +71,15 @@ public function exists(UserIdInterface $id): bool
public function usernameExists(string $username): bool
{
- $qb = $this->createUsernameQueryBuilder($username);
+ $qb = $this->createQueryBuilderForUsername($username);
- if (null === $qb) {
- return $this->doExistsByFields(['username' => $username]);
- }
+ if (null !== $qb) {
+ $qb->select('1');
- $qb->select('1');
+ return (bool) $qb->getQuery()->getScalarResult();
+ }
- return (bool) $qb->getQuery()->getScalarResult();
+ return $this->doExistsByFields($this->getUsernameCriteria($username));
}
public function save(User $user): void
@@ -95,16 +92,23 @@ public function delete(User $user): void
$this->doDelete($user);
}
- private function createUsernameQueryBuilder(string $username): ?QueryBuilder
+ private function getUsernameCriteria(string $username): array
+ {
+ if (null === $this->usernameField) {
+ throw new \LogicException(sprintf('No username field available for entity "%s".', $this->class));
+ }
+
+ return [$this->usernameField => $username];
+ }
+
+ private function createQueryBuilderForUsername(string $username): ?QueryBuilder
{
- if ($this->em->getMetadataFactory()->isTransient(Username::class)) {
+ if (null === $this->usernameClass) {
return null;
}
- $qb = $this->em->createQueryBuilder();
- $qb->select('user');
- $qb->from($this->class, 'user');
- $qb->join(Username::class, 'username', Join::WITH, 'username.user = user');
+ $qb = $this->createQueryBuilder();
+ $qb->join($this->usernameClass, 'username', Join::WITH, 'username.user = '.$this->alias);
$qb->where($qb->expr()->eq('username.username', ':username'));
$qb->setMaxResults(1);
$qb->setParameter('username', $username);
diff --git a/src/User/Infra/Doctrine/Repository/UsernameRepository.php b/src/User/Infra/Doctrine/Repository/UsernameRepository.php
index 04099f30..035e9691 100644
--- a/src/User/Infra/Doctrine/Repository/UsernameRepository.php
+++ b/src/User/Infra/Doctrine/Repository/UsernameRepository.php
@@ -22,13 +22,13 @@ final class UsernameRepository implements UsernameRepositoryInterface
}
private $alias = 'username';
- private $targetMapping;
+ private $targetMappings;
- public function __construct(string $class, EntityManagerInterface $em, array $targetMapping = [], DomainIdentityHelper $identityHelper = null)
+ public function __construct(string $class, EntityManagerInterface $em, array $targetMappings = [], DomainIdentityHelper $identityHelper = null)
{
$this->__parent_construct($class, $em, $identityHelper);
- $this->targetMapping = $targetMapping;
+ $this->targetMappings = $targetMappings;
}
/**
@@ -47,74 +47,52 @@ public function findAll(int $offset = 0, int $limit = 0): DomainCollectionInterf
*/
public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainCollectionInterface
{
- if (!$this->targetMapping) {
- throw new \LogicException('No username mapping available.');
- }
-
- $qb = $this->em->createQueryBuilder();
- $targetInfo = $aliases = [];
- foreach ($this->targetMapping as $class => $mappings) {
+ $results = $targets = [];
+ foreach ($this->targetMappings as $class => $mapping) {
+ $qb = $this->em->createQueryBuilder();
$metadata = $this->em->getClassMetadata($class);
- $alias = $aliases[$class] ?? ($aliases[$class] = 'target'.count($aliases));
+ $fields = array_flip($idFields = $metadata->getIdentifierFieldNames());
- foreach ($mappings as $mapping) {
- $fields = array_flip($idFields = $metadata->getIdentifierFieldNames());
+ foreach ($mapping as $field => $mappedBy) {
+ $targets[$class][$field] = $mappedBy;
+ $fields[$field] = true;
- if (!isset($fields[$mapping['field']])) {
- $fields[$mapping['field']] = true;
+ if (null !== $mappedBy) {
+ $fields[$mappedBy] = true;
}
-
- if (isset($mapping['mapped_by'])) {
- if (!isset($fields[$mapping['mapped_by']])) {
- $fields[$mapping['mapped_by']] = true;
- }
-
- $userField = $mapping['mapped_by'];
+ }
+ foreach ((array) $metadata->discriminatorMap as $discriminatorClass) {
+ if (isset($targets[$discriminatorClass])) {
+ $targets[$discriminatorClass] += $targets[$class];
} else {
- $userField = null;
+ $targets[$discriminatorClass] = $targets[$class];
}
+ }
- $qb->addSelect(sprintf('partial %s.{%s}', $alias, implode(', ', array_keys($fields))));
- $qb->from($mapping['target'], $alias);
-
- $targetInfo[$class][] = ['user_field' => $userField, 'username_field' => $mapping['field']];
-
- foreach ((array) $metadata->discriminatorMap as $discriminatorClass) {
- if (isset($targetInfo[$discriminatorClass]) || isset($this->targetMapping[$discriminatorClass])) {
- continue;
- }
+ $qb->addSelect(sprintf('partial e.{%s}', implode(', ', array_keys($fields))));
+ $qb->from($class, 'e');
- $targetInfo[$discriminatorClass] = $targetInfo[$class];
- }
- }
+ $results[] = $qb->getQuery()->getResult();
}
$result = [];
- foreach ($qb->getQuery()->getResult() as $targetEntity) {
- $metadata = $this->em->getClassMetadata($class = ClassUtils::getRealClass(get_class($targetEntity)));
-
- foreach ($targetInfo[$class] as $info) {
- if ($targetEntity instanceof User) {
- $user = $targetEntity;
- } elseif (isset($info['user_field'])) {
- $user = $metadata->getFieldValue($targetEntity, $info['user_field']);
-
- if (null === $user) {
- continue;
- }
-
- if (!$user instanceof User) {
- throw new \LogicException(sprintf('Field "%s.%s" must return an instance of "%s" or null, got "%s".', $class, $info['user_field'], User::class, is_object($user) ? get_class($user) : gettype($user)));
- }
- } else {
- throw new \LogicException(sprintf('No user field mapped for entity "%s".', $class));
- }
+ foreach ($results ? array_merge(...$results) : [] as $target) {
+ $metadata = $this->em->getClassMetadata($class = ClassUtils::getRealClass(\get_class($target)));
- if (null === $username = $metadata->getFieldValue($targetEntity, $info['username_field'])) {
+ foreach ($targets[$class] as $field => $mappedBy) {
+ $user = null === $mappedBy ? $target : $metadata->getFieldValue($target, $mappedBy);
+
+ if (null === $user) {
continue;
}
- $result[] = new $this->class($user, $username);
+ if (!$user instanceof User) {
+ throw new \LogicException(sprintf('Field "%s.%s" must return an instance of "%s" or null, got "%s".', $class, $field, \get_class($target), \is_object($user) ? \get_class($user) : \gettype($user)));
+ }
+
+ if (null !== $username = $metadata->getFieldValue($target, $field)) {
+ $result[] = new $this->class($user, $username);
+ }
}
}
diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml
index 2c028361..ccb858ad 100644
--- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml
+++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml
index cc3ef8be..e53d8b9c 100644
--- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml
+++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml
@@ -3,12 +3,12 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
-
+
-
+
diff --git a/src/User/Infra/Form/Type/HashedPasswordType.php b/src/User/Infra/Form/Type/HashedPasswordType.php
index f1511538..0c3c2e26 100644
--- a/src/User/Infra/Form/Type/HashedPasswordType.php
+++ b/src/User/Infra/Form/Type/HashedPasswordType.php
@@ -46,17 +46,21 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
if ($options['password_confirm_current']) {
if (!class_exists(Callback::class)) {
- throw new \LogicException('Password validation requires "symfony/validator".');
+ throw new \LogicException('Current password confirmation requires "symfony/validator".');
+ }
+ if (null === $this->tokenStorage) {
+ throw new \LogicException('Current password confirmation requires "symfony/security".');
}
$passwordOptions = self::withConstraint($passwordOptions, new Callback(function (?string $value, ExecutionContextInterface $context) use ($algorithm, $passwordOptions, &$plainPassword): void {
- $valid = true;
- if (null === $value || '' === $value || null === $this->tokenStorage || null === ($token = $this->tokenStorage->getToken()) || !($user = $token->getUser()) instanceof UserInterface) {
+ $token = $this->tokenStorage->getToken();
+
+ if (null === $value || '' === $value || null === $token || !($user = $token->getUser()) instanceof UserInterface) {
$valid = false;
} else {
$algorithm = null === $algorithm && null !== ($salt = $user->getSalt())
? PasswordAlgorithm::createLegacySalted(new PasswordSalt($salt))
- : (is_callable($algorithm) ? $algorithm() : $algorithm);
+ : (\is_callable($algorithm) ? $algorithm() : $algorithm);
$valid = $this->passwordHashing->isValid($user->getPassword(), $plainPassword, $algorithm);
}
@@ -73,12 +77,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$builder->get('password')->addModelTransformer(new CallbackTransformer(function (?string $value): ?string {
return null;
}, function (?string $value) use ($algorithm, &$plainPassword): ?string {
- return null === $value ? null : $this->passwordHashing->hash($plainPassword = $value, is_callable($algorithm) ? $algorithm() : $algorithm);
+ return null === $value ? null : $this->passwordHashing->hash($plainPassword = $value, \is_callable($algorithm) ? $algorithm() : $algorithm);
}));
if ($options['password_confirm']) {
if (!class_exists(Callback::class)) {
- throw new \LogicException('Password validation requires "symfony/validator".');
+ throw new \LogicException('Password confirmation requires "symfony/validator".');
}
$passwordConfirmOptions = ['mapped' => false] + $options['password_confirm_options'] + $defaultOptions;
@@ -96,7 +100,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
return;
}
- if (!$this->passwordHashing->isValid($password, $value, is_callable($algorithm) ? $algorithm() : $algorithm)) {
+ if (!$this->passwordHashing->isValid($password, $value, \is_callable($algorithm) ? $algorithm() : $algorithm)) {
/** @var FormInterface $form */
$form = $context->getObject();
$form->addError(new FormError($passwordConfirmOptions['invalid_message'], $passwordConfirmOptions['invalid_message'], $passwordConfirmOptions['invalid_message_parameters'], null, $this));
@@ -131,7 +135,7 @@ private static function withConstraint(array $options, Constraint $constraint):
{
if (!isset($options['constraints'])) {
$options['constraints'] = [];
- } elseif (!is_array($options['constraints'])) {
+ } elseif (!\is_array($options['constraints'])) {
$options['constraints'] = [$options['constraints']];
}
diff --git a/src/User/Infra/Security/DataCollector.php b/src/User/Infra/Security/DataCollector.php
index e60fb528..064ec1b7 100644
--- a/src/User/Infra/Security/DataCollector.php
+++ b/src/User/Infra/Security/DataCollector.php
@@ -5,9 +5,8 @@
namespace MsgPhp\User\Infra\Security;
use MsgPhp\Domain\Exception\EntityNotFoundException;
-use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
-use MsgPhp\User\Entity\User;
use MsgPhp\User\Repository\UserRepositoryInterface;
+use MsgPhp\User\UserIdInterface;
use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector as BaseDataCollector;
use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
use Symfony\Component\HttpFoundation\Request;
@@ -23,19 +22,19 @@
/**
* @author Roland Franssen
*
+ * @todo consider decorating instead
+ *
* @internal
*/
final class DataCollector extends BaseDataCollector
{
private $repository;
- private $factory;
- public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null, UserRepositoryInterface $repository = null, EntityAwareFactoryInterface $factory = null)
+ public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null, UserRepositoryInterface $repository = null)
{
parent::__construct($tokenStorage, $roleHierarchy, $logoutUrlGenerator, $accessDecisionManager, $firewallMap, $firewall);
$this->repository = $repository;
- $this->factory = $factory;
}
public function collect(Request $request, Response $response, \Exception $exception = null): void
@@ -51,7 +50,7 @@ public function collect(Request $request, Response $response, \Exception $except
$user = $token->getUser();
if ($user instanceof SecurityUser) {
- $this->data['user'] = $this->getUsername($user->getUsername());
+ $this->data['user'] = $this->getUsername($user->getUserId());
}
foreach ($token->getRoles() as $role) {
@@ -62,28 +61,22 @@ public function collect(Request $request, Response $response, \Exception $except
$impersonatorUser = $role->getSource()->getUser();
if ($impersonatorUser instanceof SecurityUser) {
- $this->data['impersonator_user'] = $this->getUsername($impersonatorUser->getUsername());
+ $this->data['impersonator_user'] = $this->getUsername($impersonatorUser->getUserId());
break;
}
}
}
- private function getUsername(string $user): string
+ private function getUsername(UserIdInterface $id): string
{
if (null === $this->repository) {
- return $user;
- }
-
- if (null === $this->factory) {
- throw new \LogicException('No entity factory set.');
+ return $id->toString();
}
try {
- return $this->repository->find($this->factory->identify(User::class, $user))
- ->getCredential()
- ->getUsername();
+ return $this->repository->find($id)->getCredential()->getUsername();
} catch (EntityNotFoundException $e) {
- return $user;
+ return $id->toString();
}
}
}
diff --git a/src/User/Infra/Security/Jwt/SecurityUserProvider.php b/src/User/Infra/Security/Jwt/SecurityUserProvider.php
index 18a2962a..12bf3ee2 100644
--- a/src/User/Infra/Security/Jwt/SecurityUserProvider.php
+++ b/src/User/Infra/Security/Jwt/SecurityUserProvider.php
@@ -4,7 +4,7 @@
namespace MsgPhp\User\Infra\Security\Jwt;
-use Lexik\Bundle\JWTAuthenticationBundle\Security\User\UserProviderWithPayloadSupportsInterface;
+use Lexik\Bundle\JWTAuthenticationBundle\Security\User\PayloadAwareUserProviderInterface;
use MsgPhp\Domain\Exception\EntityNotFoundException;
use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
use MsgPhp\User\Entity\User;
@@ -16,7 +16,7 @@
/**
* @author Damien Merchier
*/
-final class SecurityUserProvider implements UserProviderWithPayloadSupportsInterface
+final class SecurityUserProvider implements PayloadAwareUserProviderInterface
{
private $provider;
private $repository;
diff --git a/src/User/Infra/Security/SecurityUser.php b/src/User/Infra/Security/SecurityUser.php
index 311b6ad3..8c96283f 100644
--- a/src/User/Infra/Security/SecurityUser.php
+++ b/src/User/Infra/Security/SecurityUser.php
@@ -4,8 +4,10 @@
namespace MsgPhp\User\Infra\Security;
+use MsgPhp\User\Entity\Credential\Anonymous;
use MsgPhp\User\Entity\User;
use MsgPhp\User\Password\PasswordProtectedInterface;
+use MsgPhp\User\UserIdInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -15,16 +17,28 @@
final class SecurityUser implements UserInterface, EquatableInterface, \Serializable
{
private $id;
+ private $originUsername;
private $roles;
private $password;
private $passwordSalt;
- public function __construct(User $user, array $roles = [])
+ public function __construct(User $user, string $originUsername = null, array $roles = [])
{
- $this->id = $user->getId()->toString();
- $this->roles = $roles;
+ $this->id = $user->getId();
+
+ if ($this->id->isEmpty()) {
+ throw new \LogicException('The user ID cannot be empty.');
+ }
$credential = $user->getCredential();
+
+ if (null === $originUsername && !$credential instanceof Anonymous) {
+ $originUsername = $credential->getUsername();
+ }
+
+ $this->originUsername = $originUsername;
+ $this->roles = $roles;
+
if ($credential instanceof PasswordProtectedInterface) {
$this->password = $credential->getPassword();
@@ -35,11 +49,21 @@ public function __construct(User $user, array $roles = [])
}
}
- public function getUsername(): string
+ public function getUserId(): UserIdInterface
{
return $this->id;
}
+ public function getOriginUsername(): ?string
+ {
+ return $this->originUsername;
+ }
+
+ public function getUsername(): string
+ {
+ return $this->id->toString();
+ }
+
public function getRoles(): array
{
return $this->roles;
@@ -57,22 +81,21 @@ public function getSalt(): ?string
public function eraseCredentials(): void
{
- $this->password = '';
- $this->passwordSalt = null;
+ $this->password = $this->passwordSalt = null;
}
public function isEqualTo(UserInterface $user)
{
- return $user instanceof self && $this->id === $user->getUsername();
+ return $user instanceof self && $user->getUserId()->equals($this->id);
}
public function serialize(): string
{
- return serialize([$this->id, $this->roles, $this->password, $this->passwordSalt]);
+ return serialize([$this->id, $this->originUsername, $this->roles, $this->password, $this->passwordSalt]);
}
public function unserialize($serialized): void
{
- list($this->id, $this->roles, $this->password, $this->passwordSalt) = unserialize($serialized);
+ list($this->id, $this->originUsername, $this->roles, $this->password, $this->passwordSalt) = unserialize($serialized);
}
}
diff --git a/src/User/Infra/Security/SecurityUserProvider.php b/src/User/Infra/Security/SecurityUserProvider.php
index 75155acf..b7ddf293 100644
--- a/src/User/Infra/Security/SecurityUserProvider.php
+++ b/src/User/Infra/Security/SecurityUserProvider.php
@@ -5,9 +5,9 @@
namespace MsgPhp\User\Infra\Security;
use MsgPhp\Domain\Exception\EntityNotFoundException;
-use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
use MsgPhp\User\Entity\User;
use MsgPhp\User\Repository\UserRepositoryInterface;
+use MsgPhp\User\Role\RoleProviderInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -19,14 +19,12 @@
final class SecurityUserProvider implements UserProviderInterface
{
private $repository;
- private $factory;
- private $rolesProvider;
+ private $roleProvider;
- public function __construct(UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory, UserRolesProviderInterface $rolesProvider = null)
+ public function __construct(UserRepositoryInterface $repository, RoleProviderInterface $roleProvider = null)
{
$this->repository = $repository;
- $this->factory = $factory;
- $this->rolesProvider = $rolesProvider;
+ $this->roleProvider = $roleProvider;
}
public function loadUserByUsername($username): UserInterface
@@ -37,22 +35,24 @@ public function loadUserByUsername($username): UserInterface
throw new UsernameNotFoundException($e->getMessage());
}
- return $this->fromUser($user);
+ return $this->fromUser($user, $username);
}
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof SecurityUser) {
- throw new UnsupportedUserException(sprintf('Unsupported user "%s".', get_class($user)));
+ throw new UnsupportedUserException(sprintf('Unsupported user "%s".', \get_class($user)));
}
+ $securityUser = $user;
+
try {
- $user = $this->repository->find($this->factory->identify(User::class, $user->getUsername()));
+ $user = $this->repository->find($securityUser->getUserId());
} catch (EntityNotFoundException $e) {
throw new UsernameNotFoundException($e->getMessage());
}
- return $this->fromUser($user);
+ return $this->fromUser($user, $securityUser->getOriginUsername());
}
public function supportsClass($class): bool
@@ -60,8 +60,8 @@ public function supportsClass($class): bool
return SecurityUser::class === $class;
}
- public function fromUser(User $user): SecurityUser
+ public function fromUser(User $user, string $originUsername = null): SecurityUser
{
- return new SecurityUser($user, $this->rolesProvider ? $this->rolesProvider->getRoles($user) : []);
+ return new SecurityUser($user, $originUsername, $this->roleProvider ? $this->roleProvider->getRoles($user) : []);
}
}
diff --git a/src/User/Infra/Security/TokenStorageAwareTrait.php b/src/User/Infra/Security/TokenStorageAwareTrait.php
index 13c94051..b83e099c 100644
--- a/src/User/Infra/Security/TokenStorageAwareTrait.php
+++ b/src/User/Infra/Security/TokenStorageAwareTrait.php
@@ -4,7 +4,6 @@
namespace MsgPhp\User\Infra\Security;
-use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
use MsgPhp\User\Entity\User;
use MsgPhp\User\Repository\UserRepositoryInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -18,13 +17,11 @@ trait TokenStorageAwareTrait
{
private $tokenStorage;
private $repository;
- private $factory;
- public function __construct(TokenStorageInterface $tokenStorage, UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory)
+ public function __construct(TokenStorageInterface $tokenStorage, UserRepositoryInterface $repository)
{
$this->tokenStorage = $tokenStorage;
$this->repository = $repository;
- $this->factory = $factory;
}
private function isUser(): bool
@@ -44,6 +41,6 @@ private function toUser(): ?User
return null;
}
- return $this->repository->find($this->factory->identify(User::class, $user->getUsername()));
+ return $this->repository->find($user->getUserId());
}
}
diff --git a/src/User/Infra/Security/UserValueResolver.php b/src/User/Infra/Security/UserArgumentValueResolver.php
similarity index 69%
rename from src/User/Infra/Security/UserValueResolver.php
rename to src/User/Infra/Security/UserArgumentValueResolver.php
index 886a6e68..4e10c7cc 100644
--- a/src/User/Infra/Security/UserValueResolver.php
+++ b/src/User/Infra/Security/UserArgumentValueResolver.php
@@ -12,17 +12,13 @@
/**
* @author Roland Franssen
*/
-final class UserValueResolver implements ArgumentValueResolverInterface
+final class UserArgumentValueResolver implements ArgumentValueResolverInterface
{
use TokenStorageAwareTrait;
public function supports(Request $request, ArgumentMetadata $argument): bool
{
- if (User::class === ($type = $argument->getType()) || is_subclass_of($type, User::class)) {
- return $argument->isNullable() || $this->isUser();
- }
-
- return false;
+ return is_a($argument->getType(), User::class, true) ? ($argument->isNullable() || $this->isUser()) : false;
}
public function resolve(Request $request, ArgumentMetadata $argument): \Generator
diff --git a/src/User/Infra/Security/UserParamConverter.php b/src/User/Infra/Security/UserParamConverter.php
index b5451a43..15c954e7 100644
--- a/src/User/Infra/Security/UserParamConverter.php
+++ b/src/User/Infra/Security/UserParamConverter.php
@@ -29,10 +29,6 @@ public function apply(Request $request, ParamConverter $configuration): bool
public function supports(ParamConverter $configuration): bool
{
- if (User::class == ($class = $configuration->getClass()) || is_subclass_of($class, User::class)) {
- return $configuration->isOptional() || $this->isUser();
- }
-
- return false;
+ return is_a($configuration->getClass(), User::class, true) ? ($configuration->isOptional() || $this->isUser()) : false;
}
}
diff --git a/src/User/Infra/Validator/ExistingUsername.php b/src/User/Infra/Validator/ExistingUsername.php
index 6abf0e06..fc27f203 100644
--- a/src/User/Infra/Validator/ExistingUsername.php
+++ b/src/User/Infra/Validator/ExistingUsername.php
@@ -7,11 +7,18 @@
use Symfony\Component\Validator\Constraint;
/**
+ * @Annotation
+ * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
+ *
* @author Roland Franssen
*/
final class ExistingUsername extends Constraint
{
public const DOES_NOT_EXIST_ERROR = '4a8b28f7-a2b5-4435-9dd8-3be5188d23f0';
+ protected static $errorNames = [
+ self::DOES_NOT_EXIST_ERROR => 'DOES_NOT_EXIST_ERROR',
+ ];
+
public $message = 'This value is not valid.';
}
diff --git a/src/User/Infra/Validator/UniqueUsername.php b/src/User/Infra/Validator/UniqueUsername.php
index 0f8fc5a6..d33d720a 100644
--- a/src/User/Infra/Validator/UniqueUsername.php
+++ b/src/User/Infra/Validator/UniqueUsername.php
@@ -7,11 +7,18 @@
use Symfony\Component\Validator\Constraint;
/**
+ * @Annotation
+ * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
+ *
* @author Roland Franssen
*/
final class UniqueUsername extends Constraint
{
public const IS_NOT_UNIQUE_ERROR = '37c4ba30-07ae-48e5-9767-19764e027346';
+ protected static $errorNames = [
+ self::IS_NOT_UNIQUE_ERROR => 'IS_NOT_UNIQUE_ERROR',
+ ];
+
public $message = 'This value is not valid.';
}
diff --git a/src/User/Password/PasswordHashing.php b/src/User/Password/PasswordHashing.php
index b0990018..719bb7cc 100644
--- a/src/User/Password/PasswordHashing.php
+++ b/src/User/Password/PasswordHashing.php
@@ -23,7 +23,13 @@ public function hash(string $plainPassword, PasswordAlgorithm $algorithm = null)
$algorithm = $algorithm ?? $this->defaultAlgorithm;
if (!$algorithm->legacy) {
- return password_hash($plainPassword, $algorithm->type, $algorithm->options);
+ $hash = password_hash($plainPassword, $algorithm->type, $algorithm->options);
+
+ if (false === $hash) {
+ throw new \RuntimeException(sprintf('Unable to hash password with algorithm "%s".', $algorithm->type));
+ }
+
+ return $hash;
}
if ($this->deprecateLegacyApi) {
diff --git a/src/User/README.md b/src/User/README.md
index 136cc9ec..24941ca0 100644
--- a/src/User/README.md
+++ b/src/User/README.md
@@ -1,4 +1,4 @@
-# User domain layer
+# User Domain
A domain layer providing basic user management.
@@ -16,10 +16,11 @@ composer require msgphp/user
## Features
-- Doctrine persistence
- Symfony console commands
+- Symfony messenger commands & events
- Symfony security infrastructure
- Symfony validators
+- Doctrine persistence
- Credential independent (supports e-mail, nickname, etc.)
- Multiple username / credential support
- Primary and secondary user e-mails
@@ -29,6 +30,7 @@ composer require msgphp/user
## Blog posts
+- [Adding user management to your Symfony application](https://medium.com/@ro0NL/adding-user-management-to-your-symfony-application-ceeefe2a2e9)
- [Commanding a decoupled User entity](https://medium.com/@ro0NL/commanding-a-decoupled-user-entity-aee8723c43e5)
- [Decoupling the User entity with a new Symfony User Bundle](https://medium.com/@ro0NL/decoupling-the-user-entity-with-a-new-symfony-user-bundle-7d2d5d85bdf9)
- [Building a new Symfony User Bundle](https://medium.com/@ro0NL/building-a-new-symfony-user-bundle-b4fe5a9d9d80)
diff --git a/src/User/Repository/UserAttributeValueRepositoryInterface.php b/src/User/Repository/UserAttributeValueRepositoryInterface.php
index 58080531..0e3445bc 100644
--- a/src/User/Repository/UserAttributeValueRepositoryInterface.php
+++ b/src/User/Repository/UserAttributeValueRepositoryInterface.php
@@ -11,9 +11,6 @@
/**
* @author Roland Franssen
- *
- * @todo move findAllByAttributeId+findAllByAttributeIdAndValue to Eav\AttributeValueRepositoryInterface
- * @todo extend from Eav\EntityAttributeValueRepositoryInterface?
*/
interface UserAttributeValueRepositoryInterface
{
diff --git a/src/User/Role/ChainRoleProvider.php b/src/User/Role/ChainRoleProvider.php
new file mode 100644
index 00000000..ba5b5b92
--- /dev/null
+++ b/src/User/Role/ChainRoleProvider.php
@@ -0,0 +1,36 @@
+
+ */
+final class ChainRoleProvider implements RoleProviderInterface
+{
+ private $providers;
+
+ /**
+ * @param RoleProviderInterface[] $providers
+ */
+ public function __construct(iterable $providers)
+ {
+ $this->providers = $providers;
+ }
+
+ public function getRoles(User $user): array
+ {
+ $roles = [];
+
+ foreach ($this->providers as $provider) {
+ foreach ($provider->getRoles($user) as $role) {
+ $roles[$role] = true;
+ }
+ }
+
+ return array_keys($roles);
+ }
+}
diff --git a/src/User/Role/DefaultRoleProvider.php b/src/User/Role/DefaultRoleProvider.php
new file mode 100644
index 00000000..2d2b0550
--- /dev/null
+++ b/src/User/Role/DefaultRoleProvider.php
@@ -0,0 +1,28 @@
+
+ */
+final class DefaultRoleProvider implements RoleProviderInterface
+{
+ private $roles;
+
+ /**
+ * @param string[] $roles
+ */
+ public function __construct(array $roles, bool $sanitize = true)
+ {
+ $this->roles = $sanitize ? array_values(array_unique($roles)) : $roles;
+ }
+
+ public function getRoles(User $user): array
+ {
+ return $this->roles;
+ }
+}
diff --git a/src/User/Infra/Security/UserRolesProviderInterface.php b/src/User/Role/RoleProviderInterface.php
similarity index 74%
rename from src/User/Infra/Security/UserRolesProviderInterface.php
rename to src/User/Role/RoleProviderInterface.php
index bdfad3e6..7bee001a 100644
--- a/src/User/Infra/Security/UserRolesProviderInterface.php
+++ b/src/User/Role/RoleProviderInterface.php
@@ -2,14 +2,14 @@
declare(strict_types=1);
-namespace MsgPhp\User\Infra\Security;
+namespace MsgPhp\User\Role;
use MsgPhp\User\Entity\User;
/**
* @author Roland Franssen
*/
-interface UserRolesProviderInterface
+interface RoleProviderInterface
{
/**
* @return string[]
diff --git a/src/User/Role/UserRoleProvider.php b/src/User/Role/UserRoleProvider.php
new file mode 100644
index 00000000..980c1fad
--- /dev/null
+++ b/src/User/Role/UserRoleProvider.php
@@ -0,0 +1,30 @@
+
+ */
+final class UserRoleProvider implements RoleProviderInterface
+{
+ private $repository;
+
+ public function __construct(UserRoleRepositoryInterface $repository)
+ {
+ $this->repository = $repository;
+ }
+
+ public function getRoles(User $user): array
+ {
+ $roles = $this->repository->findAllByUserId($user->getId())->map(function (UserRole $userRole) {
+ return $userRole->getRoleName();
+ });
+
+ return array_values($roles);
+ }
+}
diff --git a/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php b/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php
index ecc00ec4..f1f6f00c 100644
--- a/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php
+++ b/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php
@@ -11,15 +11,15 @@
final class AttributeValuesFieldTest extends TestCase
{
- public function testGetAttributeValues(): void
+ public function testField(): void
{
$object = $this->getObject($attributeValues = [$this->createMock(UserAttributeValue::class)]);
- $this->assertSame($attributeValues, iterator_to_array($object->getAttributeValues()));
+ self::assertSame($attributeValues, iterator_to_array($object->getAttributeValues()));
$object = $this->getObject($attributeValues = $this->createMock(DomainCollectionInterface::class));
- $this->assertSame($attributeValues, $object->getAttributeValues());
+ self::assertSame($attributeValues, $object->getAttributeValues());
}
private function getObject($value)
diff --git a/src/User/Tests/Entity/Fields/EmailsFieldTest.php b/src/User/Tests/Entity/Fields/EmailsFieldTest.php
index b74a3636..bdc87a2a 100644
--- a/src/User/Tests/Entity/Fields/EmailsFieldTest.php
+++ b/src/User/Tests/Entity/Fields/EmailsFieldTest.php
@@ -11,15 +11,13 @@
final class EmailsFieldTest extends TestCase
{
- public function testGetEmails(): void
+ public function testField(): void
{
$object = $this->getObject($emails = [$this->createMock(UserEmail::class)]);
- $this->assertSame($emails, iterator_to_array($object->getEmails()));
-
- $object = $this->getObject($emails = $this->createMock(DomainCollectionInterface::class));
-
- $this->assertSame($emails, $object->getEmails());
+ self::assertInstanceOf(DomainCollectionInterface::class, $collection = $object->getEmails());
+ self::assertSame($emails, iterator_to_array($collection));
+ self::assertSame($collection = $this->createMock(DomainCollectionInterface::class), $this->getObject($collection)->getEmails());
}
private function getObject($value)
diff --git a/src/User/Tests/Entity/Fields/RoleFieldTest.php b/src/User/Tests/Entity/Fields/RoleFieldTest.php
index 957654de..60bb040d 100644
--- a/src/User/Tests/Entity/Fields/RoleFieldTest.php
+++ b/src/User/Tests/Entity/Fields/RoleFieldTest.php
@@ -10,17 +10,17 @@
final class RoleFieldTest extends TestCase
{
- public function testGetRole(): void
+ public function testField(): void
{
$value = $this->createMock(Role::class);
- $value->expects($this->any())
+ $value->expects(self::any())
->method('getName')
->willReturn('ROLE_FOO');
$object = $this->getObject($value);
- $this->assertSame($value, $object->getRole());
- $this->assertSame('ROLE_FOO', $object->getRoleName());
+ self::assertSame($value, $object->getRole());
+ self::assertSame('ROLE_FOO', $object->getRoleName());
}
private function getObject($value)
diff --git a/src/User/Tests/Entity/Fields/RolesFieldTest.php b/src/User/Tests/Entity/Fields/RolesFieldTest.php
new file mode 100644
index 00000000..2da61569
--- /dev/null
+++ b/src/User/Tests/Entity/Fields/RolesFieldTest.php
@@ -0,0 +1,34 @@
+getObject($roles = [$this->createMock(UserRole::class)]);
+
+ self::assertInstanceOf(DomainCollectionInterface::class, $collection = $object->getRoles());
+ self::assertSame($roles, iterator_to_array($collection));
+ self::assertSame($collection = $this->createMock(DomainCollectionInterface::class), $this->getObject($collection)->getRoles());
+ }
+
+ private function getObject($value)
+ {
+ return new class($value) {
+ use RolesField;
+
+ public function __construct($value)
+ {
+ $this->roles = $value;
+ }
+ };
+ }
+}
diff --git a/src/User/Tests/Entity/Fields/UserFieldTest.php b/src/User/Tests/Entity/Fields/UserFieldTest.php
index 02223ce4..8d0768dc 100644
--- a/src/User/Tests/Entity/Fields/UserFieldTest.php
+++ b/src/User/Tests/Entity/Fields/UserFieldTest.php
@@ -11,17 +11,17 @@
final class UserFieldTest extends TestCase
{
- public function testGetUser(): void
+ public function testField(): void
{
$value = $this->createMock(User::class);
- $value->expects($this->any())
+ $value->expects(self::any())
->method('getId')
->willReturn($this->createMock(UserIdInterface::class));
$object = $this->getObject($value);
- $this->assertSame($value, $object->getUser());
- $this->assertSame($value->getId(), $object->getUserId());
+ self::assertSame($value, $object->getUser());
+ self::assertSame($value->getId(), $object->getUserId());
}
private function getObject($value)
diff --git a/src/User/Tests/Entity/UserAttributeValueTest.php b/src/User/Tests/Entity/UserAttributeValueTest.php
index b9819263..a4183b28 100644
--- a/src/User/Tests/Entity/UserAttributeValueTest.php
+++ b/src/User/Tests/Entity/UserAttributeValueTest.php
@@ -4,6 +4,7 @@
namespace MsgPhp\User\Tests\Entity;
+use MsgPhp\Eav\AttributeValueIdInterface;
use MsgPhp\Eav\Entity\AttributeValue;
use MsgPhp\User\Entity\{User, UserAttributeValue};
use PHPUnit\Framework\TestCase;
@@ -12,10 +13,14 @@ final class UserAttributeValueTest extends TestCase
{
public function testCreate(): void
{
- $userAttributeValue = $this->createEntity($user = $this->createMock(User::class), $attributeValue = $this->createMock(AttributeValue::class));
+ $attributeValue = $this->createMock(AttributeValue::class);
+ $attributeValue->expects(self::once())
+ ->method('getId')
+ ->willReturn($id = $this->createMock(AttributeValueIdInterface::class));
+ $userAttributeValue = $this->createEntity($user = $this->createMock(User::class), $attributeValue);
- $this->assertSame($user, $userAttributeValue->getUser());
- $this->assertSame($attributeValue, $userAttributeValue->getAttributeValue());
+ self::assertSame($user, $userAttributeValue->getUser());
+ self::assertSame($id, $userAttributeValue->getId());
}
private function createEntity($user, $attributeValue): UserAttributeValue
diff --git a/src/User/Tests/Entity/UserEmailTest.php b/src/User/Tests/Entity/UserEmailTest.php
index 897b084f..12cd29c8 100644
--- a/src/User/Tests/Entity/UserEmailTest.php
+++ b/src/User/Tests/Entity/UserEmailTest.php
@@ -13,8 +13,8 @@ public function testCreate(): void
{
$userEmail = $this->createEntity($user = $this->createMock(User::class), 'foo@bar.baz');
- $this->assertSame($user, $userEmail->getUser());
- $this->assertSame('foo@bar.baz', $userEmail->getEmail());
+ self::assertSame($user, $userEmail->getUser());
+ self::assertSame('foo@bar.baz', $userEmail->getEmail());
}
private function createEntity($user, $email): UserEmail
diff --git a/src/User/Tests/Entity/UserRoleTest.php b/src/User/Tests/Entity/UserRoleTest.php
index 8f76b02e..88f81061 100644
--- a/src/User/Tests/Entity/UserRoleTest.php
+++ b/src/User/Tests/Entity/UserRoleTest.php
@@ -13,8 +13,8 @@ public function testCreate(): void
{
$userRole = $this->createEntity($user = $this->createMock(User::class), $role = $this->createMock(Role::class));
- $this->assertSame($user, $userRole->getUser());
- $this->assertSame($role, $userRole->getRole());
+ self::assertSame($user, $userRole->getUser());
+ self::assertSame($role, $userRole->getRole());
}
private function createEntity($user, $role): UserRole
diff --git a/src/User/Tests/Entity/UserTest.php b/src/User/Tests/Entity/UserTest.php
index 7910bc3d..b9bedd7f 100644
--- a/src/User/Tests/Entity/UserTest.php
+++ b/src/User/Tests/Entity/UserTest.php
@@ -12,7 +12,7 @@ final class UserTest extends TestCase
{
public function testGetCredential(): void
{
- $this->assertInstanceOf(Credential\Anonymous::class, $this->createEntity($this->createMock(UserIdInterface::class))->getCredential());
+ self::assertInstanceOf(Credential\Anonymous::class, $this->createEntity($this->createMock(UserIdInterface::class))->getCredential());
}
private function createEntity($id): User
diff --git a/src/User/Tests/Infra/Doctrine/EntityFieldsMappingTest.php b/src/User/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
deleted file mode 100644
index c2d56fc8..00000000
--- a/src/User/Tests/Infra/Doctrine/EntityFieldsMappingTest.php
+++ /dev/null
@@ -1,29 +0,0 @@
-assertSame(array_keys($available), $mapping);
- }
-}
diff --git a/src/User/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php b/src/User/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
new file mode 100644
index 00000000..538d946b
--- /dev/null
+++ b/src/User/Tests/Infra/Doctrine/ObjectFieldMappingsTest.php
@@ -0,0 +1,30 @@
+createRepository($this->createUser()), $this->createFactory()))->loadUserByUsername('username');
+ /** @var SecurityUser $user */
+ $user = (new SecurityUserProvider($this->createRepository($entity = $this->createUser())))->loadUserByUsername('username');
+
+ self::assertInstanceOf(SecurityUser::class, $user);
+ self::assertSame($entity->getId(), $user->getUserId());
+ self::assertSame('id', $user->getUsername());
+ self::assertSame('username', $user->getOriginUsername());
+ self::assertSame([], $user->getRoles());
+ self::assertSame('', $user->getPassword());
+ self::assertNull($user->getSalt());
+ }
- $this->assertSame('id', $user->getUsername());
- $this->assertSame([], $user->getRoles());
- $this->assertSame('', $user->getPassword());
- $this->assertNull($user->getSalt());
+ public function testLoadUserByUsernameWithOriginUsername(): void
+ {
+ /** @var SecurityUser $user */
+ $user = (new SecurityUserProvider($this->createRepository($entity = $this->createUser(), 'origin-username')))->loadUserByUsername('origin-username');
+
+ self::assertInstanceOf(SecurityUser::class, $user);
+ self::assertSame($entity->getId(), $user->getUserId());
+ self::assertSame('id', $user->getUsername());
+ self::assertSame('origin-username', $user->getOriginUsername());
+ self::assertSame([], $user->getRoles());
+ self::assertSame('', $user->getPassword());
+ self::assertNull($user->getSalt());
}
public function testLoadUserByUsernameWithRoles(): void
{
- $rolesProvider = $this->createMock(UserRolesProviderInterface::class);
- $rolesProvider->expects($this->any())
+ $roleProvider = $this->createMock(RoleProviderInterface::class);
+ $roleProvider->expects(self::any())
->method('getRoles')
- ->willReturn($roles = ['ROLE_FOO']);
- $user = (new SecurityUserProvider($this->createRepository($this->createUser()), $this->createFactory(), $rolesProvider))->loadUserByUsername('username');
-
- $this->assertSame('id', $user->getUsername());
- $this->assertSame($roles, $user->getRoles());
- $this->assertSame('', $user->getPassword());
- $this->assertNull($user->getSalt());
+ ->willReturn(['ROLE_FOO']);
+
+ /** @var SecurityUser $user */
+ $user = (new SecurityUserProvider($this->createRepository($entity = $this->createUser()), $roleProvider))->loadUserByUsername('username');
+
+ self::assertInstanceOf(SecurityUser::class, $user);
+ self::assertSame($entity->getId(), $user->getUserId());
+ self::assertSame('id', $user->getUsername());
+ self::assertSame('username', $user->getOriginUsername());
+ self::assertSame(['ROLE_FOO'], $user->getRoles());
+ self::assertSame('', $user->getPassword());
+ self::assertNull($user->getSalt());
}
public function testLoadUserByUsernameWithUnknownUsername(): void
{
- $provider = new SecurityUserProvider($this->createRepository(), $this->createMock(EntityAwareFactoryInterface::class));
+ $provider = new SecurityUserProvider($this->createRepository());
$this->expectException(UsernameNotFoundException::class);
@@ -52,16 +75,33 @@ public function testLoadUserByUsernameWithUnknownUsername(): void
public function testRefreshUser(): void
{
- $provider = new SecurityUserProvider($this->createRepository($this->createUser()), $this->createFactory());
+ $provider = new SecurityUserProvider($this->createRepository($this->createUser()));
+
+ /** @var SecurityUser $user */
$user = $provider->refreshUser($originUser = $provider->loadUserByUsername('username'));
- $this->assertEquals($originUser, $user);
- $this->assertNotSame($originUser, $user);
+ self::assertInstanceOf(SecurityUser::class, $user);
+ self::assertEquals($originUser, $user);
+ self::assertNotSame($originUser, $user);
+ self::assertSame('username', $user->getOriginUsername());
+ }
+
+ public function testRefreshUserWithOriginUsername(): void
+ {
+ $provider = new SecurityUserProvider($this->createRepository($this->createUser(), 'origin-username'));
+
+ /** @var SecurityUser $user */
+ $user = $provider->refreshUser($originUser = $provider->loadUserByUsername('origin-username'));
+
+ self::assertInstanceOf(SecurityUser::class, $user);
+ self::assertEquals($originUser, $user);
+ self::assertNotSame($originUser, $user);
+ self::assertSame('origin-username', $user->getOriginUsername());
}
public function testRefreshUserWithUnknownUser(): void
{
- $provider = new SecurityUserProvider($this->createRepository(), $this->createFactory());
+ $provider = new SecurityUserProvider($this->createRepository());
$this->expectException(UsernameNotFoundException::class);
@@ -70,7 +110,7 @@ public function testRefreshUserWithUnknownUser(): void
public function testRefreshUserWithUnsupportedUser(): void
{
- $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class));
+ $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class));
$this->expectException(UnsupportedUserException::class);
@@ -79,45 +119,42 @@ public function testRefreshUserWithUnsupportedUser(): void
public function testSupportsClass(): void
{
- $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class));
+ $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class));
- $this->assertTrue($provider->supportsClass(SecurityUser::class));
- $this->assertFalse($provider->supportsClass(UserInterface::class));
+ self::assertTrue($provider->supportsClass(SecurityUser::class));
+ self::assertFalse($provider->supportsClass(UserInterface::class));
}
public function testFromUser(): void
{
- $rolesProvider = $this->createMock(UserRolesProviderInterface::class);
- $rolesProvider->expects($this->once())
+ $roleProvider = $this->createMock(RoleProviderInterface::class);
+ $roleProvider->expects(self::once())
->method('getRoles')
- ->willReturn($roles = ['ROLE_FOO']);
- $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class), $rolesProvider);
+ ->willReturn(['ROLE_FOO']);
- $this->assertInstanceOf(SecurityUser::class, $user = $provider->fromUser($this->createMock(User::class)));
- $this->assertSame($roles, $user->getRoles());
+ $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $roleProvider);
+ $securityUser = $provider->fromUser($user = $this->createUser());
+
+ self::assertSame($user->getId(), $securityUser->getUserId());
+ self::assertSame('id', $securityUser->getUsername());
+ self::assertSame('username', $securityUser->getOriginUsername());
+ self::assertSame('', $securityUser->getPassword());
+ self::assertNull($securityUser->getSalt());
+ self::assertSame(['ROLE_FOO'], $securityUser->getRoles());
}
- private function createFactory(): EntityAwareFactoryInterface
+ public function testFromUserWithOriginUsername(): void
{
- $factory = $this->createMock(EntityAwareFactoryInterface::class);
- $factory->expects($this->any())
- ->method('identify')
- ->willReturnCallback(function ($class, $id) {
- $userId = $this->createMock(UserIdInterface::class);
- $userId->expects($this->any())
- ->method('toString')
- ->willReturn($id);
-
- return $userId;
- });
+ $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class));
+ $securityUser = $provider->fromUser($user = $this->createUser(), 'origin-username');
- return $factory;
+ self::assertSame('origin-username', $securityUser->getOriginUsername());
}
- private function createRepository(User $user = null): UserRepositoryInterface
+ private function createRepository(User $user = null, string $originUsername = null): UserRepositoryInterface
{
$repository = $this->createMock(UserRepositoryInterface::class);
- $repository->expects($this->any())
+ $repository->expects(self::any())
->method('find')
->willReturnCallback(function (UserIdInterface $id) use ($user) {
if (null === $user || $id->toString() !== $user->getId()->toString()) {
@@ -126,11 +163,11 @@ private function createRepository(User $user = null): UserRepositoryInterface
return $user;
});
- $repository->expects($this->any())
+ $repository->expects(self::any())
->method('findByUsername')
- ->willReturnCallback(function (string $username) use ($user) {
- if (null === $user || $username !== $user->getCredential()->getUsername()) {
- throw EntityNotFoundException::createForFields(User::class, ['username' => $$username]);
+ ->willReturnCallback(function (string $username) use ($user, $originUsername) {
+ if (null === $user || ($username !== $originUsername && $username !== $user->getCredential()->getUsername())) {
+ throw EntityNotFoundException::createForFields(User::class, ['username' => $username]);
}
return $user;
@@ -143,17 +180,17 @@ private function createUser(string $id = 'id', string $username = 'username'): U
{
$user = $this->createMock(User::class);
- $user->expects($this->any())
+ $user->expects(self::any())
->method('getId')
->willReturn($userId = $this->createMock(UserIdInterface::class));
- $userId->expects($this->any())
+ $userId->expects(self::any())
->method('toString')
->willReturn($id);
- $user->expects($this->any())
+ $user->expects(self::any())
->method('getCredential')
->willReturn($credential = $this->createMock(CredentialInterface::class));
- $credential->expects($this->any())
+ $credential->expects(self::any())
->method('getUsername')
->willReturn($username);
diff --git a/src/User/Tests/Infra/Security/SecurityUserTest.php b/src/User/Tests/Infra/Security/SecurityUserTest.php
index bc3982f7..1209419b 100644
--- a/src/User/Tests/Infra/Security/SecurityUserTest.php
+++ b/src/User/Tests/Infra/Security/SecurityUserTest.php
@@ -4,6 +4,8 @@
namespace MsgPhp\User\Tests\Infra\Security;
+use MsgPhp\Domain\DomainId;
+use MsgPhp\Domain\DomainIdInterface;
use MsgPhp\User\{CredentialInterface, UserIdInterface};
use MsgPhp\User\Entity\User;
use MsgPhp\User\Infra\Security\SecurityUser;
@@ -15,31 +17,44 @@ final class SecurityUserTest extends TestCase
{
public function testCreate(): void
{
- $user = new SecurityUser($this->createUser('id'), ['ROLE_FOO']);
+ $user = new SecurityUser($entity = $this->createUser('id'), null, ['ROLE_FOO']);
+
+ self::assertSame($entity->getId(), $user->getUserId());
+ self::assertSame('username', $user->getOriginUsername());
+ self::assertSame('id', $user->getUsername());
+ self::assertSame(['ROLE_FOO'], $user->getRoles());
+ self::assertSame('', $user->getPassword());
+ self::assertNull($user->getSalt());
+ }
- $this->assertSame('id', $user->getUsername());
- $this->assertSame(['ROLE_FOO'], $user->getRoles());
- $this->assertSame('', $user->getPassword());
- $this->assertNull($user->getSalt());
- $this->assertEquals($user, unserialize(serialize($user)));
+ public function testCreateWithOriginUsername(): void
+ {
+ self::assertSame('origin-username', (new SecurityUser($this->createUser('id'), 'origin-username'))->getOriginUsername());
}
public function testCreateWithPassword(): void
{
$user = new SecurityUser($this->createUser('id', 'password'));
- $this->assertSame('password', $user->getPassword());
- $this->assertNull($user->getSalt());
- $this->assertEquals($user, unserialize(serialize($user)));
+ self::assertSame('password', $user->getPassword());
+ self::assertNull($user->getSalt());
}
public function testCreateWithSaltedPassword(): void
{
$user = new SecurityUser($this->createUser('id', 'password', 'salt'));
- $this->assertSame('password', $user->getPassword());
- $this->assertSame('salt', $user->getSalt());
- $this->assertEquals($user, unserialize(serialize($user)));
+ self::assertSame('password', $user->getPassword());
+ self::assertSame('salt', $user->getSalt());
+ }
+
+ public function testCreateWithEmptyId(): void
+ {
+ $user = $this->createUser();
+
+ $this->expectException(\LogicException::class);
+
+ new SecurityUser($user);
}
public function testEraseCredentials(): void
@@ -47,35 +62,63 @@ public function testEraseCredentials(): void
$user = new SecurityUser($this->createUser('id', 'password', 'salt'));
$user->eraseCredentials();
- $this->assertSame('', $user->getPassword());
- $this->assertNull($user->getSalt());
+ self::assertSame('', $user->getPassword());
+ self::assertNull($user->getSalt());
}
public function testIsEqualTo(): void
{
$user = new SecurityUser($this->createUser('id'));
- $this->assertTrue($user->isEqualTo($user));
- $this->assertTrue($user->isEqualTo(new SecurityUser($this->createUser('id'))));
- $this->assertTrue($user->isEqualTo(new SecurityUser($this->createUser('id', 'password', 'salt'))));
- $this->assertFalse($user->isEqualTo(new SecurityUser($this->createUser('other'))));
+ self::assertTrue($user->isEqualTo($user));
+ self::assertTrue($user->isEqualTo(new SecurityUser($this->createUser('id'))));
+ self::assertTrue($user->isEqualTo(new SecurityUser($this->createUser('id', 'password', 'salt'))));
+ self::assertFalse($user->isEqualTo(new SecurityUser($this->createUser('other'))));
+ }
+ public function testIsEqualToWithOtherUserType(): void
+ {
$other = $this->createMock(UserInterface::class);
- $other->expects($this->any())
+ $other->expects(self::any())
->method('getUsername')
->willReturn('id');
- $this->assertFalse($user->isEqualTo($other));
+ self::assertFalse((new SecurityUser($this->createUser('id')))->isEqualTo($other));
+ }
+
+ public function testIsEqualToWithOtherOriginUsername(): void
+ {
+ self::assertTrue((new SecurityUser($this->createUser('id')))->isEqualTo(new SecurityUser($this->createUser('id'), 'origin-username')));
+ }
+
+ public function testSerialize(): void
+ {
+ $user = new SecurityUser($this->createUser(new TestUserId('id'), 'password', 'salt'), 'origin-username', ['ROLE_FOO']);
+
+ self::assertEquals($user, unserialize(serialize($user)));
}
- private function createUser(string $id, string $password = null, string $salt = null): User
+ private function createUser($id = null, string $password = null, string $salt = null): User
{
- $userId = $this->createMock(UserIdInterface::class);
- $userId->expects($this->any())
- ->method('toString')
- ->willReturn($id);
+ if ($id instanceof UserIdInterface) {
+ $userId = $id;
+ } else {
+ $userId = $this->createMock(UserIdInterface::class);
+ $userId->expects(self::any())
+ ->method('toString')
+ ->willReturn($id);
+ $userId->expects(self::any())
+ ->method('equals')
+ ->willReturnCallback(function (DomainIdInterface $other) use ($id) {
+ return $id === $other->toString();
+ });
+ $userId->expects(self::any())
+ ->method('isEmpty')
+ ->willReturn(null === $id);
+ }
+
$user = $this->createMock(User::class);
- $user->expects($this->any())
+ $user->expects(self::any())
->method('getId')
->willReturn($userId);
@@ -83,18 +126,26 @@ private function createUser(string $id, string $password = null, string $salt =
$credential = $this->createMock(CredentialInterface::class);
} else {
$credential = $this->createMock([CredentialInterface::class, PasswordProtectedInterface::class]);
- $credential->expects($this->any())
+ $credential->expects(self::any())
->method('getPassword')
->willReturn($password);
- $credential->expects($this->any())
+ $credential->expects(self::any())
->method('getPasswordAlgorithm')
->willReturn(null === $salt ? PasswordAlgorithm::create() : PasswordAlgorithm::createLegacySalted(new PasswordSalt($salt)));
}
- $user->expects($this->any())
+ $credential->expects(self::any())
+ ->method('getUsername')
+ ->willReturn('username');
+
+ $user->expects(self::any())
->method('getCredential')
->willReturn($credential);
return $user;
}
}
+
+class TestUserId extends DomainId implements UserIdInterface
+{
+}
diff --git a/src/User/Tests/Password/PasswordAlgorithmTest.php b/src/User/Tests/Password/PasswordAlgorithmTest.php
index d0c2ede0..83644681 100644
--- a/src/User/Tests/Password/PasswordAlgorithmTest.php
+++ b/src/User/Tests/Password/PasswordAlgorithmTest.php
@@ -40,19 +40,19 @@ public function testCreateLegacyWithSalt(): void
private function assertNonLegacyAlgorithm(int $type, array $options, PasswordAlgorithm $algorithm): void
{
- $this->assertSame($type, $algorithm->type);
- $this->assertSame($options, $algorithm->options);
- $this->assertFalse($algorithm->legacy);
- $this->assertNull($algorithm->salt);
- $this->assertNull($algorithm->encodeBase64);
+ self::assertSame($type, $algorithm->type);
+ self::assertSame($options, $algorithm->options);
+ self::assertFalse($algorithm->legacy);
+ self::assertNull($algorithm->salt);
+ self::assertNull($algorithm->encodeBase64);
}
private function assertLegacyAlgorithm(string $type, ?PasswordSalt $salt, bool $encodeBase64, PasswordAlgorithm $algorithm): void
{
- $this->assertSame($type, $algorithm->type);
- $this->assertNull($algorithm->options);
- $this->assertTrue($algorithm->legacy);
- $this->assertSame($salt, $algorithm->salt);
- $this->assertSame($encodeBase64, $algorithm->encodeBase64);
+ self::assertSame($type, $algorithm->type);
+ self::assertNull($algorithm->options);
+ self::assertTrue($algorithm->legacy);
+ self::assertSame($salt, $algorithm->salt);
+ self::assertSame($encodeBase64, $algorithm->encodeBase64);
}
}
diff --git a/src/User/Tests/Password/PasswordHashingTest.php b/src/User/Tests/Password/PasswordHashingTest.php
index 64e050e2..a1ac7190 100644
--- a/src/User/Tests/Password/PasswordHashingTest.php
+++ b/src/User/Tests/Password/PasswordHashingTest.php
@@ -16,17 +16,17 @@ public function testHash(bool $deprecateLegacyApi): void
{
$hashing = new PasswordHashing($algorithm = self::createAlgorithm(), $deprecateLegacyApi);
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('password'));
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('password', $algorithm));
- $this->assertNotSame($hashing->hash('password', $algorithm), $hashing->hash('password', $algorithm));
- $this->assertNotSame($hashing->hash('password', $algorithm), $hashing->hash('password'));
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('password'));
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('password', $algorithm));
+ self::assertNotSame($hashing->hash('password', $algorithm), $hashing->hash('password', $algorithm));
+ self::assertNotSame($hashing->hash('password', $algorithm), $hashing->hash('password'));
if (!$deprecateLegacyApi) {
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('password', self::createAlgorithm(true)));
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('password', self::createAlgorithm(true)));
}
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('other-password'));
- $this->assertNotSame($hashing->hash('password'), 'password');
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('other-password'));
+ self::assertNotSame($hashing->hash('password'), 'password');
}
/**
@@ -36,19 +36,19 @@ public function testIsValid(bool $deprecateLegacyApi): void
{
$hashing = new PasswordHashing($algorithm = self::createAlgorithm(), $deprecateLegacyApi);
- $this->assertTrue($hashing->isValid($hashing->hash('password'), 'password'));
- $this->assertTrue($hashing->isValid($hashing->hash('password'), 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password'));
+ self::assertTrue($hashing->isValid($hashing->hash('password'), 'password'));
+ self::assertTrue($hashing->isValid($hashing->hash('password'), 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password'));
if (!$deprecateLegacyApi) {
- $this->assertFalse($hashing->isValid($hashing->hash('password'), 'password', self::createAlgorithm(true)));
+ self::assertFalse($hashing->isValid($hashing->hash('password'), 'password', self::createAlgorithm(true)));
}
- $this->assertFalse($hashing->isValid($hashing->hash('password'), $hashing->hash('password')));
- $this->assertFalse($hashing->isValid('password', $hashing->hash('password')));
- $this->assertFalse($hashing->isValid('password', 'password'));
- $this->assertFalse($hashing->isValid($hashing->hash('other-password'), 'password'));
+ self::assertFalse($hashing->isValid($hashing->hash('password'), $hashing->hash('password')));
+ self::assertFalse($hashing->isValid('password', $hashing->hash('password')));
+ self::assertFalse($hashing->isValid('password', 'password'));
+ self::assertFalse($hashing->isValid($hashing->hash('other-password'), 'password'));
}
public function provideLegacyApiModes(): iterable
@@ -61,28 +61,28 @@ public function testHashWithLegacyApi(): void
{
$hashing = new PasswordHashing($algorithm = self::createAlgorithm(true), false);
- $this->assertSame($hashing->hash('password'), $hashing->hash('password'));
- $this->assertSame($hashing->hash('password'), $hashing->hash('password', $algorithm));
- $this->assertSame($hashing->hash('password', $algorithm), $hashing->hash('password', $algorithm));
- $this->assertSame($hashing->hash('password', $algorithm), $hashing->hash('password'));
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('password', self::createAlgorithm()));
- $this->assertNotSame($hashing->hash('password'), $hashing->hash('other-password'));
- $this->assertNotSame($hashing->hash('password'), 'password');
+ self::assertSame($hashing->hash('password'), $hashing->hash('password'));
+ self::assertSame($hashing->hash('password'), $hashing->hash('password', $algorithm));
+ self::assertSame($hashing->hash('password', $algorithm), $hashing->hash('password', $algorithm));
+ self::assertSame($hashing->hash('password', $algorithm), $hashing->hash('password'));
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('password', self::createAlgorithm()));
+ self::assertNotSame($hashing->hash('password'), $hashing->hash('other-password'));
+ self::assertNotSame($hashing->hash('password'), 'password');
}
public function testIsValidWithLegacyApi(): void
{
$hashing = new PasswordHashing($algorithm = self::createAlgorithm(true), false);
- $this->assertTrue($hashing->isValid($hashing->hash('password'), 'password'));
- $this->assertTrue($hashing->isValid($hashing->hash('password'), 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password'));
- $this->assertFalse($hashing->isValid($hashing->hash('password'), 'password', self::createAlgorithm()));
- $this->assertFalse($hashing->isValid($hashing->hash('password'), $hashing->hash('password')));
- $this->assertFalse($hashing->isValid('password', $hashing->hash('password')));
- $this->assertFalse($hashing->isValid('password', 'password'));
- $this->assertFalse($hashing->isValid($hashing->hash('other-password'), 'password'));
+ self::assertTrue($hashing->isValid($hashing->hash('password'), 'password'));
+ self::assertTrue($hashing->isValid($hashing->hash('password'), 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password'));
+ self::assertFalse($hashing->isValid($hashing->hash('password'), 'password', self::createAlgorithm()));
+ self::assertFalse($hashing->isValid($hashing->hash('password'), $hashing->hash('password')));
+ self::assertFalse($hashing->isValid('password', $hashing->hash('password')));
+ self::assertFalse($hashing->isValid('password', 'password'));
+ self::assertFalse($hashing->isValid($hashing->hash('other-password'), 'password'));
}
public function testBase64EncodedAlgorithm(): void
@@ -91,13 +91,13 @@ public function testBase64EncodedAlgorithm(): void
$algorithm = PasswordAlgorithm::createLegacyBase64Encoded('md5');
$algorithm2 = PasswordAlgorithm::createLegacyBase64Encoded('sha1');
- $this->assertNotSame($baseHash = $hashing->hash('password'), $hashing->hash('password', $algorithm));
- $this->assertSame($hash = base64_encode(hex2bin($baseHash)), $hashing->hash('password', $algorithm));
- $this->assertNotSame($hash, $hashing->hash('password', $algorithm2));
+ self::assertNotSame($baseHash = $hashing->hash('password'), $hashing->hash('password', $algorithm));
+ self::assertSame($hash = base64_encode((string) hex2bin($baseHash)), $hashing->hash('password', $algorithm));
+ self::assertNotSame($hash, $hashing->hash('password', $algorithm2));
- $this->assertFalse($hashing->isValid($baseHash, 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hash, 'password', $algorithm));
- $this->assertFalse($hashing->isValid($hash, 'password', $algorithm2));
+ self::assertFalse($hashing->isValid($baseHash, 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hash, 'password', $algorithm));
+ self::assertFalse($hashing->isValid($hash, 'password', $algorithm2));
}
/**
@@ -111,17 +111,17 @@ public function testSaltedAlgorithm(PasswordSalt $otherSalt): void
$algorithmBase64 = PasswordAlgorithm::createLegacySalted($salt, true, 'md5');
$algorithmOtherSalt = PasswordAlgorithm::createLegacySalted($otherSalt, false, 'md5');
- $this->assertNotSame($baseHash = $hashing->hash('password'), $hashing->hash('password', $algorithm));
- $this->assertSame($hash = $hashing->hash('password{token}'), $hashing->hash('password', $algorithm));
- $this->assertNotSame($hash, $hashing->hash('password', $algorithm2));
- $this->assertNotSame($hash, $hashing->hash('password', $algorithmBase64));
- $this->assertNotSame($hash, $hashing->hash('password', $algorithmOtherSalt));
-
- $this->assertFalse($hashing->isValid($baseHash, 'password', $algorithm));
- $this->assertTrue($hashing->isValid($hash, 'password', $algorithm));
- $this->assertFalse($hashing->isValid($hash, 'password', $algorithm2));
- $this->assertFalse($hashing->isValid($hash, 'password', $algorithmBase64));
- $this->assertFalse($hashing->isValid($hash, 'password', $algorithmOtherSalt));
+ self::assertNotSame($baseHash = $hashing->hash('password'), $hashing->hash('password', $algorithm));
+ self::assertSame($hash = $hashing->hash('password{token}'), $hashing->hash('password', $algorithm));
+ self::assertNotSame($hash, $hashing->hash('password', $algorithm2));
+ self::assertNotSame($hash, $hashing->hash('password', $algorithmBase64));
+ self::assertNotSame($hash, $hashing->hash('password', $algorithmOtherSalt));
+
+ self::assertFalse($hashing->isValid($baseHash, 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hash, 'password', $algorithm));
+ self::assertFalse($hashing->isValid($hash, 'password', $algorithm2));
+ self::assertFalse($hashing->isValid($hash, 'password', $algorithmBase64));
+ self::assertFalse($hashing->isValid($hash, 'password', $algorithmOtherSalt));
}
public function provideOtherPasswordSalts(): iterable
@@ -178,7 +178,7 @@ public function testHashWithLegacyAlgorithm(): void
{
$hashing = new PasswordHashing();
- $this->assertNotSame('password', $hashing->hash('password', self::createAlgorithm(true)));
+ self::assertNotSame('password', $hashing->hash('password', self::createAlgorithm(true)));
}
/**
@@ -192,7 +192,7 @@ public function testIsValidhWithLegacyAlgorithm(): void
$hashing = new PasswordHashing();
$algorithm = self::createAlgorithm(true);
- $this->assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
+ self::assertTrue($hashing->isValid($hashing->hash('password', $algorithm), 'password', $algorithm));
}
private static function createAlgorithm(bool $legacy = false): PasswordAlgorithm
diff --git a/src/User/Tests/Role/ChainRoleProviderTest.php b/src/User/Tests/Role/ChainRoleProviderTest.php
new file mode 100644
index 00000000..4bd34e82
--- /dev/null
+++ b/src/User/Tests/Role/ChainRoleProviderTest.php
@@ -0,0 +1,29 @@
+createMock(User::class);
+ $first = $this->createMock(RoleProviderInterface::class);
+ $first->expects(self::once())
+ ->method('getRoles')
+ ->with($user)
+ ->willReturn(['ROLE_USER', 'ROLE_ADMIN']);
+ $second = $this->createMock(RoleProviderInterface::class);
+ $second->expects(self::once())
+ ->method('getRoles')
+ ->with($user)
+ ->willReturn(['ROLE_ADMIN', 'foo' => 'super admin']);
+
+ self::assertSame(['ROLE_USER', 'ROLE_ADMIN', 'super admin'], (new ChainRoleProvider([$first, $second]))->getRoles($user));
+ }
+}
diff --git a/src/User/Tests/Role/DefaultRoleProviderTest.php b/src/User/Tests/Role/DefaultRoleProviderTest.php
new file mode 100644
index 00000000..333623b3
--- /dev/null
+++ b/src/User/Tests/Role/DefaultRoleProviderTest.php
@@ -0,0 +1,26 @@
+ 'ROLE_admin', 'duplicate', 'duplicate']);
+
+ self::assertSame(['ROLE_USER', 'ROLE_admin', 'duplicate'], $provider->getRoles($this->createMock(User::class)));
+ }
+
+ public function testRolesWithoutSanitizing(): void
+ {
+ $provider = new DefaultRoleProvider($expected = ['ROLE_USER', 'foo' => 'ROLE_admin', 'duplicate', 'duplicate'], false);
+
+ self::assertSame($expected, $provider->getRoles($this->createMock(User::class)));
+ }
+}
diff --git a/src/User/Tests/Role/UserRoleProviderTest.php b/src/User/Tests/Role/UserRoleProviderTest.php
new file mode 100644
index 00000000..2418dab3
--- /dev/null
+++ b/src/User/Tests/Role/UserRoleProviderTest.php
@@ -0,0 +1,34 @@
+createMock(User::class);
+ $user->expects(self::once())
+ ->method('getId')
+ ->willReturn($id = $this->createMock(UserIdInterface::class));
+ $userRole = $this->createMock(UserRole::class);
+ $userRole
+ ->method('getRoleName')
+ ->willReturn('ROLE_USER');
+ $repository = $this->createMock(UserRoleRepositoryInterface::class);
+ $repository->expects(self::once())
+ ->method('findAllByUserId')
+ ->with($id)
+ ->willReturn(new DomainCollection(['foo' => $userRole]));
+
+ self::assertSame(['ROLE_USER'], (new UserRoleProvider($repository))->getRoles($user));
+ }
+}
diff --git a/src/User/composer.json b/src/User/composer.json
index ca853001..1f06374f 100644
--- a/src/User/composer.json
+++ b/src/User/composer.json
@@ -2,8 +2,8 @@
"name": "msgphp/user",
"type": "library",
"license": "MIT",
- "description": "User domain layer",
- "keywords": ["user", "security"],
+ "description": "A domain layer providing basic user management",
+ "keywords": ["ddd", "domain-driven-design", "rich domain model", "user", "security"],
"authors": [
{
"name": "Roland Franssen",
@@ -18,21 +18,24 @@
"exclude-from-classmap": ["/Tests/"]
},
"require": {
- "msgphp/domain": "^0.2|^1.0"
+ "msgphp/domain": "^0.8"
},
"require-dev": {
"doctrine/orm": "^2.6",
- "lexik/jwt-authentication-bundle": "dev-master",
- "msgphp/eav": "^0.2",
+ "lexik/jwt-authentication-bundle": "^2.5",
+ "msgphp/eav": "^0.8",
"sensio/framework-extra-bundle": "^5.1",
"symfony/console": "^3.4|^4.0",
"symfony/form": "^3.4|^4.0",
"symfony/http-kernel": "^3.4|^4.0",
- "symfony/phpunit-bridge": "^3.4|^4.0",
+ "symfony/phpunit-bridge": "^3.4.9|^4.0.9",
"symfony/security-core": "^3.4|^4.0",
"symfony/validator": "^3.4|^4.0",
"symfony/var-dumper": "^3.4|^4.0"
},
+ "suggest": {
+ "msgphp/eav": "To enable user attribute values"
+ },
"config": {
"preferred-install": {
"*": "dist"
@@ -41,7 +44,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "0.8-dev"
}
}
}
diff --git a/src/UserBundle/DependencyInjection/Compiler/CleanupPass.php b/src/UserBundle/DependencyInjection/Compiler/CleanupPass.php
new file mode 100644
index 00000000..abaa6767
--- /dev/null
+++ b/src/UserBundle/DependencyInjection/Compiler/CleanupPass.php
@@ -0,0 +1,73 @@
+
+ *
+ * @internal
+ */
+final class CleanupPass implements CompilerPassInterface
+{
+ public function process(ContainerBuilder $container): void
+ {
+ ContainerHelper::removeIf($container, !$container->has(Repository\RoleRepositoryInterface::class), [
+ Command\Handler\CreateRoleHandler::class,
+ Command\Handler\DeleteRoleHandler::class,
+ ConsoleInfra\Command\AddUserRoleCommand::class,
+ ConsoleInfra\Command\CreateRoleCommand::class,
+ ConsoleInfra\Command\DeleteRoleCommand::class,
+ ConsoleInfra\Command\DeleteUserRoleCommand::class,
+ ]);
+ ContainerHelper::removeIf($container, !$container->has(Repository\UserRepositoryInterface::class), [
+ Command\Handler\ChangeUserCredentialHandler::class,
+ Command\Handler\ConfirmUserHandler::class,
+ Command\Handler\CreateUserHandler::class,
+ Command\Handler\DeleteUserHandler::class,
+ Command\Handler\DisableUserHandler::class,
+ Command\Handler\EnableUserHandler::class,
+ Command\Handler\RequestUserPasswordHandler::class,
+ ConsoleInfra\Command\AddUserRoleCommand::class,
+ ConsoleInfra\Command\ChangeUserCredentialCommand::class,
+ ConsoleInfra\Command\ConfirmUserCommand::class,
+ ConsoleInfra\Command\CreateUserCommand::class,
+ ConsoleInfra\Command\DeleteUserCommand::class,
+ ConsoleInfra\Command\DeleteUserRoleCommand::class,
+ ConsoleInfra\Command\DisableUserCommand::class,
+ ConsoleInfra\Command\EnableUserCommand::class,
+ SecurityInfra\Jwt\SecurityUserProvider::class,
+ SecurityInfra\SecurityUserProvider::class,
+ SecurityInfra\UserParamConverter::class,
+ SecurityInfra\UserArgumentValueResolver::class,
+ ValidatorInfra\ExistingUsernameValidator::class,
+ ValidatorInfra\UniqueUsernameValidator::class,
+ ]);
+ ContainerHelper::removeIf($container, !$container->has(Repository\UsernameRepositoryInterface::class), [
+ ConsoleInfra\Command\SynchronizeUsernamesCommand::class,
+ ]);
+ ContainerHelper::removeIf($container, !$container->has(Repository\UserAttributeValueRepositoryInterface::class), [
+ Command\Handler\AddUserAttributeValueHandler::class,
+ Command\Handler\ChangeUserAttributeValueHandler::class,
+ Command\Handler\DeleteUserAttributeValueHandler::class,
+ ]);
+ ContainerHelper::removeIf($container, !$container->has(Repository\UserEmailRepositoryInterface::class), [
+ Command\Handler\AddUserEmailHandler::class,
+ Command\Handler\DeleteUserEmailHandler::class,
+ ]);
+ ContainerHelper::removeIf($container, !$container->has(Repository\UserRoleRepositoryInterface::class), [
+ Command\Handler\AddUserRoleHandler::class,
+ Command\Handler\DeleteUserRoleHandler::class,
+ ConsoleInfra\Command\AddUserRoleCommand::class,
+ ConsoleInfra\Command\DeleteUserRoleCommand::class,
+ Role\UserRoleProvider::class,
+ ]);
+ }
+}
diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php
index 83d76d09..eff52149 100644
--- a/src/UserBundle/DependencyInjection/Configuration.php
+++ b/src/UserBundle/DependencyInjection/Configuration.php
@@ -6,10 +6,11 @@
use MsgPhp\Domain\DomainIdInterface;
use MsgPhp\Domain\Entity\Features;
+use MsgPhp\Domain\Event\DomainEventHandlerInterface;
use MsgPhp\Domain\Infra\Config\{NodeBuilder, TreeBuilder};
use MsgPhp\Domain\Infra\DependencyInjection\ConfigHelper;
use MsgPhp\User\{Command, CredentialInterface, Entity, UserId, UserIdInterface};
-use MsgPhp\User\Infra\Uuid as UuidInfra;
+use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Uuid as UuidInfra};
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
@@ -24,7 +25,7 @@ final class Configuration implements ConfigurationInterface
];
public const IDENTITY_MAPPING = [
Entity\Role::class => ['name'],
- 'MsgPhp\\User\\Entity\\UserAttributeValue' => ['attributeValue'],
+ Entity\UserAttributeValue::class => ['attributeValue'],
Entity\User::class => ['id'],
Entity\Username::class => ['username'],
Entity\UserRole::class => ['user', 'role'],
@@ -36,10 +37,37 @@ final class Configuration implements ConfigurationInterface
public const UUID_CLASS_MAPPING = [
UserIdInterface::class => UuidInfra\UserId::class,
];
+ public const DOCTRINE_TYPE_MAPPING = [
+ UserIdInterface::class => DoctrineInfra\Type\UserIdType::class,
+ ];
+ public const DOCTRINE_REPOSITORY_MAPPING = [
+ Entity\Role::class => DoctrineInfra\Repository\RoleRepository::class,
+ Entity\User::class => DoctrineInfra\Repository\UserRepository::class,
+ Entity\Username::class => DoctrineInfra\Repository\UsernameRepository::class,
+ Entity\UserAttributeValue::class => DoctrineInfra\Repository\UserAttributeValueRepository::class,
+ Entity\UserRole::class => DoctrineInfra\Repository\UserRoleRepository::class,
+ Entity\UserEmail::class => DoctrineInfra\Repository\UserEmailRepository::class,
+ ];
+ public const CONSOLE_COMMAND_MAPPING = [
+ Command\AddUserRoleCommand::class => [ConsoleInfra\Command\AddUserRoleCommand::class],
+ Command\ChangeUserCredentialCommand::class => [ConsoleInfra\Command\ChangeUserCredentialCommand::class],
+ Command\ConfirmUserCommand::class => [ConsoleInfra\Command\ConfirmUserCommand::class],
+ Command\CreateRoleCommand::class => [ConsoleInfra\Command\CreateRoleCommand::class],
+ Command\CreateUserCommand::class => [ConsoleInfra\Command\CreateUserCommand::class],
+ Command\DeleteRoleCommand::class => [ConsoleInfra\Command\DeleteRoleCommand::class],
+ Command\DeleteUserCommand::class => [ConsoleInfra\Command\DeleteUserCommand::class],
+ Command\DeleteUserRoleCommand::class => [ConsoleInfra\Command\DeleteUserRoleCommand::class],
+ Command\DisableUserCommand::class => [ConsoleInfra\Command\DisableUserCommand::class],
+ Command\EnableUserCommand::class => [ConsoleInfra\Command\EnableUserCommand::class],
+ ];
private const COMMAND_MAPPING = [
+ Entity\Role::class => [
+ Command\CreateRoleCommand::class,
+ Command\DeleteRoleCommand::class,
+ ],
Entity\User::class => [
- Command\CreateUserCommand::class => true,
- Command\DeleteUserCommand::class => true,
+ Command\CreateUserCommand::class,
+ Command\DeleteUserCommand::class,
Features\CanBeConfirmed::class => [
Command\ConfirmUserCommand::class,
@@ -52,24 +80,32 @@ final class Configuration implements ConfigurationInterface
Command\RequestUserPasswordCommand::class,
],
],
- 'MsgPhp\\User\\Entity\\UserAttributeValue' => [
- Command\AddUserAttributeValueCommand::class => true,
- Command\ChangeUserAttributeValueCommand::class => true,
- Command\DeleteUserAttributeValueCommand::class => true,
+ Entity\UserAttributeValue::class => [
+ Command\AddUserAttributeValueCommand::class,
+ Command\ChangeUserAttributeValueCommand::class,
+ Command\DeleteUserAttributeValueCommand::class,
],
Entity\UserEmail::class => [
- Command\AddUserEmailCommand::class => true,
- Command\DeleteUserEmailCommand::class => true,
+ Command\AddUserEmailCommand::class,
+ Command\DeleteUserEmailCommand::class,
Features\CanBeConfirmed::class => [
Command\ConfirmUserEmailCommand::class,
],
],
Entity\UserRole::class => [
- Command\AddUserRoleCommand::class => true,
- Command\DeleteUserRoleCommand::class => true,
+ Command\AddUserRoleCommand::class,
+ Command\DeleteUserRoleCommand::class,
],
];
+ private const DEFAULT_ROLE = 'ROLE_USER';
+
+ private static $packageDir;
+
+ public static function getPackageDir(): string
+ {
+ return self::$packageDir ?? (self::$packageDir = \dirname((string) (new \ReflectionClass(UserIdInterface::class))->getFileName()));
+ }
public function getConfigTreeBuilder(): TreeBuilder
{
@@ -79,7 +115,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$children
->classMappingNode('class_mapping')
->requireClasses([Entity\User::class])
- ->disallowClasses([CredentialInterface::class, Entity\Username::class])
+ ->disallowClasses([CredentialInterface::class])
->groupClasses([Entity\Role::class, Entity\UserRole::class])
->subClassValues()
->end()
@@ -102,15 +138,70 @@ public function getConfigTreeBuilder(): TreeBuilder
->cannotBeEmpty()
->validate()
->ifTrue(function ($value): bool {
- return Entity\Username::class === $value;
+ return !class_exists($value);
})
- ->thenInvalid('Target %s is not applicable.')
+ ->thenInvalid('Target class %s does not exists.')
->end()
->end()
->scalarNode('field')->isRequired()->cannotBeEmpty()->end()
- ->scalarNode('mapped_by')->defaultValue('user')->cannotBeEmpty()->end()
+ ->scalarNode('mapped_by')->defaultNull()->end()
->end()
->end()
+ ->validate()
+ ->always(function (array $value): array {
+ $result = [];
+ foreach ($value as $lookup) {
+ ['target' => $target, 'field' => $field, 'mapped_by' => $mappedBy] = $lookup;
+ if (Entity\Username::class === $target || is_subclass_of($target, Entity\Username::class)) {
+ throw new \LogicException(sprintf('Lookup target "%s" is not applicable.', (string) $target));
+ }
+ if (null === $mappedBy && !is_subclass_of($target, Entity\User::class)) {
+ throw new \LogicException(sprintf('Lookup for target "%s" must be a sub class of "%s" or specify the "mapped_by" node.', $target, Entity\User::class));
+ }
+
+ $result[$target][$field] = $mappedBy;
+ }
+
+ return $result;
+ })
+ ->end()
+ ->end()
+ ->arrayNode('role_providers')
+ ->defaultValue(['default' => [self::DEFAULT_ROLE]])
+ ->requiresAtLeastOneElement()
+ ->beforeNormalization()
+ ->always(function ($value) {
+ if (\is_array($value)) {
+ if (!isset($value['default'])) {
+ $value['default'] = [self::DEFAULT_ROLE];
+ } elseif (false === $value['default']) {
+ $value['default'] = [];
+ } elseif (!\is_array($value['default'])) {
+ throw new \LogicException(sprintf('Default role provider must be of type array or false, got "%s".', \gettype($value['default'])));
+ }
+ }
+
+ return $value;
+ })
+ ->end()
+ ->validate()
+ ->always(function (array $value): array {
+ foreach ($value as $k => $v) {
+ if ('default' !== $k && !\is_string($v)) {
+ throw new \LogicException(sprintf('Role provider must be of type string, got "%s".', \gettype($v)));
+ }
+ }
+
+ return $value;
+ })
+ ->end()
+ ->variablePrototype()->end()
+ ->end()
+ ->arrayNode('doctrine')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('auto_sync_username')->defaultTrue()->end()
+ ->end()
->end()
->end()
->validate()
@@ -121,43 +212,18 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->validate()
->always(function (array $config): array {
- $usernameLookup = [];
- foreach ($config['username_lookup'] as &$value) {
- if (isset($config['class_mapping'][$value['target']])) {
- $value['target'] = $config['class_mapping'][$value['target']];
- }
-
- if (isset($usernameLookup[$value['target']]) && in_array($value, $usernameLookup[$value['target']], true)) {
- throw new \LogicException(sprintf('Duplicate username lookup mapping for "%s".', $value['target']));
- }
-
- $usernameLookup[$value['target']][] = $value;
- }
- unset($value);
-
$userCredential = self::getUserCredential($userClass = $config['class_mapping'][Entity\User::class]);
+ $config['username_field'] = $userCredential['username_field'];
+ $config['class_mapping'][CredentialInterface::class] = $userCredential['class'];
+ $config['commands'][Command\ChangeUserCredentialCommand::class] = isset($config['username_field']) ? is_subclass_of($userClass, DomainEventHandlerInterface::class) : false;
- if ($usernameLookup) {
- if (isset($usernameLookup[$userClass])) {
- throw new \LogicException(sprintf('Username lookup mapping for "%s" cannot be overwritten.', $userClass));
+ if ($config['username_lookup']) {
+ if (!isset($config['class_mapping'][Entity\Username::class])) {
+ throw new \LogicException(sprintf('Configuring "username_lookup" requires the "%s" entity to be mapped under "class_mapping".', Entity\Username::class));
}
-
- if (isset($userCredential['username_field'])) {
- $usernameLookup[$userClass][] = ['target' => $userClass, 'field' => $userCredential['username_field']];
+ if (isset($config['username_field'])) {
+ $config['username_lookup'][$userClass][$config['username_field']] = null;
}
-
- $config['class_mapping'][Entity\Username::class] = Entity\Username::class;
- }
-
- if (isset($userCredential['class'])) {
- $config['class_mapping'][CredentialInterface::class] = $userCredential['class'];
- }
-
- $config['username_field'] = $userCredential['username_field'];
- $config['username_lookup'] = $usernameLookup;
-
- if (null !== $userCredential['username_field'] && !isset($config['commands'][Command\ChangeUserCredentialCommand::class])) {
- $config['commands'][Command\ChangeUserCredentialCommand::class] = true;
}
ConfigHelper::resolveCommandMappingConfig(self::COMMAND_MAPPING, $config['class_mapping'], $config['commands']);
@@ -171,15 +237,27 @@ public function getConfigTreeBuilder(): TreeBuilder
private static function getUserCredential(string $userClass): array
{
- if (null === $credential = (new \ReflectionMethod($userClass, 'getCredential'))->getReturnType()) {
+ $reflection = new \ReflectionMethod($userClass, 'getCredential');
+
+ if (Entity\User::class === $reflection->getDeclaringClass()->getName()) {
return ['class' => Entity\Credential\Anonymous::class, 'username_field' => null];
}
- if ($credential->isBuiltin() || !is_subclass_of($class = $credential->getName(), CredentialInterface::class)) {
- throw new \LogicException(sprintf('Method "%s::getCredential()" must return a sub class of "%s", got "%s".', $userClass, CredentialInterface::class, $credential->getName()));
+ $type = $reflection->getReturnType();
+
+ if (null === $type) {
+ throw new \LogicException(sprintf('Method "%s::getCredential()" must have a return type set.', $userClass));
+ }
+
+ if (Entity\Credential\Anonymous::class === $class = $type->getName()) {
+ return ['class' => $class, 'username_field' => null];
+ }
+
+ if ($type->isBuiltin() || !is_subclass_of($class, CredentialInterface::class)) {
+ throw new \LogicException(sprintf('Method "%s::getCredential()" must return a sub class of "%s", got "%s".', $userClass, CredentialInterface::class, $class));
}
- if ($credential->allowsNull()) {
+ if ($type->allowsNull()) {
throw new \LogicException(sprintf('Method "%s::getCredential()" cannot be null-able.', $userClass));
}
diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php
index e6384ac2..b4430972 100644
--- a/src/UserBundle/DependencyInjection/Extension.php
+++ b/src/UserBundle/DependencyInjection/Extension.php
@@ -4,31 +4,21 @@
namespace MsgPhp\UserBundle\DependencyInjection;
-use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
-use Doctrine\ORM\Version as DoctrineOrmVersion;
-use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
-use MsgPhp\Domain\Infra\Console as BaseConsoleInfra;
-use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
-use MsgPhp\EavBundle\MsgPhpEavBundle;
-use MsgPhp\User\{Command, CredentialInterface, Entity, Repository, UserIdInterface};
-use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Validator as ValidatorInfra};
-use MsgPhp\UserBundle\Twig\GlobalVariables;
-use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle;
-use Symfony\Bundle\SecurityBundle\SecurityBundle;
-use Symfony\Bundle\TwigBundle\TwigBundle;
+use MsgPhp\Domain\Infra\Console\Context\ClassContextFactory as ConsoleClassContextFactory;
+use MsgPhp\Domain\Infra\DependencyInjection\{ContainerHelper, ExtensionHelper, FeatureDetection};
+use MsgPhp\User\{CredentialInterface, Entity, Repository, Role};
+use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra};
+use MsgPhp\UserBundle\Twig;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
-use Symfony\Component\Console\ConsoleEvents;
+use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension as BaseExtension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
-use Symfony\Component\Form\Form;
-use Symfony\Component\Validator\Validation;
-use Twig\Environment as Twig;
/**
* @author Roland Franssen
@@ -51,147 +41,67 @@ public function getConfiguration(array $config, ContainerBuilder $container): Co
public function load(array $configs, ContainerBuilder $container): void
{
- $loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
+ $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
$config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);
+ ExtensionHelper::configureDomain($container, $config['class_mapping'], Configuration::AGGREGATE_ROOTS, Configuration::IDENTITY_MAPPING);
+
+ // default infra
$loader->load('services.php');
- ContainerHelper::configureIdentityMapping($container, $config['class_mapping'], Configuration::IDENTITY_MAPPING);
- ContainerHelper::configureEntityFactory($container, $config['class_mapping'], Configuration::AGGREGATE_ROOTS);
- ContainerHelper::configureDoctrineOrmMapping($container, self::getDoctrineMappingFiles($config, $container), [DoctrineInfra\EntityFieldsMapping::class]);
+ // message infra
+ $loader->load('message.php');
+ ExtensionHelper::finalizeCommandHandlers($container, $config['class_mapping'], $config['commands'], array_map(function (string $file): string {
+ return 'MsgPhp\\User\\Event\\'.basename($file, '.php');
+ }, glob(Configuration::getPackageDir().'/Event/*Event.php')));
// persistence infra
- if (class_exists(DoctrineOrmVersion::class) && ContainerHelper::hasBundle($container, DoctrineBundle::class)) {
- $this->prepareDoctrineOrm($config, $loader, $container);
- }
-
- // message infra
- if (ContainerHelper::hasBundle($container, SimpleBusCommandBusBundle::class)) {
- $loader->load('message.php');
-
- ContainerHelper::removeIf($container, !$container->has(Repository\UserRepositoryInterface::class), [
- Command\Handler\ChangeUserCredentialHandler::class,
- Command\Handler\ConfirmUserHandler::class,
- Command\Handler\CreateUserHandler::class,
- Command\Handler\DeleteUserHandler::class,
- Command\Handler\DisableUserHandler::class,
- Command\Handler\EnableUserHandler::class,
- Command\Handler\RequestUserPasswordHandler::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Repository\UserAttributeValueRepositoryInterface::class), [
- Command\Handler\AddUserAttributeValueHandler::class,
- Command\Handler\ChangeUserAttributeValueHandler::class,
- Command\Handler\DeleteUserAttributeValueHandler::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Repository\UserEmailRepositoryInterface::class), [
- Command\Handler\AddUserEmailHandler::class,
- Command\Handler\DeleteUserEmailHandler::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Repository\UserRoleRepositoryInterface::class), [
- Command\Handler\AddUserRoleHandler::class,
- Command\Handler\DeleteUserRoleHandler::class,
- ]);
- ContainerHelper::configureCommandMessages($container, $config['class_mapping'], $config['commands']);
- ContainerHelper::configureEventMessages($container, $config['class_mapping'], array_map(function (string $file): string {
- return 'MsgPhp\\User\\Event\\'.basename($file, '.php');
- }, glob(dirname(ContainerHelper::getClassReflection($container, UserIdInterface::class)->getFileName()).'/Event/*Event.php')));
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ $this->loadDoctrineOrm($config, $loader, $container);
}
// framework infra
- if (ContainerHelper::hasBundle($container, SecurityBundle::class)) {
- $loader->load('security.php');
-
- ContainerHelper::removeIf($container, !$container->has(Repository\UserRepositoryInterface::class), [
- SecurityInfra\Jwt\SecurityUserProvider::class,
- SecurityInfra\SecurityUserProvider::class,
- SecurityInfra\UserParamConverter::class,
- SecurityInfra\UserValueResolver::class,
- ]);
+ if (FeatureDetection::isConsoleAvailable($container)) {
+ $this->loadConsole($config, $loader, $container);
}
- if (class_exists(Form::class)) {
+ if (FeatureDetection::isFormAvailable($container)) {
$loader->load('form.php');
}
- if (class_exists(Validation::class)) {
+ if (FeatureDetection::isValidatorAvailable($container)) {
$loader->load('validator.php');
+ }
- ContainerHelper::removeIf($container, !$container->has(Repository\UserRepositoryInterface::class), [
- ValidatorInfra\ExistingUsernameValidator::class,
- ValidatorInfra\UniqueUsernameValidator::class,
- ]);
+ if (FeatureDetection::hasSecurityBundle($container)) {
+ $loader->load('security.php');
}
- if (class_exists(Twig::class)) {
+ if (FeatureDetection::hasTwigBundle($container)) {
$loader->load('twig.php');
}
- if (class_exists(ConsoleEvents::class)) {
- $loader->load('console.php');
-
- $container->getDefinition(ConsoleInfra\Command\AddUserRoleCommand::class)
- ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory(
- $container,
- $config['class_mapping'][Entity\UserRole::class] ?? Entity\UserRole::class,
- BaseConsoleInfra\Context\ClassContextFactory::REUSE_DEFINITION
- ));
-
- $container->getDefinition(ConsoleInfra\Command\CreateUserCommand::class)
- ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory(
- $container,
- $config['class_mapping'][Entity\User::class]
- ));
-
- $container->getDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class)
- ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory(
- $container,
- $config['class_mapping'][CredentialInterface::class],
- BaseConsoleInfra\Context\ClassContextFactory::ALWAYS_OPTIONAL | BaseConsoleInfra\Context\ClassContextFactory::NO_DEFAULTS
- ));
-
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\AddUserRoleHandler::class), [
- ConsoleInfra\Command\AddUserRoleCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\ChangeUserCredentialHandler::class), [
- ConsoleInfra\Command\ChangeUserCredentialCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\ConfirmUserHandler::class), [
- ConsoleInfra\Command\ConfirmUserCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\CreateUserHandler::class), [
- ConsoleInfra\Command\CreateUserCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\DeleteUserHandler::class), [
- ConsoleInfra\Command\DeleteUserCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\DeleteUserRoleHandler::class), [
- ConsoleInfra\Command\DeleteUserRoleCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\DisableUserHandler::class), [
- ConsoleInfra\Command\DisableUserCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Command\Handler\EnableUserHandler::class), [
- ConsoleInfra\Command\EnableUserCommand::class,
- ]);
- ContainerHelper::removeIf($container, !$container->has(Repository\UsernameRepositoryInterface::class), [
- ConsoleInfra\Command\SynchronizeUsernamesCommand::class,
- ]);
- }
+ $this->configureRoleProvider($config, $container);
}
public function prepend(ContainerBuilder $container): void
{
$config = $this->processConfiguration($this->getConfiguration($configs = $container->getExtensionConfig($this->getAlias()), $container), $configs);
- ContainerHelper::configureDoctrineTypes($container, $config['class_mapping'], $config['id_type_mapping'], [
- UserIdInterface::class => DoctrineInfra\Type\UserIdType::class,
- ]);
- ContainerHelper::configureDoctrineOrmTargetEntities($container, $config['class_mapping']);
+ if (FeatureDetection::isDoctrineOrmAvailable($container)) {
+ ExtensionHelper::configureDoctrineOrm(
+ $container,
+ $config['class_mapping'],
+ $config['id_type_mapping'],
+ Configuration::DOCTRINE_TYPE_MAPPING,
+ self::getDoctrineMappingFiles($config, $container)
+ );
+ }
- if (ContainerHelper::hasBundle($container, TwigBundle::class)) {
+ if (FeatureDetection::hasTwigBundle($container)) {
$container->prependExtensionConfig('twig', [
'globals' => [
- 'msgphp_user' => '@'.GlobalVariables::class,
+ Twig\GlobalVariable::NAME => '@'.Twig\GlobalVariable::class,
],
]);
}
@@ -199,56 +109,122 @@ public function prepend(ContainerBuilder $container): void
public function process(ContainerBuilder $container): void
{
- if ($container->hasDefinition('data_collector.security')) {
- $container->getDefinition('data_collector.security')
+ if (FeatureDetection::hasSecurityBundle($container) && $container->hasDefinition($id = 'data_collector.security')) {
+ $container->getDefinition($id)
->setClass(SecurityInfra\DataCollector::class)
- ->setArgument('$repository', new Reference(Repository\UserRepositoryInterface::class, ContainerBuilder::NULL_ON_INVALID_REFERENCE))
- ->setArgument('$factory', new Reference(EntityAwareFactoryInterface::class, ContainerBuilder::NULL_ON_INVALID_REFERENCE));
+ ->setArgument('$repository', new Reference(Repository\UserRepositoryInterface::class, ContainerBuilder::NULL_ON_INVALID_REFERENCE));
}
}
- private function prepareDoctrineOrm(array $config, LoaderInterface $loader, ContainerBuilder $container): void
+ private static function getDoctrineMappingFiles(array $config, ContainerBuilder $container): array
{
- $loader->load('doctrine.php');
+ $baseDir = Configuration::getPackageDir().'/Infra/Doctrine/Resources/dist-mapping';
+ $files = array_flip(glob($baseDir.'/*.orm.xml'));
- if (null !== $config['username_field']) {
- $container->getDefinition(DoctrineInfra\Repository\UserRepository::class)
- ->setArgument('$usernameField', $config['username_field']);
+ if (!isset($config['class_mapping'][Entity\Role::class])) {
+ unset($files[$baseDir.'/User.Entity.Role.orm.xml'], $files[$baseDir.'/User.Entity.UserRole.orm.xml']);
}
- if ($config['username_lookup']) {
- $container->getDefinition(DoctrineInfra\Event\UsernameListener::class)
- ->setArgument('$mapping', $config['username_lookup']);
+ if (!isset($config['class_mapping'][Entity\UserEmail::class])) {
+ unset($files[$baseDir.'/User.Entity.UserEmail.orm.xml']);
+ }
+
+ if (!isset($config['class_mapping'][Entity\Username::class])) {
+ unset($files[$baseDir.'/User.Entity.Username.orm.xml']);
+ }
- $container->getDefinition(DoctrineInfra\Repository\UsernameRepository::class)
- ->setArgument('$targetMapping', $config['username_lookup']);
+ if (!FeatureDetection::hasMsgPhpEavBundle($container)) {
+ unset($files[$baseDir.'/User.Entity.UserAttributeValue.orm.xml']);
+ }
+
+ return array_keys($files);
+ }
+
+ private function loadDoctrineOrm(array $config, LoaderInterface $loader, ContainerBuilder $container): void
+ {
+ $loader->load('doctrine.php');
+
+ $container->getDefinition(DoctrineInfra\Repository\UserRepository::class)
+ ->setArgument('$usernameField', $config['username_field'])
+ ->setArgument('$usernameClass', $config['username_lookup'] ? $config['class_mapping'][Entity\Username::class] : null);
+
+ $container->getDefinition(DoctrineInfra\Repository\UsernameRepository::class)
+ ->setArgument('$targetMappings', $config['username_lookup'])
+ ->addTag('msgphp.domain.process_class_mapping', ['argument' => '$targetMappings', 'array_keys' => true]);
+
+ if ($config['doctrine']['auto_sync_username']) {
+ $container->getDefinition(DoctrineInfra\Event\UsernameListener::class)
+ ->setArgument('$targetMappings', $config['username_lookup'])
+ ->addTag('msgphp.domain.process_class_mapping', ['argument' => '$targetMappings', 'array_keys' => true]);
} else {
$container->removeDefinition(DoctrineInfra\Event\UsernameListener::class);
}
- ContainerHelper::configureDoctrineOrmRepositories($container, $config['class_mapping'], [
- DoctrineInfra\Repository\RoleRepository::class => Entity\Role::class,
- DoctrineInfra\Repository\UserRepository::class => Entity\User::class,
- DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class,
- DoctrineInfra\Repository\UserAttributeValueRepository::class => 'MsgPhp\\User\\Entity\\UserAttributeValue',
- DoctrineInfra\Repository\UserRoleRepository::class => Entity\UserRole::class,
- DoctrineInfra\Repository\UserEmailRepository::class => Entity\UserEmail::class,
- ]);
+ ExtensionHelper::finalizeDoctrineOrmRepositories($container, $config['class_mapping'], Configuration::DOCTRINE_REPOSITORY_MAPPING);
}
- private static function getDoctrineMappingFiles(array $config, ContainerBuilder $container): array
+ private function loadConsole(array $config, LoaderInterface $loader, ContainerBuilder $container): void
{
- $baseDir = dirname(ContainerHelper::getClassReflection($container, UserIdInterface::class)->getFileName()).'/Infra/Doctrine/Resources/dist-mapping';
- $files = array_flip(glob($baseDir.'/*.orm.xml'));
+ $loader->load('console.php');
- if (!ContainerHelper::hasBundle($container, MsgPhpEavBundle::class)) {
- unset($files[$baseDir.'/User.Entity.UserAttributeValue.orm.xml']);
+ $container->getDefinition(ConsoleInfra\Command\CreateUserCommand::class)
+ ->setArgument('$contextFactory', ExtensionHelper::registerConsoleClassContextFactory(
+ $container,
+ $config['class_mapping'][Entity\User::class]
+ ));
+
+ if (isset($config['class_mapping'][Entity\Role::class])) {
+ $container->getDefinition(ConsoleInfra\Command\CreateRoleCommand::class)
+ ->setArgument('$contextFactory', ExtensionHelper::registerConsoleClassContextFactory(
+ $container,
+ $config['class_mapping'][Entity\Role::class]
+ ));
}
- if (!$config['username_lookup']) {
- unset($files[$baseDir.'/User.Entity.Username.orm.xml']);
+ if (isset($config['class_mapping'][Entity\UserRole::class])) {
+ $container->getDefinition(ConsoleInfra\Command\AddUserRoleCommand::class)
+ ->setArgument('$contextFactory', ExtensionHelper::registerConsoleClassContextFactory(
+ $container,
+ $config['class_mapping'][Entity\UserRole::class],
+ ConsoleClassContextFactory::REUSE_DEFINITION
+ ));
}
- return array_keys($files);
+ if (isset($config['username_field'])) {
+ $container->getDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class)
+ ->setArgument('$contextFactory', ExtensionHelper::registerConsoleClassContextFactory(
+ $container,
+ $config['class_mapping'][CredentialInterface::class],
+ ConsoleClassContextFactory::ALWAYS_OPTIONAL | ConsoleClassContextFactory::NO_DEFAULTS
+ ));
+ }
+
+ ExtensionHelper::finalizeConsoleCommands($container, $config['commands'], Configuration::CONSOLE_COMMAND_MAPPING);
+ }
+
+ private function configureRoleProvider(array $config, ContainerBuilder $container): void
+ {
+ if (isset($config['class_mapping'][Entity\UserRole::class])) {
+ $userProvider = $container->autowire(Role\UserRoleProvider::class);
+ $userProvider->setPublic(false);
+ }
+
+ $providers = [];
+ foreach ($config['role_providers'] as $factory => $provider) {
+ if ('default' === $factory) {
+ if (!$provider) {
+ continue;
+ }
+
+ $defaultProvider = ContainerHelper::registerAnonymous($container, Role\DefaultRoleProvider::class, false, $defaultProviderId);
+ $defaultProvider->setArgument('$roles', $provider);
+ $provider = $defaultProviderId;
+ }
+
+ $providers[] = new Reference($provider);
+ }
+
+ $container->getDefinition(Role\ChainRoleProvider::class)
+ ->setArgument('$providers', new IteratorArgument($providers));
}
}
diff --git a/src/UserBundle/Maker/UserMaker.php b/src/UserBundle/Maker/UserMaker.php
new file mode 100644
index 00000000..253a1385
--- /dev/null
+++ b/src/UserBundle/Maker/UserMaker.php
@@ -0,0 +1,679 @@
+
+ *
+ * @internal
+ */
+final class UserMaker implements MakerInterface
+{
+ private $classMapping;
+ private $projectDir;
+ private $credential;
+ private $passwordReset = false;
+ private $configs = [];
+ private $services = [];
+ private $routes = [];
+ private $writes = [];
+
+ /** @var \ReflectionClass */
+ private $user;
+
+ public function __construct(array $classMapping, string $projectDir)
+ {
+ $this->classMapping = $classMapping;
+ $this->projectDir = $projectDir;
+ }
+
+ public static function getCommandName(): string
+ {
+ return 'make:user:msgphp';
+ }
+
+ public function configureCommand(Command $command, InputConfiguration $inputConfig): void
+ {
+ }
+
+ public function configureDependencies(DependencyBuilder $dependencies): void
+ {
+ }
+
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
+ {
+ }
+
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
+ {
+ $this->credential = $this->user = null;
+ $this->passwordReset = false;
+ $this->configs = $this->services = $this->routes = $this->writes = [];
+
+ if (!isset($this->classMapping[Entity\User::class])) {
+ throw new \LogicException('User class not configured. Did you install the bundle using Symfony Recipes?');
+ }
+
+ if (!interface_exists(EntityManagerInterface::class)) {
+ $io->note(['It\'s recommended to enable Doctrine ORM, run:', 'composer require orm']);
+
+ if (!$io->confirm('Continue anyway?')) {
+ return;
+ }
+ }
+
+ if (!interface_exists(MessageBusInterface::class)) {
+ $io->note(['It\'s recommended to enable Symfony Messenger, run:', 'composer require messenger']);
+
+ if (!$io->confirm('Continue anyway?')) {
+ return;
+ }
+ }
+
+ if ($io->confirm('Enable Symfony Messenger configuration? (config/packages/messenger.yaml)')) {
+ $this->writes[] = [$this->projectDir.'/config/packages/messenger.yaml', self::getSkeleton('messenger.php')];
+ }
+
+ $this->user = new \ReflectionClass($this->classMapping[Entity\User::class]);
+
+ $this->generateUser($io);
+ $this->generateControllers($io);
+ $this->generateConsole($io);
+
+ if ($this->configs || $this->services) {
+ array_unshift($this->writes, [$this->projectDir.'/config/packages/msgphp_user.make.php', self::getSkeleton('config.php', [
+ 'config' => $this->configs ? var_export(array_merge_recursive(...$this->configs), true) : null,
+ 'services' => $this->services,
+ ])]);
+ $this->configs = $this->services = [];
+ }
+
+ if ($this->routes) {
+ array_unshift($this->writes, [$this->projectDir.'/config/routes/user.php', self::getSkeleton('routes.php', [
+ 'routes' => $this->routes,
+ ])]);
+ $this->routes = [];
+ }
+
+ if (!$this->writes) {
+ return;
+ }
+
+ $io->success('All questions have been answered!');
+ $io->note(\count($this->writes).' file(s) are about to be written');
+
+ $review = $io->confirm('Review changes? All changes will be written otherwise!');
+ $written = [];
+ $writer = function (string $file, string $contents) use ($io, &$written): void {
+ if (!file_put_contents($file, $contents)) {
+ $io->error(sprintf('Cannot write changes to "%s"', $file));
+
+ return;
+ }
+
+ $written[] = $file;
+ };
+ $differ = class_exists(Differ::class) ? new Differ() : null;
+ $choices = ['y' => 'Yes', 'r' => 'No, show this review once more', 'n' => 'No, skip this file and continue'];
+
+ while ($write = array_shift($this->writes)) {
+ [$file, $contents] = $write;
+
+ if (!is_dir($parent = \dirname($file))) {
+ mkdir($parent, 0777, true);
+ }
+
+ if (!$review) {
+ $writer($file, $contents);
+ continue;
+ }
+
+ do {
+ $io->text(''.(($exist = file_exists($file)) ? '[changed file]' : '[new file]').'> '.$file);
+ if (null === $differ) {
+ if ($exist) {
+ $io->writeln(['--- Original', file_get_contents($file), '+++ New', $contents]);
+ } else {
+ $io->writeln($contents);
+ }
+ } else {
+ $io->writeln($differ->diff($exist ? file_get_contents($file) : '', $contents));
+ }
+ } while ('r' === $choice = $io->choice('Write changes and continue reviewing?', $choices, $choices['r']));
+
+ if ('y' === $choice) {
+ $writer($file, $contents);
+ }
+ }
+
+ $io->success('Done!');
+ $io->note('Don\'t forget to update your database schema, if needed');
+
+ if ($io->confirm('Show written file names?')) {
+ sort($written);
+ $io->listing($written);
+ }
+ }
+
+ private function generateUser(ConsoleStyle $io): void
+ {
+ $lines = file($fileName = $this->user->getFileName());
+ $traits = array_flip($this->user->getTraitNames());
+ $implementors = array_flip($this->user->getInterfaceNames());
+ $inClass = $inClassBody = $hasUses = $hasTraitUses = $hasImplements = false;
+ $useLine = $traitUseLine = $implementsLine = $constructorLine = 0;
+ $nl = null;
+ $indent = '';
+
+ foreach ($tokens = token_get_all(implode('', $lines)) as $i => $token) {
+ if (!\is_array($token)) {
+ if ('{' === $token && $inClass && !$inClassBody) {
+ $inClassBody = true;
+ }
+ continue;
+ }
+
+ if ($inClassBody) {
+ if (!$traitUseLine) {
+ $traitUseLine = $token[2];
+ }
+ } else {
+ if (!$useLine) {
+ $useLine = $token[2];
+ }
+ }
+
+ if (\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
+ if (\T_WHITESPACE === $token[0]) {
+ if (!$nl) {
+ $nl = \in_array($nl = trim($token[1], ' '), ["\n", "\r", "\r\n"], true) ? $nl : null;
+ }
+ if (!$indent && $inClassBody && $nl) {
+ $spaces = explode($nl, $token[1]);
+ $indent = end($spaces);
+ }
+ }
+ continue;
+ }
+
+ if (\T_NAMESPACE === $token[0] && !$useLine) {
+ $useLine = $token[2];
+ } elseif (\T_CLASS === $token[0] && !$inClass) {
+ $inClass = true;
+ } elseif (\T_USE === $token[0]) {
+ if (!$inClass) {
+ $useLine = $token[2];
+ $hasUses = true;
+ } else {
+ $traitUseLine = $token[2];
+ $hasTraitUses = true;
+ }
+ } elseif (\T_EXTENDS === $token[0] && $inClass) {
+ $implementsLine = $tokens[2];
+ $j = $i + 1;
+ while (isset($tokens[$j])) {
+ if (isset($tokens[$j][0]) && \T_STRING === $tokens[$j][0]) {
+ $implementsLine = $tokens[$j][2];
+ } elseif ('{' === $tokens[$j] || (isset($tokens[$j][0]) && \T_IMPLEMENTS === $tokens[$j][0])) {
+ break;
+ }
+ ++$j;
+ }
+ } elseif (\T_IMPLEMENTS === $token[0] && $inClass) {
+ $hasImplements = true;
+ $implementsLine = $token[2];
+ $j = $i + 1;
+ while (isset($tokens[$j])) {
+ if (\is_array($tokens[$j]) && \T_STRING === $tokens[$j][0]) {
+ $implementsLine = $tokens[$j][2];
+ } elseif ('{' === $tokens[$j]) {
+ break;
+ }
+ ++$j;
+ }
+ } elseif (\T_FUNCTION === $token[0]) {
+ $constructorLine = $token[2];
+ $j = $i - 1;
+ while (isset($tokens[$j])) {
+ if (\is_array($tokens[$j])) {
+ $constructorLine = $tokens[$j][2];
+ } elseif (';' === $tokens[$j] || '}' === $tokens[$j]) {
+ break;
+ }
+ --$j;
+ }
+ }
+ }
+ if (!$constructorLine) {
+ $constructorLine = $traitUseLine;
+ }
+
+ $nl = $nl ?? \PHP_EOL;
+ $addUses = $addTraitUses = $addImplementors = [];
+ $write = false;
+ $enableEventHandler = function () use ($implementors, &$addUses, &$addImplementors, &$addTraitUses): void {
+ if (!isset($implementors[DomainEventHandlerInterface::class])) {
+ $addUses[DomainEventHandlerInterface::class] = true;
+ $addUses[DomainEventHandlerTrait::class] = true;
+ $addImplementors['DomainEventHandlerInterface'] = true;
+ $addTraitUses['DomainEventHandlerTrait'] = true;
+ }
+ };
+
+ $this->credential = $this->classMapping[CredentialInterface::class] ?? null;
+
+ if (!$this->hasCredential() && $io->confirm('Generate a user credential?')) {
+ $credentials = [];
+ foreach (glob(Configuration::getPackageDir().'/Entity/Credential/*.php') as $file) {
+ if ('Anonymous' === ($credential = basename($file, '.php')) || false !== strpos($credential, 'SaltedPassword')) {
+ continue;
+ }
+ $credentials[] = $credential;
+ }
+
+ $credential = $io->choice('Select credential type:', $credentials, 'EmailPassword');
+ $credentialClass = $this->credential = 'MsgPhp\\User\\Entity\\Credential\\'.$credential;
+ $credentialTrait = 'MsgPhp\\User\\Entity\\Features\\'.($credentialName = $credential.'Credential');
+ $credentialSignature = self::getConstructorSignature(new \ReflectionClass($credentialClass));
+ $credentialInit = '$this->credential = new '.$credential.'('.self::getSignatureVariables($credentialSignature).');';
+
+ $addUses[$credentialClass] = true;
+ if (!isset($traits[$credentialTrait])) {
+ $addUses[$credentialTrait] = true;
+ $addTraitUses[$credentialName] = true;
+ $enableEventHandler();
+ }
+
+ if (null !== $constructor = $this->user->getConstructor()) {
+ $offset = $constructor->getStartLine() - 1;
+ $length = $constructor->getEndLine() - $offset;
+ $contents = preg_replace_callback_array([
+ '~^[^_]*+__construct\([^\)]*+\)~i' => function (array $match) use ($credentialSignature): string {
+ $signature = substr($match[0], 0, -1);
+ if ('' !== $credentialSignature) {
+ $signature .= ('(' !== substr(rtrim($signature), -1) ? ', ' : '').$credentialSignature;
+ }
+
+ return $signature.')';
+ },
+ '~\s*+}\s*+$~s' => function ($match) use ($nl, $indent, $credential, $credentialInit): string {
+ $indent = ltrim(substr($match[0], 0, strpos($match[0], '}')), "\r\n").' ';
+
+ return $nl.$indent.$credentialInit.$match[0];
+ },
+ ], $oldContents = implode('', \array_slice($lines, $offset, $length)));
+
+ if ($contents !== $oldContents) {
+ array_splice($lines, $offset, $length, $contents);
+ $write = true;
+ if ($traitUseLine > $offset + 1) {
+ ++$traitUseLine;
+ }
+ }
+ } else {
+ $constructor = array_map(function (string $line) use ($nl, $indent): string {
+ return $indent.$line.$nl;
+ }, explode("\n", << $constructorLine) {
+ $traitUseLine += 4;
+ }
+ }
+ }
+
+ $this->passwordReset = isset($traits[Entity\Features\ResettablePassword::class]);
+ if (!$this->passwordReset && $this->hasPassword() && $io->confirm('Can users reset their password?')) {
+ $addUses[Entity\Features\ResettablePassword::class] = true;
+ $addTraitUses['ResettablePassword'] = true;
+ $enableEventHandler();
+ $this->passwordReset = true;
+ }
+
+ $roleProviders = [];
+ if (!isset($this->classMapping[Entity\Role::class]) && $io->confirm('Can users have assigned roles?')) {
+ $baseDir = \dirname($this->user->getFileName());
+ $vars = ['ns' => $ns = $this->user->getNamespaceName()];
+
+ $addUses[Entity\Fields\RolesField::class] = true;
+ $addTraitUses['RolesField'] = true;
+
+ $this->writes[] = [$baseDir.'/Role.php', self::getSkeleton('entity/Role.php', $vars)];
+ $this->writes[] = [$baseDir.'/UserRole.php', self::getSkeleton('entity/UserRole.php', $vars)];
+ $this->configs[] = ['class_mapping' => [
+ Entity\Role::class => $ns.'\\Role',
+ Entity\UserRole::class => $userRoleClass = $ns.'\\UserRole',
+ ]];
+ $roleProviders[] = Role\UserRoleProvider::class;
+ }
+
+ $defaultRoles = [];
+ do {
+ do {
+ $defaultRole = $io->ask('Provide a default user role (e.g. ROLE_USER>)');
+ } while (null === $defaultRole);
+ $defaultRoles[] = $defaultRole;
+ } while ($io->confirm('Add another default user role?', false));
+
+ $this->configs[] = ['role_providers' => ['default' => $defaultRoles] + $roleProviders];
+
+// if (!isset($traits[Features\CanBeEnabled::class]) && $io->confirm('Can users be enabled / disabled?')) {
+// $implementors[] = DomainEventHandlerInterface::class;
+// $addUses[Features\CanBeEnabled::class] = true;
+// $addTraitUses['CanBeEnabled'] = true;
+// $enableEventHandler();
+// }
+//
+// if (!isset($traits[Features\CanBeConfirmed::class]) && $io->confirm('Can users be confirmed?')) {
+// $implementors[] = DomainEventHandlerInterface::class;
+// $addUses[Features\CanBeConfirmed::class] = true;
+// $addTraitUses['CanBeConfirmed'] = true;
+// $enableEventHandler();
+// }
+
+ if ($numUses = \count($addUses)) {
+ ksort($addUses);
+ $uses = array_map(function (string $use) use ($nl): string {
+ return 'use '.$use.';'.$nl;
+ }, array_keys($addUses));
+ if (!$hasUses) {
+ $uses[] = $nl;
+ ++$implementsLine;
+ ++$traitUseLine;
+ }
+ array_splice($lines, $useLine, 0, $uses);
+ $write = true;
+ $implementsLine += $numUses;
+ $traitUseLine += $numUses;
+ }
+
+ if ($numTraitUses = \count($addTraitUses)) {
+ ksort($addTraitUses);
+ $traitUses = array_map(function (string $use) use ($nl, $indent): string {
+ return $indent.'use '.$use.';'.$nl;
+ }, array_keys($addTraitUses));
+ if (!$hasTraitUses) {
+ $traitUses[] = $nl;
+ }
+ array_splice($lines, $traitUseLine, 0, $traitUses);
+ $write = true;
+ }
+
+ if ($numImplementors = \count($addImplementors)) {
+ ksort($addImplementors);
+ $implements = ($hasImplements ? ', ' : ' implements ').implode(', ', array_keys($addImplementors));
+ $lines[$implementsLine - 1] = preg_replace('~(\s*+{?\s*+)$~', $implements.'\\1', $lines[$implementsLine - 1], 1);
+ $write = true;
+ }
+
+ if ($write) {
+ $this->writes[] = [$fileName, implode('', $lines)];
+ }
+ }
+
+ private function generateControllers(ConsoleStyle $io): void
+ {
+ if (!$this->credential || !$io->confirm('Generate controllers?')) {
+ return;
+ }
+
+ if (
+ !class_exists(AnnotatedRouteControllerLoader::class) ||
+ !class_exists(Route::class) ||
+ !interface_exists(FormInterface::class) ||
+ !interface_exists(ValidatorInterface::class) ||
+ !class_exists(Environment::class) ||
+ !interface_exists(MessageBusInterface::class)
+ ) {
+ $io->warning(['Not all controller dependencies are met, run:', 'composer require annotations router form validator twig messenger']);
+
+ if (!$io->confirm('Continue anyway?')) {
+ return;
+ }
+ }
+
+ $usernameField = $this->hasCredential() ? $this->credential::getUsernameField() : null;
+ $nsForm = trim($io->ask('Provide the form namespace', 'App\\Form\\User\\'), '\\');
+ $nsController = trim($io->ask('Provide the controller namespace', 'App\\Controller\\User\\'), '\\');
+ $templateDir = trim($io->ask('Provide the base template directory', 'user/'), '/');
+ $baseTemplate = ltrim($io->ask('Provide the base template file', 'base.html.twig'), '/');
+ $baseTemplateBlock = $io->ask('Provide the base template block name', 'body');
+ $hasRegistration = $this->hasCredential() && $io->confirm('Add a registration controller?');
+ $hasForgotPassword = $this->passwordReset && $io->confirm('Add a forgot and reset password controller?');
+ $hasLogin = $this->hasPassword() && $io->confirm('Add a login and profile controller?'); // keep last
+
+ if ($hasLogin) {
+ if (!class_exists(Security::class)) {
+ $io->warning(['Not all controller dependencies are met, run:', 'composer require security']);
+
+ if (!$io->confirm('Continue anyway?')) {
+ return;
+ }
+ }
+
+ $this->routes[] = <<add('logout', '/logout')
+PHP;
+ $this->writes[] = [$this->projectDir.'/config/packages/security.yaml', self::getSkeleton('security.php', [
+ 'hashAlgorithm' => $this->getPassordHashAlgorithm(),
+ 'fieldName' => $usernameField,
+ ])];
+ }
+
+ if ($hasRegistration) {
+ $this->writes[] = [$this->getClassFileName($nsForm.'\\RegisterType'), self::getSkeleton('form/RegisterType.php', [
+ 'hasPassword' => $this->hasPassword(),
+ 'ns' => $nsForm,
+ 'fieldName' => $usernameField,
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsController.'\\RegisterController'), self::getSkeleton('controller/RegisterController.php', [
+ 'ns' => $nsController,
+ 'formNs' => $nsForm,
+ 'fieldName' => $usernameField,
+ 'template' => $template = $templateDir.'/register.html.twig',
+ 'redirect' => $hasLogin ? '/login' : '/',
+ ])];
+ $this->writes[] = [$this->getTemplateFileName($template), self::getSkeleton('template/register.html.php', [
+ 'hasPassword' => $this->hasPassword(),
+ 'base' => $baseTemplate,
+ 'block' => $baseTemplateBlock,
+ 'fieldName' => $usernameField,
+ ])];
+ }
+
+ if ($hasLogin) {
+ $this->writes[] = [$this->getClassFileName($nsForm.'\\LoginType'), self::getSkeleton('form/LoginType.php', [
+ 'ns' => $nsForm,
+ 'fieldName' => $usernameField,
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsController.'\\LoginController'), self::getSkeleton('controller/LoginController.php', [
+ 'ns' => $nsController,
+ 'formNs' => $nsForm,
+ 'fieldName' => $usernameField,
+ 'template' => $templateLogin = $templateDir.'/login.html.twig',
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsController.'\\ProfileController'), self::getSkeleton('controller/ProfileController.php', [
+ 'ns' => $nsController,
+ 'template' => $templateProfile = $templateDir.'/profile.html.twig',
+ ])];
+ $this->writes[] = [$this->getTemplateFileName($templateLogin), self::getSkeleton('template/login.html.php', [
+ 'hasForgotPassword' => $hasForgotPassword,
+ 'base' => $baseTemplate,
+ 'block' => $baseTemplateBlock,
+ 'fieldName' => $usernameField,
+ ])];
+ $this->writes[] = [$this->getTemplateFileName($templateProfile), self::getSkeleton('template/profile.html.php', [
+ 'base' => $baseTemplate,
+ 'block' => $baseTemplateBlock,
+ 'fieldName' => $usernameField,
+ ])];
+ }
+
+ if ($hasForgotPassword) {
+ $this->writes[] = [$this->getClassFileName($nsForm.'\\ForgotPasswordType'), self::getSkeleton('form/ForgotPasswordType.php', [
+ 'ns' => $nsForm,
+ 'fieldName' => $usernameField,
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsForm.'\\ResetPasswordType'), self::getSkeleton('form/ResetPasswordType.php', [
+ 'ns' => $nsForm,
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsController.'\\ForgotPasswordController'), self::getSkeleton('controller/ForgotPasswordController.php', [
+ 'ns' => $nsController,
+ 'formNs' => $nsForm,
+ 'fieldName' => $usernameField,
+ 'userClass' => $this->user->getName(),
+ 'userShortClass' => $this->user->getShortName(),
+ 'template' => $templateForgot = $templateDir.'/forgot_password.html.twig',
+ ])];
+ $this->writes[] = [$this->getClassFileName($nsController.'\\ResetPasswordController'), self::getSkeleton('controller/ResetPasswordController.php', [
+ 'ns' => $nsController,
+ 'formNs' => $nsForm,
+ 'userClass' => $this->user->getName(),
+ 'userShortClass' => $this->user->getShortName(),
+ 'template' => $templateReset = $templateDir.'/reset_password.html.twig',
+ ])];
+ $this->writes[] = [$this->getTemplateFileName($templateForgot), self::getSkeleton('template/forgot_password.html.php', [
+ 'base' => $baseTemplate,
+ 'block' => $baseTemplateBlock,
+ 'fieldName' => $usernameField,
+ ])];
+ $this->writes[] = [$this->getTemplateFileName($templateReset), self::getSkeleton('template/reset_password.html.php', [
+ 'base' => $baseTemplate,
+ 'block' => $baseTemplateBlock,
+ 'fieldName' => $usernameField,
+ ])];
+ }
+ }
+
+ private function generateConsole(ConsoleStyle $io): void
+ {
+ [$contextElementFactoryNs, $contextElementFactoryShortClass] = self::splitClass($contextElementFactoryClass = 'App\\Console\\ClassContextElementFactory');
+
+ $this->writes[] = [$this->getClassFileName($contextElementFactoryClass), self::getSkeleton('service/ClassContextElementFactory.php', [
+ 'ns' => $contextElementFactoryNs,
+ 'class' => $contextElementFactoryShortClass,
+ 'userClass' => $this->user->getName(),
+ 'userShortClass' => $this->user->getShortName(),
+ 'credentialClass' => $this->credential,
+ 'credentialShortClass' => self::splitClass($this->credential)[1],
+ ])];
+ $this->services[] = <<set(${contextElementFactoryClass}::class)
+->alias(MsgPhp\\Domain\\Infra\\Console\\Context\\ClassContextElementFactoryInterface::class, ${contextElementFactoryClass}::class)
+PHP;
+ }
+
+ private static function getConstructorSignature(\ReflectionClass $class): string
+ {
+ if (null === $constructor = $class->getConstructor()) {
+ return '';
+ }
+
+ $lines = file($class->getFileName());
+ $offset = $constructor->getStartLine() - 1;
+ $body = implode('', \array_slice($lines, $offset, $constructor->getEndLine() - $offset));
+
+ if (preg_match('~^[^_]*+__construct\(([^\)]++)\)~i', $body, $matches)) {
+ return $matches[1];
+ }
+
+ return '';
+ }
+
+ private static function getSignatureVariables(string $signature): string
+ {
+ preg_match_all('~(?:\.{3})?\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $signature, $matches);
+
+ return isset($matches[0][0]) ? implode(', ', $matches[0]) : '';
+ }
+
+ private static function getSkeleton(string $path, array $vars = [])
+ {
+ return (function () use ($path, $vars) {
+ extract($vars);
+
+ return require \dirname(__DIR__).'/Resources/skeleton/'.$path;
+ })();
+ }
+
+ private static function splitClass(string $class): array
+ {
+ $ns = 'App';
+
+ if (false !== $i = strrpos($class, '\\')) {
+ $ns = substr($class, 0, $i);
+ $class = substr($class, $i + 1);
+ }
+
+ return [$ns, $class];
+ }
+
+ private function getTemplateFileName(string $path): string
+ {
+ return $this->projectDir.'/templates/'.$path;
+ }
+
+ private function getClassFileName(string $class): string
+ {
+ if ('App\\' === substr($class, 0, 4)) {
+ $class = substr($class, 4);
+ }
+
+ return $this->projectDir.'/src/'.str_replace('\\', '/', $class).'.php';
+ }
+
+ private function hasCredential(): bool
+ {
+ return $this->credential && Entity\Credential\Anonymous::class !== $this->credential;
+ }
+
+ private function hasPassword(): bool
+ {
+ return $this->hasCredential() && false !== strpos($this->credential, 'Password');
+ }
+
+ private function getPassordHashAlgorithm(): string
+ {
+ if ($this->hasCredential() && false !== strpos($this->credential, 'SaltedPassword')) {
+ return PasswordAlgorithm::DEFAULT_LEGACY;
+ }
+
+ switch (\PASSWORD_DEFAULT) {
+ case \defined('PASSWORD_ARGON2I') ? \PASSWORD_ARGON2I : 2:
+ return 'argon2i';
+ case \PASSWORD_BCRYPT:
+ default:
+ return 'bcrypt';
+ }
+ }
+}
diff --git a/src/UserBundle/MsgPhpUserBundle.php b/src/UserBundle/MsgPhpUserBundle.php
index 835603ae..199d8995 100644
--- a/src/UserBundle/MsgPhpUserBundle.php
+++ b/src/UserBundle/MsgPhpUserBundle.php
@@ -5,7 +5,9 @@
namespace MsgPhp\UserBundle;
use MsgPhp\Domain\Infra\DependencyInjection\BundleHelper;
+use MsgPhp\UserBundle\DependencyInjection\Compiler\CleanupPass;
use MsgPhp\UserBundle\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -17,12 +19,14 @@ final class MsgPhpUserBundle extends Bundle
{
public function boot(): void
{
- BundleHelper::initDoctrineTypes($this->container);
+ BundleHelper::boot($this->container);
}
public function build(ContainerBuilder $container): void
{
- BundleHelper::initDomain($container);
+ $container->addCompilerPass(new CleanupPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100);
+
+ BundleHelper::build($container);
}
public function getContainerExtension(): ?ExtensionInterface
diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md
index 1c04c745..85ded8a2 100644
--- a/src/UserBundle/README.md
+++ b/src/UserBundle/README.md
@@ -1,4 +1,4 @@
-# User bundle
+# User Bundle
A new Symfony bundle for basic user management.
@@ -17,19 +17,21 @@ composer require msgphp/user-bundle
## Features
- Symfony 3.4 / 4.0 ready
-- Doctrine persistence
- Symfony console commands
+- Symfony messenger commands & events
- Symfony security infrastructure
- Symfony validators
+- Doctrine persistence
- Credential independent (supports e-mail, nickname, etc.)
-- Multiple username / credential support
+- Multiple username support
- Primary and secondary user e-mails
- Disabled / enabled users
- User roles
- User attribute values
-## Blog posts
+## Blog Posts
+- [Adding user management to your Symfony application](https://medium.com/@ro0NL/adding-user-management-to-your-symfony-application-ceeefe2a2e9)
- [Commanding a decoupled User entity](https://medium.com/@ro0NL/commanding-a-decoupled-user-entity-aee8723c43e5)
- [Decoupling the User entity with a new Symfony User Bundle](https://medium.com/@ro0NL/decoupling-the-user-entity-with-a-new-symfony-user-bundle-7d2d5d85bdf9)
- [Building a new Symfony User Bundle](https://medium.com/@ro0NL/building-a-new-symfony-user-bundle-b4fe5a9d9d80)
@@ -38,7 +40,7 @@ composer require msgphp/user-bundle
```php
autoconfigure()
->private()
- ->load('MsgPhp\\User\\Infra\\Console\\Command\\', '%kernel.project_dir%/vendor/msgphp/user/Infra/Console/Command')
+ ->load('MsgPhp\\User\\Infra\\Console\\Command\\', Configuration::getPackageDir().'/Infra/Console/Command/*Command.php')
;
};
diff --git a/src/UserBundle/Resources/config/doctrine.php b/src/UserBundle/Resources/config/doctrine.php
index 3b155082..d4c84007 100644
--- a/src/UserBundle/Resources/config/doctrine.php
+++ b/src/UserBundle/Resources/config/doctrine.php
@@ -2,35 +2,28 @@
declare(strict_types=1);
+use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events as DoctrineOrmEvents;
-use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
use MsgPhp\User\Infra\Doctrine;
-use MsgPhp\User\UserIdInterface;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
+use MsgPhp\UserBundle\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use function Symfony\Component\DependencyInjection\Loader\Configurator\ref;
-/** @var ContainerBuilder $container */
-$container = $container ?? (function (): ContainerBuilder { throw new \LogicException('Invalid context.'); })();
-$reflector = ContainerHelper::getClassReflector($container);
-
-return function (ContainerConfigurator $container) use ($reflector): void {
- $baseDir = dirname($reflector(UserIdInterface::class)->getFileName());
- $services = $container->services()
+return function (ContainerConfigurator $container): void {
+ $container->services()
->defaults()
->autowire()
->private()
- ->load($ns = 'MsgPhp\\User\\Infra\\Doctrine\\Repository\\', $repositories = $baseDir.'/Infra/Doctrine/Repository/*Repository.php')
+ ->load('MsgPhp\\User\\Infra\\Doctrine\\Repository\\', Configuration::getPackageDir().'/Infra/Doctrine/Repository/*Repository.php')
+ ->bind(EntityManagerInterface::class, ref('msgphp.doctrine.entity_manager'))
+
+ ->set(Doctrine\ObjectFieldMappings::class)
+ ->tag('msgphp.doctrine.object_field_mappings', ['priority' => -100])
->set(Doctrine\Event\UsernameListener::class)
->tag('doctrine.orm.entity_listener')
->tag('doctrine.event_listener', ['event' => DoctrineOrmEvents::loadClassMetadata])
->tag('doctrine.event_listener', ['event' => DoctrineOrmEvents::postFlush])
;
-
- foreach (glob($repositories) as $file) {
- foreach ($reflector($repository = $ns.basename($file, '.php'))->getInterfaceNames() as $interface) {
- $services->alias($interface, $repository);
- }
- }
};
diff --git a/src/UserBundle/Resources/config/message.php b/src/UserBundle/Resources/config/message.php
index 1d67c949..e2893624 100644
--- a/src/UserBundle/Resources/config/message.php
+++ b/src/UserBundle/Resources/config/message.php
@@ -2,32 +2,16 @@
declare(strict_types=1);
-use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper;
-use MsgPhp\User\UserIdInterface;
-use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
+use MsgPhp\UserBundle\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
-/** @var ContainerBuilder $container */
-$container = $container ?? (function (): ContainerBuilder { throw new \LogicException('Invalid context.'); })();
-$reflector = ContainerHelper::getClassReflector($container);
-$simpleCommandBusEnabled = ContainerHelper::hasBundle($container, SimpleBusCommandBusBundle::class);
-
-return function (ContainerConfigurator $container) use ($reflector, $simpleCommandBusEnabled): void {
- $baseDir = dirname($reflector(UserIdInterface::class)->getFileName());
- $services = $container->services()
+return function (ContainerConfigurator $container): void {
+ $container->services()
->defaults()
- ->autowire()
- ->public()
+ ->autowire()
+ ->private()
- ->load($ns = 'MsgPhp\\User\\Command\\Handler\\', $handlers = $baseDir.'/Command/Handler/*Handler.php')
+ ->load('MsgPhp\\User\\Command\\Handler\\', Configuration::getPackageDir().'/Command/Handler/*Handler.php')
+ ->tag('msgphp.domain.command_handler')
;
-
- if ($simpleCommandBusEnabled) {
- foreach (glob($handlers) as $file) {
- $services->get($handler = $ns.basename($file, '.php'))->tag('command_handler', [
- 'handles' => $reflector($handler)->getMethod('__invoke')->getParameters()[0]->getClass()->getName(),
- ]);
- }
- }
};
diff --git a/src/UserBundle/Resources/config/security.php b/src/UserBundle/Resources/config/security.php
index ea956df6..485155ed 100644
--- a/src/UserBundle/Resources/config/security.php
+++ b/src/UserBundle/Resources/config/security.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use Lexik\Bundle\JWTAuthenticationBundle\Security\User\UserProviderWithPayloadSupportsInterface;
+use Lexik\Bundle\JWTAuthenticationBundle\Security\User\PayloadAwareUserProviderInterface;
use MsgPhp\User\Password\PasswordHashingInterface;
use MsgPhp\User\Infra\Security;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
@@ -25,7 +25,7 @@
->alias(PasswordHashingInterface::class, Security\PasswordHashing::class)
->set(Security\SecurityUserProvider::class)
- ->set(Security\UserValueResolver::class)
+ ->set(Security\UserArgumentValueResolver::class)
->tag('controller.argument_value_resolver')
;
@@ -34,7 +34,7 @@
->tag('request.param_converter', ['converter' => Security\UserParamConverter::NAME]);
}
- if (interface_exists(UserProviderWithPayloadSupportsInterface::class)) {
+ if (interface_exists(PayloadAwareUserProviderInterface::class)) {
$services->set(Security\Jwt\SecurityUserProvider::class);
}
};
diff --git a/src/UserBundle/Resources/config/services.php b/src/UserBundle/Resources/config/services.php
index 0b3ac615..0ea45dae 100644
--- a/src/UserBundle/Resources/config/services.php
+++ b/src/UserBundle/Resources/config/services.php
@@ -3,10 +3,13 @@
declare(strict_types=1);
use MsgPhp\User\Password\{PasswordHashing, PasswordHashingInterface};
+use MsgPhp\User\Role\{ChainRoleProvider, RoleProviderInterface};
+use MsgPhp\UserBundle\Maker;
+use Symfony\Bundle\MakerBundle\MakerInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $container): void {
- $container->services()
+ $services = $container->services()
->defaults()
->autowire()
->autoconfigure()
@@ -14,5 +17,15 @@
->set(PasswordHashing::class)
->alias(PasswordHashingInterface::class, PasswordHashing::class)
+
+ ->set(ChainRoleProvider::class)
+ ->alias(RoleProviderInterface::class, ChainRoleProvider::class)
;
+
+ if (interface_exists(MakerInterface::class)) {
+ $services->set(Maker\UserMaker::class, Maker\UserMaker::class)
+ ->arg('$classMapping', '%msgphp.domain.class_mapping%')
+ ->arg('$projectDir', '%kernel.project_dir%')
+ ->tag('maker.command');
+ }
};
diff --git a/src/UserBundle/Resources/config/twig.php b/src/UserBundle/Resources/config/twig.php
index ee75228a..27dbf2ed 100644
--- a/src/UserBundle/Resources/config/twig.php
+++ b/src/UserBundle/Resources/config/twig.php
@@ -12,6 +12,6 @@
->autoconfigure()
->private()
- ->set(Twig\GlobalVariables::class)
+ ->set(Twig\GlobalVariable::class)
;
};
diff --git a/src/UserBundle/Resources/skeleton/config.php b/src/UserBundle/Resources/skeleton/config.php
new file mode 100644
index 00000000..7a3141c4
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/config.php
@@ -0,0 +1,47 @@
+extension('msgphp_user', ${config});
+PHP;
+}
+
+foreach ($services as $service) {
+ $servicesConfig .= $sep.' '.str_replace("\n", "\n ", $service);
+ $sep = "\n\n";
+}
+
+if ($servicesConfig) {
+ if ($extensionConfig) {
+ $extensionConfig .= "\n\n";
+ }
+ $servicesConfig = <<services()
+ ->defaults()
+ ->private()
+ ->autoconfigure()
+ ->autowire()
+
+${servicesConfig}
+ ;
+PHP;
+}
+
+return <<createNamed('', ForgotPasswordType::class);
+ \$form->handleRequest(\$request);
+
+ if (\$form->isSubmitted() && \$form->isValid()) {
+ \$user = \$em->getRepository(${userShortClass}::class)->findOneBy(['credential.${fieldName}' => \$form->getData()['${fieldName}']]);
+ \$bus->dispatch(new RequestUserPasswordCommand(\$user->getId()));
+ \$flashBag->add('success', 'You\'re password is requested.');
+
+ return new RedirectResponse('/login');
+ }
+
+ return new Response(\$twig->render('${template}', [
+ 'form' => \$form->createView(),
+ ]));
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/controller/LoginController.php b/src/UserBundle/Resources/skeleton/controller/LoginController.php
new file mode 100644
index 00000000..430d447a
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/controller/LoginController.php
@@ -0,0 +1,51 @@
+createNamed('', LoginType::class, [
+ '${fieldName}' => \$authenticationUtils->getLastUsername(),
+ ]);
+
+ if (null !== \$error = \$authenticationUtils->getLastAuthenticationError(true)) {
+ \$form->addError(new FormError(\$error->getMessage(), \$error->getMessageKey(), \$error->getMessageData()));
+ }
+
+ return new Response(\$twig->render('${template}', [
+ 'form' => \$form->createView(),
+ ]));
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/controller/ProfileController.php b/src/UserBundle/Resources/skeleton/controller/ProfileController.php
new file mode 100644
index 00000000..b27fb563
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/controller/ProfileController.php
@@ -0,0 +1,34 @@
+render('${template}'));
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/controller/RegisterController.php b/src/UserBundle/Resources/skeleton/controller/RegisterController.php
new file mode 100644
index 00000000..d1f40b48
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/controller/RegisterController.php
@@ -0,0 +1,58 @@
+createNamed('', RegisterType::class);
+ \$form->handleRequest(\$request);
+
+ if (\$form->isSubmitted() && \$form->isValid()) {
+ \$bus->dispatch(new CreateUserCommand(\$form->getData()));
+ \$flashBag->add('success', 'You\'re successfully registered.');
+
+ return new RedirectResponse('${redirect}');
+ }
+
+ return new Response(\$twig->render('${template}', [
+ 'form' => \$form->createView(),
+ ]));
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/controller/ResetPasswordController.php b/src/UserBundle/Resources/skeleton/controller/ResetPasswordController.php
new file mode 100644
index 00000000..3ef4b6b2
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/controller/ResetPasswordController.php
@@ -0,0 +1,69 @@
+getRepository(${userShortClass}::class)->findOneBy(['passwordResetToken' => \$token]);
+
+ if (!\$user instanceof ${userShortClass}) {
+ throw new NotFoundHttpException();
+ }
+
+ \$form = \$formFactory->createNamed('', ResetPasswordType::class);
+ \$form->handleRequest(\$request);
+
+ if (\$form->isSubmitted() && \$form->isValid()) {
+ \$bus->dispatch(new ChangeUserCredentialCommand(\$user->getId(), ['password' => \$form->getData()['password']]));
+ \$flashBag->add('success', 'You\'re password is changed.');
+
+ return new RedirectResponse('/login');
+ }
+
+ return new Response(\$twig->render('${template}', [
+ 'form' => \$form->createView(),
+ ]));
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/entity/Role.php b/src/UserBundle/Resources/skeleton/entity/Role.php
new file mode 100644
index 00000000..ab320d2f
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/entity/Role.php
@@ -0,0 +1,36 @@
+name = \$name;
+ }
+
+ public function getName(): string
+ {
+ return \$this->name;
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/entity/UserRole.php b/src/UserBundle/Resources/skeleton/entity/UserRole.php
new file mode 100644
index 00000000..4a7bfdf0
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/entity/UserRole.php
@@ -0,0 +1,27 @@
+add('${fieldName}', ${fieldType}::class, [
+ 'constraints' => [${constraints}],
+ ]);
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/form/LoginType.php b/src/UserBundle/Resources/skeleton/form/LoginType.php
new file mode 100644
index 00000000..bde1d1c5
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/form/LoginType.php
@@ -0,0 +1,34 @@
+add('${fieldName}', ${fieldType}::class);
+ \$builder->add('password', PasswordType::class);
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/form/RegisterType.php b/src/UserBundle/Resources/skeleton/form/RegisterType.php
new file mode 100644
index 00000000..fa265a8b
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/form/RegisterType.php
@@ -0,0 +1,60 @@
+add('${fieldName}', ${fieldType}::class, [
+ 'constraints' => [${constraints}],
+ ]);
+PHP;
+
+if ($hasPassword) {
+ $uses[] = 'use MsgPhp\\User\\Infra\\Form\\Type\\HashedPasswordType;';
+ $fields .= <<add('password', HashedPasswordType::class, [
+ 'password_confirm' => true,
+ 'password_options' => ['constraints' => new NotBlank()],
+ ]);
+PHP;
+}
+
+sort($uses);
+$uses = implode("\n", $uses);
+
+return <<add('password', HashedPasswordType::class, [
+ 'password_confirm' => true,
+ 'password_options' => ['constraints' => new NotBlank()],
+ ]);
+ }
+}
+
+PHP;
diff --git a/src/UserBundle/Resources/skeleton/messenger.php b/src/UserBundle/Resources/skeleton/messenger.php
new file mode 100644
index 00000000..16685ce4
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/messenger.php
@@ -0,0 +1,31 @@
+label = 'E-mail';
+ break;
+PHP;
+}
+
+$constructor = '{';
+if (false !== strpos($credentialClass, 'Password')) {
+ $uses[] = 'use MsgPhp\User\Password\PasswordHashingInterface;';
+ $constructor = <<passwordHashing = \$passwordHashing;
+ }
+
+PHP;
+ $cases[] = <<hide()
+ ->generator(function (): string {
+ return bin2hex(random_bytes(8));
+ })
+ ->normalizer(function (string \$value): string {
+ return \$this->passwordHashing->hash(\$value);
+ });
+ }
+ break;
+PHP;
+}
+
+$switch = '';
+if ($cases) {
+ $switch = "\n\n switch (\$argument) {\n".implode("\n", $cases)."\n }";
+}
+
+sort($uses);
+$uses = implode("\n", $uses);
+
+return <<Forgot Password
+
+ {{ form_start(form) }}
+ {{ form_errors(form) }}
+ {{ form_row(form.${fieldName}) }}
+
+
+
+
+ {{ form_end(form) }}
+{% endblock %}
+
+TWIG;
diff --git a/src/UserBundle/Resources/skeleton/template/login.html.php b/src/UserBundle/Resources/skeleton/template/login.html.php
new file mode 100644
index 00000000..14c5c4a4
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/template/login.html.php
@@ -0,0 +1,31 @@
+Forgot password?
+TWIG;
+} else {
+ $forgotPasswordLink = '';
+}
+
+return <<Login
+
+ {{ form_start(form) }}
+ {{ form_errors(form) }}
+ {{ form_row(form.${fieldName}) }}
+ {{ form_row(form.password) }}
+
+
+ ${forgotPasswordLink}
+
+ {{ form_end(form) }}
+{% endblock %}
+
+TWIG;
diff --git a/src/UserBundle/Resources/skeleton/template/profile.html.php b/src/UserBundle/Resources/skeleton/template/profile.html.php
new file mode 100644
index 00000000..0c8284b1
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/template/profile.html.php
@@ -0,0 +1,19 @@
+Your Profile
+
+ Logged in as: {{ ${userVariable}.current.${fieldName} }}
+ Logout
+{% endblock %}
+
+TWIG;
diff --git a/src/UserBundle/Resources/skeleton/template/register.html.php b/src/UserBundle/Resources/skeleton/template/register.html.php
new file mode 100644
index 00000000..170301dc
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/template/register.html.php
@@ -0,0 +1,33 @@
+Register
+
+ {{ form_start(form) }}
+ {{ form_errors(form) }}
+${fields}
+
+
+
+
+ {{ form_end(form) }}
+{% endblock %}
+
+TWIG;
diff --git a/src/UserBundle/Resources/skeleton/template/reset_password.html.php b/src/UserBundle/Resources/skeleton/template/reset_password.html.php
new file mode 100644
index 00000000..3081d57b
--- /dev/null
+++ b/src/UserBundle/Resources/skeleton/template/reset_password.html.php
@@ -0,0 +1,22 @@
+Reset Password
+
+ {{ form_start(form) }}
+ {{ form_errors(form) }}
+ {{ form_row(form.password.password) }}
+ {{ form_row(form.password.confirmation) }}
+
+
+
+
+ {{ form_end(form) }}
+{% endblock %}
+
+TWIG;
diff --git a/src/UserBundle/Twig/GlobalVariables.php b/src/UserBundle/Twig/GlobalVariable.php
similarity index 63%
rename from src/UserBundle/Twig/GlobalVariables.php
rename to src/UserBundle/Twig/GlobalVariable.php
index c542c2eb..35101f9d 100644
--- a/src/UserBundle/Twig/GlobalVariables.php
+++ b/src/UserBundle/Twig/GlobalVariable.php
@@ -4,7 +4,6 @@
namespace MsgPhp\UserBundle\Twig;
-use MsgPhp\Domain\Factory\EntityAwareFactoryInterface;
use MsgPhp\User\Entity\User;
use MsgPhp\User\Infra\Security\SecurityUser;
use MsgPhp\User\Repository\UserRepositoryInterface;
@@ -18,8 +17,10 @@
*
* @internal
*/
-final class GlobalVariables implements ServiceSubscriberInterface
+final class GlobalVariable implements ServiceSubscriberInterface
{
+ public const NAME = 'msgphp_user';
+
private $container;
public function __construct(ContainerInterface $container)
@@ -27,31 +28,26 @@ public function __construct(ContainerInterface $container)
$this->container = $container;
}
- public function getUser(): ?User
+ public function getCurrent(): User
{
- $id = $this->getUserId();
-
- return null === $id ? null : $this->getRepository()->find($id);
+ return $this->getUserRepository()->find($this->getCurrentId());
}
- public function getUserId(): ?UserIdInterface
+ public function getCurrentId(): UserIdInterface
{
$token = $this->getTokenStorage()->getToken();
if (null === $token) {
- return null;
+ throw new \LogicException('User not authenticated.');
}
$user = $token->getUser();
if (!$user instanceof SecurityUser) {
- return null;
+ throw new \LogicException('User not authenticated.');
}
- /** @var UserIdInterface $id */
- $id = $this->getDomainFactory()->identify(User::class, $user->getUsername());
-
- return $id;
+ return $user->getUserId();
}
public function isUserType(User $user, string $class): bool
@@ -62,30 +58,24 @@ public function isUserType(User $user, string $class): bool
public static function getSubscribedServices(): array
{
return [
- EntityAwareFactoryInterface::class,
'?'.TokenStorageInterface::class,
'?'.UserRepositoryInterface::class,
];
}
- private function getDomainFactory(): EntityAwareFactoryInterface
- {
- return $this->container->get(EntityAwareFactoryInterface::class);
- }
-
private function getTokenStorage(): TokenStorageInterface
{
if (!$this->container->has(TokenStorageInterface::class)) {
- throw new \LogicException('No token storage available.');
+ throw new \LogicException('Token storage not available.');
}
return $this->container->get(TokenStorageInterface::class);
}
- private function getRepository(): UserRepositoryInterface
+ private function getUserRepository(): UserRepositoryInterface
{
if (!$this->container->has(UserRepositoryInterface::class)) {
- throw new \LogicException('No repository available.');
+ throw new \LogicException('User repository not available.');
}
return $this->container->get(UserRepositoryInterface::class);
diff --git a/src/UserBundle/composer.json b/src/UserBundle/composer.json
index ee321169..e036e33d 100644
--- a/src/UserBundle/composer.json
+++ b/src/UserBundle/composer.json
@@ -2,7 +2,7 @@
"name": "msgphp/user-bundle",
"type": "symfony-bundle",
"license": "MIT",
- "description": "User domain layer",
+ "description": "Basic user management (the `User` domain)",
"keywords": ["user", "security"],
"authors": [
{
@@ -18,7 +18,7 @@
"exclude-from-classmap": ["/Tests/"]
},
"require": {
- "msgphp/user": "^0.2|^1.0",
+ "msgphp/user": "^0.7",
"symfony/config": "^3.4|^4.0",
"symfony/dependency-injection": "^3.4|^4.0",
"symfony/http-kernel": "^3.4|^4.0"
@@ -26,10 +26,14 @@
"require-dev": {
"doctrine/doctrine-bundle": "^1.8",
"doctrine/orm": "^2.6",
+ "lexik/jwt-authentication-bundle": "^2.5",
+ "msgphp/eav-bundle": "^0.7",
"simple-bus/symfony-bridge": "^5.1",
- "symfony/phpunit-bridge": "^3.4|^4.0",
"symfony/console": "^3.4|^4.0",
"symfony/form": "^3.4|^4.0",
+ "symfony/maker-bundle": "^1.5",
+ "symfony/messenger": "^4.2",
+ "symfony/phpunit-bridge": "^3.4.9|^4.0.9",
"symfony/security-bundle": "^3.4|^4.0",
"symfony/twig-bundle": "^3.4|^4.0",
"symfony/validator": "^3.4|^4.0",
@@ -46,7 +50,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "0.7-dev"
}
}
}
diff --git a/tests/fixtures/sa/return-static.php b/tests/fixtures/sa/return-static.php
new file mode 100644
index 00000000..62a8abc0
--- /dev/null
+++ b/tests/fixtures/sa/return-static.php
@@ -0,0 +1,27 @@
+accept(SomeId::fromValue('id'));
+$test->accept($id = SomeId::fromValue('id'));
+
+// test it autocompletes
+//$id->auto