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