From 5776b5069c1b2a0f9bc4dbc821f03e1e9fd9843f Mon Sep 17 00:00:00 2001 From: Sviatoslav Melnyk Date: Wed, 16 Sep 2020 18:04:46 +0200 Subject: [PATCH 1/3] fixed yaml format --- config/api_platform/User/User.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config/api_platform/User/User.yaml b/config/api_platform/User/User.yaml index efbe90b..3bb6fef 100644 --- a/config/api_platform/User/User.yaml +++ b/config/api_platform/User/User.yaml @@ -6,14 +6,14 @@ Acme\Domain\User\User: get: method: GET filters: [ 'user.search_filter' ] - output: Acme\UI\Http\Rest\Presentation\User\UserView - normalization_context: - groups: [ profile ] - get_v2: - path: v2/users - method: GET - query: Acme\Application\UseCase\Query\User\GetUsers\GetUsersQuery - filters: [ 'user.search_filter' ] + output: Acme\UI\Http\Rest\Presentation\User\UserView + normalization_context: + groups: [ profile ] + get_v2: + path: v2/users + method: GET + query: Acme\Application\UseCase\Query\User\GetUsers\GetUsersQuery + filters: [ 'user.search_filter' ] output: Acme\UI\Http\Rest\Presentation\User\UserProfileView post: method: POST From 935e3e114c7b4301d0e8964bf01329f1151a9c3b Mon Sep 17 00:00:00 2001 From: Sviatoslav Melnyk Date: Tue, 22 Sep 2020 13:10:20 +0200 Subject: [PATCH 2/3] Organization model and related use cases --- composer.json | 1 + composer.lock | 1074 ++--------------- .../Organization/Organization.yaml | 98 ++ .../packages/doctrine/dbal/type/string.yaml | 2 + .../doctrine/orm/mapping/organization.yaml | 9 + .../CreateOrganizationCommand.php | 52 + .../CreateOrganizationCommandHandler.php | 27 + .../CreateOrganizationInput.php | 70 ++ ...CreateOrganizationInputDataTransformer.php | 46 + .../UpdateBillingInformationCommand.php | 68 ++ ...UpdateBillingInformationCommandHandler.php | 30 + .../UpdateBillingInformationInput.php | 63 + ...BillingInformationInputDataTransformer.php | 54 + .../User/ChangeEmail/ChangeEmailCommand.php | 2 +- .../ChangeEmail/ChangeEmailCommandHandler.php | 2 +- .../ChangeEmailDataTransformer.php | 2 +- .../User/ChangeEmail/ChangeEmailInput.php | 2 +- .../Event/BillingInformationWasUpdated.php | 37 + .../Event/OrganizationWasCreated.php | 33 + src/Domain/Organization/Organization.php | 73 ++ .../OrganizationRepositoryInterface.php | 15 + .../Organization/ValueObject/Address.php | 36 + .../ValueObject/BillingInformation.php | 31 + .../ValueObject/OrganizationProfile.php | 24 + src/Domain/Shared/ValueObject/PhoneNumber.php | 41 + .../Doctrine/Orm/Mapping/Organization.orm.xml | 13 + .../Orm/Mapping/ValueObject.Address.orm.xml | 13 + .../ValueObject.BillingInformation.orm.xml | 11 + .../ValueObject.OrganizationProfile.orm.xml | 10 + .../Repository/OrganizationStore.php | 37 + .../Shared/Doctrine/PhoneNumberType.php | 55 + .../2020/09/Version20200922090134.php | 31 + symfony.lock | 6 + 33 files changed, 1115 insertions(+), 953 deletions(-) create mode 100644 config/api_platform/Organization/Organization.yaml create mode 100644 config/packages/doctrine/orm/mapping/organization.yaml create mode 100644 src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommand.php create mode 100644 src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommandHandler.php create mode 100644 src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationInput.php create mode 100644 src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationInputDataTransformer.php create mode 100644 src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommand.php create mode 100644 src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommandHandler.php create mode 100644 src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationInput.php create mode 100644 src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationInputDataTransformer.php create mode 100644 src/Domain/Organization/Event/BillingInformationWasUpdated.php create mode 100644 src/Domain/Organization/Event/OrganizationWasCreated.php create mode 100644 src/Domain/Organization/Organization.php create mode 100644 src/Domain/Organization/Repository/OrganizationRepositoryInterface.php create mode 100644 src/Domain/Organization/ValueObject/Address.php create mode 100644 src/Domain/Organization/ValueObject/BillingInformation.php create mode 100644 src/Domain/Organization/ValueObject/OrganizationProfile.php create mode 100644 src/Domain/Shared/ValueObject/PhoneNumber.php create mode 100644 src/Infrastructure/Organization/Doctrine/Orm/Mapping/Organization.orm.xml create mode 100644 src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.Address.orm.xml create mode 100644 src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.BillingInformation.orm.xml create mode 100644 src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.OrganizationProfile.orm.xml create mode 100644 src/Infrastructure/Organization/Repository/OrganizationStore.php create mode 100644 src/Infrastructure/Shared/Doctrine/PhoneNumberType.php create mode 100644 src/Infrastructure/Shared/Migration/2020/09/Version20200922090134.php diff --git a/composer.json b/composer.json index 5088de6..1731132 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "ext-iconv": "*", "ext-json": "*", "api-platform/core": "^2.5", + "giggsey/libphonenumber-for-php": "^8.12", "lexik/jwt-authentication-bundle": "^2.8", "nelmio/cors-bundle": "^2.1", "ramsey/uuid-doctrine": "^1.6", diff --git a/composer.lock b/composer.lock index c8cfc30..61b27ab 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "68a12cd899566277e4d6c26c5246591e", + "content-hash": "ab705d4032176d0b77a1c17c5bd8447a", "packages": [ { "name": "api-platform/core", @@ -199,12 +199,6 @@ "brick", "math" ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" - } - ], "time": "2020-08-18T23:57:15+00:00" }, { @@ -260,20 +254,6 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-08-25T05:50:16+00:00" }, { @@ -426,20 +406,6 @@ "redis", "xcache" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], "time": "2020-07-07T18:54:01+00:00" }, { @@ -581,20 +547,6 @@ "doctrine", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", - "type": "tidelift" - } - ], "time": "2020-06-05T16:59:53+00:00" }, { @@ -690,20 +642,6 @@ "sqlserver", "sqlsrv" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", - "type": "tidelift" - } - ], "time": "2020-09-02T01:35:42+00:00" }, { @@ -780,20 +718,6 @@ "event system", "events" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], "time": "2020-05-29T18:28:51+00:00" }, { @@ -872,20 +796,6 @@ "uppercase", "words" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", - "type": "tidelift" - } - ], "time": "2020-05-29T07:19:59+00:00" }, { @@ -942,20 +852,6 @@ "constructor", "instantiate" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -1018,20 +914,6 @@ "parser", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -1120,20 +1002,6 @@ "database", "orm" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine/orm", - "type": "tidelift" - } - ], "time": "2020-05-26T16:03:49+00:00" }, { @@ -1218,20 +1086,6 @@ "orm", "persistence" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", - "type": "tidelift" - } - ], "time": "2020-05-12T19:32:44+00:00" }, { @@ -1369,6 +1223,123 @@ ], "time": "2020-04-27T06:40:36+00:00" }, + { + "name": "giggsey/libphonenumber-for-php", + "version": "8.12.9", + "source": { + "type": "git", + "url": "https://github.com/giggsey/libphonenumber-for-php.git", + "reference": "674b79ae3362409d08e550cb196d982485a76225" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/674b79ae3362409d08e550cb196d982485a76225", + "reference": "674b79ae3362409d08e550cb196d982485a76225", + "shasum": "" + }, + "require": { + "giggsey/locale": "^1.7", + "php": ">=5.3.2", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "^2.7", + "php-coveralls/php-coveralls": "^1.0|^2.0", + "phpunit/phpunit": "^4.8.36|^5.0", + "symfony/console": "^2.8|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "libphonenumber\\": "src/" + }, + "exclude-from-classmap": [ + "/src/data/", + "/src/carrier/data/", + "/src/geocoding/data/", + "/src/timezone/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "PHP Port of Google's libphonenumber", + "homepage": "https://github.com/giggsey/libphonenumber-for-php", + "keywords": [ + "geocoding", + "geolocation", + "libphonenumber", + "mobile", + "phonenumber", + "validation" + ], + "time": "2020-08-31T09:51:44+00:00" + }, + { + "name": "giggsey/locale", + "version": "1.9", + "source": { + "type": "git", + "url": "https://github.com/giggsey/Locale.git", + "reference": "b07f1eace8072ccc61445ad8fbd493ff9d783043" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/Locale/zipball/b07f1eace8072ccc61445ad8fbd493ff9d783043", + "reference": "b07f1eace8072ccc61445ad8fbd493ff9d783043", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "~2.7", + "php-coveralls/php-coveralls": "^1.0|^2.0", + "phpunit/phpunit": "^4.8|^5.0", + "symfony/console": "^2.8|^3.0|^4.0", + "symfony/filesystem": "^2.8|^3.0|^4.0", + "symfony/finder": "^2.8|^3.0|^4.0", + "symfony/process": "^2.8|^3.0|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Giggsey\\Locale\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "http://giggsey.com/" + } + ], + "description": "Locale functions required by libphonenumber-for-php", + "time": "2020-07-07T11:16:24+00:00" + }, { "name": "lcobucci/jwt", "version": "3.3.3", @@ -1422,16 +1393,6 @@ "JWS", "jwt" ], - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], "time": "2020-08-20T13:22:28+00:00" }, { @@ -1528,12 +1489,6 @@ "rest", "symfony" ], - "funding": [ - { - "url": "https://github.com/chalasr", - "type": "github" - } - ], "time": "2020-06-14T13:20:35+00:00" }, { @@ -1954,12 +1909,6 @@ "queue", "set" ], - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - } - ], "time": "2020-08-11T00:57:21+00:00" }, { @@ -2041,12 +1990,6 @@ "identifier", "uuid" ], - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - } - ], "time": "2020-08-18T17:17:46+00:00" }, { @@ -2149,20 +2092,6 @@ ], "description": "Symfony AMQP extension Messenger Bridge", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -2220,20 +2149,6 @@ ], "description": "Symfony Asset Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-30T20:35:19+00:00" }, { @@ -2314,20 +2229,6 @@ "caching", "psr6" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-01T05:52:18+00:00" }, { @@ -2390,20 +2291,6 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:23:11+00:00" }, { @@ -2470,20 +2357,6 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:48:54+00:00" }, { @@ -2563,20 +2436,6 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-02T07:07:40+00:00" }, { @@ -2652,20 +2511,6 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-01T18:07:16+00:00" }, { @@ -2716,20 +2561,6 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-06-06T08:49:21+00:00" }, { @@ -2790,20 +2621,6 @@ ], "description": "Symfony Doctrine Messenger Bridge", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-21T12:10:32+00:00" }, { @@ -2862,20 +2679,6 @@ "env", "environment" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-28T08:20:44+00:00" }, { @@ -2933,20 +2736,6 @@ ], "description": "Symfony ErrorHandler Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T10:01:29+00:00" }, { @@ -3019,20 +2808,6 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-13T14:19:42+00:00" }, { @@ -3095,20 +2870,6 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:23:11+00:00" }, { @@ -3161,20 +2922,6 @@ ], "description": "Symfony ExpressionLanguage Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-31T07:33:39+00:00" }, { @@ -3225,20 +2972,6 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-21T17:19:47+00:00" }, { @@ -3288,20 +3021,6 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T10:01:29+00:00" }, { @@ -3351,20 +3070,6 @@ } ], "description": "Composer plugin for Symfony", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-31T14:36:07+00:00" }, { @@ -3498,20 +3203,6 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-30T09:59:07+00:00" }, { @@ -3563,30 +3254,16 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", "time": "2020-08-17T07:48:54+00:00" }, { @@ -3686,20 +3363,6 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-02T08:15:18+00:00" }, { @@ -3776,20 +3439,6 @@ ], "description": "Symfony Messenger Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T12:05:19+00:00" }, { @@ -3854,20 +3503,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -3935,20 +3570,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -4012,20 +3633,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -4088,20 +3695,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -4168,20 +3761,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -4250,20 +3829,6 @@ "property path", "reflection" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-30T08:29:58+00:00" }, { @@ -4341,20 +3906,6 @@ "type", "validator" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-18T07:27:13+00:00" }, { @@ -4409,20 +3960,6 @@ ], "description": "Symfony Redis extension Messenger Bridge", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:48:54+00:00" }, { @@ -4501,20 +4038,6 @@ "uri", "url" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-10T08:03:57+00:00" }, { @@ -4600,20 +4123,6 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-18T11:41:36+00:00" }, { @@ -4689,20 +4198,6 @@ ], "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-18T11:34:54+00:00" }, { @@ -4762,20 +4257,6 @@ ], "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -4831,20 +4312,6 @@ ], "description": "Symfony Security Component - Guard", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -4913,20 +4380,6 @@ ], "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-25T15:26:05+00:00" }, { @@ -5010,20 +4463,6 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-01T05:52:18+00:00" }, { @@ -5086,20 +4525,6 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:23:11+00:00" }, { @@ -5171,20 +4596,6 @@ "utf-8", "utf8" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:48:54+00:00" }, { @@ -5246,20 +4657,6 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:23:11+00:00" }, { @@ -5362,20 +4759,6 @@ ], "description": "Symfony Twig Bridge", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-26T14:33:04+00:00" }, { @@ -5451,20 +4834,6 @@ ], "description": "Symfony TwigBundle", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -5562,20 +4931,6 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-31T09:01:51+00:00" }, { @@ -5652,20 +5007,6 @@ "debug", "dump" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:42:30+00:00" }, { @@ -5727,20 +5068,6 @@ "instantiate", "serialize" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-06-07T15:42:22+00:00" }, { @@ -5816,20 +5143,6 @@ "psr13", "push" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-28T08:20:44+00:00" }, { @@ -5900,20 +5213,6 @@ "transition", "workflow" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -5977,20 +5276,6 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-26T08:30:57+00:00" }, { @@ -6053,16 +5338,6 @@ "keywords": [ "templating" ], - "funding": [ - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/twig/twig", - "type": "tidelift" - } - ], "time": "2020-08-05T15:13:19+00:00" }, { @@ -6258,20 +5533,6 @@ "orm", "persistence" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", - "type": "tidelift" - } - ], "time": "2020-08-25T10:57:15+00:00" }, { @@ -6342,20 +5603,6 @@ "migrations", "schema" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", - "type": "tidelift" - } - ], "time": "2020-06-15T06:04:38+00:00" }, { @@ -6441,20 +5688,6 @@ "migrations", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", - "type": "tidelift" - } - ], "time": "2020-06-21T08:55:42+00:00" }, { @@ -6766,20 +5999,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], "time": "2020-09-02T13:14:53+00:00" }, { @@ -6880,20 +6099,6 @@ ], "description": "Symfony Doctrine Bridge", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-21T17:19:47+00:00" }, { @@ -6944,20 +6149,6 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -7023,20 +6214,6 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-10T08:03:57+00:00" }, { @@ -7164,6 +6341,5 @@ "ext-iconv": "*", "ext-json": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } diff --git a/config/api_platform/Organization/Organization.yaml b/config/api_platform/Organization/Organization.yaml new file mode 100644 index 0000000..3e1ed65 --- /dev/null +++ b/config/api_platform/Organization/Organization.yaml @@ -0,0 +1,98 @@ +Acme\Domain\Organization\Organization: + properties: + uuid: + identifier: true + collectionOperations: + post: + method: POST + path: '/organizations' + messenger: true + input: Acme\Application\UseCase\Command\Organization\CreateOrganization\CreateOrganizationInput + output: false + validate: false + status: 202 + openapi_context: + summary: Create an organization + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + format: uuid + required: true + description: Uuid Version 4 https://www.uuidgenerator.net/ + name: + type: string + required: true + example: Apple + description: + type: string + required: true + shortDescription: + type: string + required: true + addressLine1: + type: string + required: true + example: One Apple Park Way + addressLine2: + type: string + required: true + city: + type: string + required: true + example: Cupertino + region: + type: string + required: true + example: CA + country: + type: string + required: true + example: USA + zipCode: + type: string + required: true + example: "95014" + responses: + 202: + description: Request has been received and will be treated later + 400: + description: Invalid input + itemOperations: + get: + method: GET +# output: Acme\UI\Http\Rest\Presentation\User\UserView +# normalization_context: +# groups: [ profile ] + update_billing_information: + method: PUT + path: '/organizations/{uuid}/billing_information' + messenger: true + input: Acme\Application\UseCase\Command\Organization\UpdateBillingInformation\UpdateBillingInformationInput + # Disable API Platform data retriever + read: false + # No content in the response + output: false + # By pass response validation to allow null response + validate: false + # Override default documentation + openapi_context: + summary: Update organization's billing information + # API Platform only supports identifier named `id` in path + parameters: + - in: path + name: uuid + type: string + required: true + responses: + 204: + description: User email updated successfully + 400: + description: Invalid input + 409: + description: Conflict diff --git a/config/packages/doctrine/dbal/type/string.yaml b/config/packages/doctrine/dbal/type/string.yaml index 1ac15d7..9b7e9c1 100644 --- a/config/packages/doctrine/dbal/type/string.yaml +++ b/config/packages/doctrine/dbal/type/string.yaml @@ -3,6 +3,8 @@ doctrine: types: email: Acme\Infrastructure\Shared\Doctrine\EmailType hashed_password: Acme\Infrastructure\Shared\Doctrine\HashedPasswordType + phone_number: Acme\Infrastructure\Shared\Doctrine\PhoneNumberType mapping_types: email: string hashed_password: string + phone_number: string diff --git a/config/packages/doctrine/orm/mapping/organization.yaml b/config/packages/doctrine/orm/mapping/organization.yaml new file mode 100644 index 0000000..2e2f3b4 --- /dev/null +++ b/config/packages/doctrine/orm/mapping/organization.yaml @@ -0,0 +1,9 @@ +doctrine: + orm: + mappings: + Organization: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/src/Infrastructure/Organization/Doctrine/Orm/Mapping' + prefix: 'Acme\Domain\Organization' + alias: Organization diff --git a/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommand.php b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommand.php new file mode 100644 index 0000000..8f7aa32 --- /dev/null +++ b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommand.php @@ -0,0 +1,52 @@ +uuid = Uuid::fromString($uuid); + $this->profile = new OrganizationProfile($name, $description, $shortDescription); + $this->address = new Address($addressLine1, $addressLine2, $city, $region, $country, $zipCode); + } + + public function uuid(): UuidInterface + { + return $this->uuid; + } + + public function profile(): OrganizationProfile + { + return $this->profile; + } + + public function address(): Address + { + return $this->address; + } +} diff --git a/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommandHandler.php b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommandHandler.php new file mode 100644 index 0000000..a806311 --- /dev/null +++ b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationCommandHandler.php @@ -0,0 +1,27 @@ +organizationRepository = $organizationRepository; + } + + public function __invoke(CreateOrganizationCommand $command): void + { + $organization = Organization::create($command->uuid(), $command->profile(), $command->address()); + + $this->organizationRepository->store($organization); + } +} diff --git a/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationInput.php b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationInput.php new file mode 100644 index 0000000..f7e3b6c --- /dev/null +++ b/src/Application/UseCase/Command/Organization/CreateOrganization/CreateOrganizationInput.php @@ -0,0 +1,70 @@ +validator = $validator; + } + + public function transform($object, string $to, array $context = []) + { + if (false === $object instanceof CreateOrganizationInput) { + throw new \InvalidArgumentException(\sprintf('Object is not an instance of %s', + CreateOrganizationInput::class)); + } + + $this->validator->validate($object, $context); + + return new CreateOrganizationCommand( + $object->uuid, + $object->name, + $object->description, + $object->shortDescription, + $object->addressLine1, + $object->addressLine2, + $object->city, + $object->region, + $object->country, + $object->zipCode + ); + } + + public function supportsTransformation($data, string $to, array $context = []): bool + { + return CreateOrganizationInput::class === ($context['input']['class'] ?? null); + } +} diff --git a/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommand.php b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommand.php new file mode 100644 index 0000000..7d4ed0f --- /dev/null +++ b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommand.php @@ -0,0 +1,68 @@ +organizationUuid = Uuid::fromString($organizationUuid); + $this->email = Email::fromString($email); + $this->phoneNumber = PhoneNumber::fromString($phoneNumber); + $this->companyName = $companyName; + $this->address = new Address($addressLine1, $addressLine2, $city, $region, $country, $zipCode); + } + + public function organizationUuid(): UuidInterface + { + return $this->organizationUuid; + } + + public function companyName(): string + { + return $this->companyName; + } + + public function email(): Email + { + return $this->email; + } + + public function phoneNumber(): PhoneNumber + { + return $this->phoneNumber; + } + + public function address(): Address + { + return $this->address; + } +} diff --git a/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommandHandler.php b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommandHandler.php new file mode 100644 index 0000000..3eb3a1d --- /dev/null +++ b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationCommandHandler.php @@ -0,0 +1,30 @@ +organizationRepository = $organizationRepository; + } + + public function __invoke(UpdateBillingInformationCommand $command): void + { + $organization = $this->organizationRepository->get($command->organizationUuid()); + + $organization->updateBillingInformation($command->companyName(), $command->email(), $command->phoneNumber(), $command->address()); + + $this->organizationRepository->store($organization); + } +} diff --git a/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationInput.php b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationInput.php new file mode 100644 index 0000000..8b552ed --- /dev/null +++ b/src/Application/UseCase/Command/Organization/UpdateBillingInformation/UpdateBillingInformationInput.php @@ -0,0 +1,63 @@ +validator = $validator; + } + + public function transform($object, string $to, array $context = []) + { + if (!$object instanceof UpdateBillingInformationInput) { + throw new \InvalidArgumentException(\sprintf('Object is not an instance of %s', UpdateBillingInformationInput::class)); + } + + if (!isset($context['uuid'])) { + throw new \RuntimeException(\sprintf('Missing uuid value in context')); + } + + if (($uuid = $context['uuid']) && !$uuid instanceof UuidInterface) { + throw new \InvalidArgumentException(\sprintf('Given uuid must be an instance of %s', UuidInterface::class)); + } + + $this->validator->validate($object, $context); + + return new UpdateBillingInformationCommand( + $uuid->toString(), + $object->companyName, + $object->addressLine1, + $object->addressLine2, + $object->city, + $object->region, + $object->country, + $object->zipCode, + $object->phoneNumber, + $object->email + ); + } + + public function supportsTransformation($data, string $to, array $context = []): bool + { + return UpdateBillingInformationInput::class === ($context['input']['class'] ?? null); + } +} diff --git a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommand.php b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommand.php index 71f0b64..a612c67 100644 --- a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommand.php +++ b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommand.php @@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; -class ChangeEmailCommand implements CommandInterface +final class ChangeEmailCommand implements CommandInterface { private UuidInterface $userUuid; diff --git a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommandHandler.php b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommandHandler.php index cf6a95c..19e5934 100644 --- a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommandHandler.php +++ b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailCommandHandler.php @@ -8,7 +8,7 @@ use Acme\Domain\User\Specification\Checker\CustomerEmailUniquenessCheckerInterface; use Acme\Infrastructure\Shared\Bus\Command\CommandHandlerInterface; -class ChangeEmailCommandHandler implements CommandHandlerInterface +final class ChangeEmailCommandHandler implements CommandHandlerInterface { private UserRepositoryInterface $userRepository; diff --git a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailDataTransformer.php b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailDataTransformer.php index d342b64..4beae4b 100644 --- a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailDataTransformer.php +++ b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailDataTransformer.php @@ -8,7 +8,7 @@ use ApiPlatform\Core\Validator\ValidatorInterface; use Ramsey\Uuid\UuidInterface; -class ChangeEmailDataTransformer implements DataTransformerInterface +final class ChangeEmailDataTransformer implements DataTransformerInterface { private ValidatorInterface $validator; diff --git a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailInput.php b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailInput.php index fe82260..b9b0ab1 100644 --- a/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailInput.php +++ b/src/Application/UseCase/Command/User/ChangeEmail/ChangeEmailInput.php @@ -6,7 +6,7 @@ use Symfony\Component\Validator\Constraints as Assert; -class ChangeEmailInput +final class ChangeEmailInput { /** * @Assert\Email diff --git a/src/Domain/Organization/Event/BillingInformationWasUpdated.php b/src/Domain/Organization/Event/BillingInformationWasUpdated.php new file mode 100644 index 0000000..3e3c21f --- /dev/null +++ b/src/Domain/Organization/Event/BillingInformationWasUpdated.php @@ -0,0 +1,37 @@ +companyName = $companyName; + $this->email = $email; + $this->phoneNumber = $phoneNumber; + $this->companyAddress = $companyAddress; + $this->updatedAt = $updatedAt; + } +} diff --git a/src/Domain/Organization/Event/OrganizationWasCreated.php b/src/Domain/Organization/Event/OrganizationWasCreated.php new file mode 100644 index 0000000..2c3e3a6 --- /dev/null +++ b/src/Domain/Organization/Event/OrganizationWasCreated.php @@ -0,0 +1,33 @@ +uuid = $uuid; + $this->profile = $profile; + $this->address = $address; + $this->createdAt = $createdAt; + } +} diff --git a/src/Domain/Organization/Organization.php b/src/Domain/Organization/Organization.php new file mode 100644 index 0000000..b1b8cf7 --- /dev/null +++ b/src/Domain/Organization/Organization.php @@ -0,0 +1,73 @@ +addDomainEvent( + new OrganizationWasCreated($uuid, $organizationProfile, $address, DateTime::now()) + ); + + return $organization; + } + + public function updateBillingInformation( + string $companyName, + Email $email, + PhoneNumber $phoneNumber, + Address $address + ): void { + $this->addDomainEvent( + new BillingInformationWasUpdated($companyName, $email, $phoneNumber, $address, DateTime::now()) + ); + } + + protected function applyOrganizationWasCreated(OrganizationWasCreated $event): void + { + $this->uuid = $event->uuid; + $this->profile = $event->profile; + $this->address = $event->address; + $this->createdAt = $event->createdAt; + $this->updatedAt = $event->createdAt; + } + + protected function applyBillingInformationWasUpdated(BillingInformationWasUpdated $event): void + { + $this->billingInformation = new BillingInformation( + $event->companyName, $event->companyAddress, $event->phoneNumber, $event->email + ); + $this->updatedAt = $event->updatedAt; + } +} diff --git a/src/Domain/Organization/Repository/OrganizationRepositoryInterface.php b/src/Domain/Organization/Repository/OrganizationRepositoryInterface.php new file mode 100644 index 0000000..ffd238a --- /dev/null +++ b/src/Domain/Organization/Repository/OrganizationRepositoryInterface.php @@ -0,0 +1,15 @@ +addressLine1 = $addressLine1; + $this->addressLine2 = $addressLine2; + $this->city = $city; + $this->region = $region; + $this->country = $country; + $this->zipCode = $zipCode; + } +} diff --git a/src/Domain/Organization/ValueObject/BillingInformation.php b/src/Domain/Organization/ValueObject/BillingInformation.php new file mode 100644 index 0000000..107cff3 --- /dev/null +++ b/src/Domain/Organization/ValueObject/BillingInformation.php @@ -0,0 +1,31 @@ +companyName = $companyName; + $this->companyAddress = $companyAddress; + $this->phoneNumber = $phoneNumber; + $this->email = $email; + } +} diff --git a/src/Domain/Organization/ValueObject/OrganizationProfile.php b/src/Domain/Organization/ValueObject/OrganizationProfile.php new file mode 100644 index 0000000..ecdb768 --- /dev/null +++ b/src/Domain/Organization/ValueObject/OrganizationProfile.php @@ -0,0 +1,24 @@ +name = $name; + $this->description = $description; + $this->shorDescription = $shorDescription; + } +} diff --git a/src/Domain/Shared/ValueObject/PhoneNumber.php b/src/Domain/Shared/ValueObject/PhoneNumber.php new file mode 100644 index 0000000..b69aa45 --- /dev/null +++ b/src/Domain/Shared/ValueObject/PhoneNumber.php @@ -0,0 +1,41 @@ +phoneNumber = $phoneNumber; + } + + public static function fromString(string $phoneNumber): self + { + $util = PhoneNumberUtil::getInstance(); + try { + $phoneNumberObject = $util->parse($phoneNumber); + } catch (NumberParseException $e) { + throw new \InvalidArgumentException('Not a valid phone number'); + } + + return new self($util->format($phoneNumberObject, PhoneNumberFormat::E164)); + } + + public function toString(): string + { + return $this->phoneNumber; + } + + public function __toString(): string + { + return $this->phoneNumber; + } +} diff --git a/src/Infrastructure/Organization/Doctrine/Orm/Mapping/Organization.orm.xml b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/Organization.orm.xml new file mode 100644 index 0000000..5d09683 --- /dev/null +++ b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/Organization.orm.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.Address.orm.xml b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.Address.orm.xml new file mode 100644 index 0000000..a0d2a51 --- /dev/null +++ b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.Address.orm.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.BillingInformation.orm.xml b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.BillingInformation.orm.xml new file mode 100644 index 0000000..547b3f2 --- /dev/null +++ b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.BillingInformation.orm.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.OrganizationProfile.orm.xml b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.OrganizationProfile.orm.xml new file mode 100644 index 0000000..8f1329f --- /dev/null +++ b/src/Infrastructure/Organization/Doctrine/Orm/Mapping/ValueObject.OrganizationProfile.orm.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/Infrastructure/Organization/Repository/OrganizationStore.php b/src/Infrastructure/Organization/Repository/OrganizationStore.php new file mode 100644 index 0000000..f3d5eca --- /dev/null +++ b/src/Infrastructure/Organization/Repository/OrganizationStore.php @@ -0,0 +1,37 @@ +oneByIdOrException($uuid->getBytes()); + } + + public function store(Organization $organization): void + { + $this->register($organization); + } +} diff --git a/src/Infrastructure/Shared/Doctrine/PhoneNumberType.php b/src/Infrastructure/Shared/Doctrine/PhoneNumberType.php new file mode 100644 index 0000000..847f86e --- /dev/null +++ b/src/Infrastructure/Shared/Doctrine/PhoneNumberType.php @@ -0,0 +1,55 @@ +getName(), ['null', PhoneNumber::class]); + } + + return $value->toString(); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if (null === $value || $value instanceof PhoneNumber) { + return $value; + } + + try { + $phoneNumber = PhoneNumber::fromString($value); + } catch (Throwable $e) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), + $platform->getDateTimeFormatString()); + } + + return $phoneNumber; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } + + public function getName() + { + return self::TYPE; + } +} diff --git a/src/Infrastructure/Shared/Migration/2020/09/Version20200922090134.php b/src/Infrastructure/Shared/Migration/2020/09/Version20200922090134.php new file mode 100644 index 0000000..96ca5fc --- /dev/null +++ b/src/Infrastructure/Shared/Migration/2020/09/Version20200922090134.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE organization (uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', profile_name VARCHAR(255) NOT NULL, profile_description LONGTEXT NOT NULL, profile_shor_description LONGTEXT NOT NULL, billing_information_company_name VARCHAR(255) DEFAULT NULL, billing_information_phone_number VARCHAR(255) DEFAULT NULL COMMENT \'(DC2Type:phone_number)\', billing_information_email VARCHAR(255) DEFAULT NULL COMMENT \'(DC2Type:email)\', billing_information_company_address_addressLine1 VARCHAR(255) DEFAULT NULL, billing_information_company_address_addressLine2 VARCHAR(255) DEFAULT NULL, billing_information_company_address_city VARCHAR(255) DEFAULT NULL, billing_information_company_address_region VARCHAR(255) DEFAULT NULL, billing_information_company_address_country VARCHAR(255) DEFAULT NULL, billing_information_company_address_zipCode VARCHAR(255) DEFAULT NULL, address_addressLine1 VARCHAR(255) DEFAULT NULL, address_addressLine2 VARCHAR(255) DEFAULT NULL, address_city VARCHAR(255) DEFAULT NULL, address_region VARCHAR(255) DEFAULT NULL, address_country VARCHAR(255) DEFAULT NULL, address_zipCode VARCHAR(255) DEFAULT NULL, PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE organization'); + } +} diff --git a/symfony.lock b/symfony.lock index 7b954d7..768609d 100644 --- a/symfony.lock +++ b/symfony.lock @@ -104,6 +104,12 @@ "fig/link-util": { "version": "1.1.1" }, + "giggsey/libphonenumber-for-php": { + "version": "8.12.9" + }, + "giggsey/locale": { + "version": "1.9" + }, "lcobucci/jwt": { "version": "3.3.3" }, From 02775d463ddd7bfd0ad960ee0b6689294a6efd5c Mon Sep 17 00:00:00 2001 From: Sviatoslav Melnyk Date: Thu, 24 Sep 2020 08:24:09 +0200 Subject: [PATCH 3/3] Signup in organization and tenant context --- config/api_platform/User/User.yaml | 36 +++++++ .../packages/doctrine/dbal/type/string.yaml | 2 + config/packages/security.yaml | 7 +- .../SignUp/SignUpInputDataTransformer.php | 5 +- .../SignUpInOrganizationCommand.php | 44 ++++++++ .../SignUpInOrganizationDataTransformer.php | 53 ++++++++++ .../SignUpInOrganizationHandler.php | 32 ++++++ .../SignUpInOrganizationInput.php | 28 +++++ src/Domain/Organization/Organization.php | 5 + .../ValueObject/OrganizationProfile.php | 5 + .../Event/OrganizationHasBeenAssigned.php | 25 +++++ src/Domain/User/Event/UserRoleChanged.php | 25 +++++ .../Event/UserWasCreatedInOrganization.php | 34 ++++++ src/Domain/User/User.php | 100 ++++++++++++++++++ src/Domain/User/UserRole.php | 41 +++++++ src/Domain/User/ValueObject/Role.php | 47 ++++++++ .../Tenant/Context/TenantContext.php | 49 +++++++++ .../Tenant/Context/TenantContextInterface.php | 14 +++ .../Resolver/TenantOrganizationResolver.php | 30 ++++++ .../Resolver/TenantResolverInterface.php | 13 +++ .../Organization/Tenant/Tenant.php | 30 ++++++ .../Organization/Tenant/TenantInterface.php | 14 +++ .../Tenant/TenantOrganization.php | 30 ++++++ .../Shared/Doctrine/DateTimeType.php | 2 +- .../Shared/Doctrine/RoleType.php | 55 ++++++++++ .../2020/09/Version20200923061249.php | 33 ++++++ .../User/Doctrine/Orm/Mapping/User.orm.xml | 5 + .../Doctrine/Orm/Mapping/UserRole.orm.xml | 22 ++++ 28 files changed, 782 insertions(+), 4 deletions(-) create mode 100644 src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationCommand.php create mode 100644 src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationDataTransformer.php create mode 100644 src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationHandler.php create mode 100644 src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationInput.php create mode 100644 src/Domain/User/Event/OrganizationHasBeenAssigned.php create mode 100644 src/Domain/User/Event/UserRoleChanged.php create mode 100644 src/Domain/User/Event/UserWasCreatedInOrganization.php create mode 100644 src/Domain/User/UserRole.php create mode 100644 src/Domain/User/ValueObject/Role.php create mode 100644 src/Infrastructure/Organization/Tenant/Context/TenantContext.php create mode 100644 src/Infrastructure/Organization/Tenant/Context/TenantContextInterface.php create mode 100644 src/Infrastructure/Organization/Tenant/Resolver/TenantOrganizationResolver.php create mode 100644 src/Infrastructure/Organization/Tenant/Resolver/TenantResolverInterface.php create mode 100644 src/Infrastructure/Organization/Tenant/Tenant.php create mode 100644 src/Infrastructure/Organization/Tenant/TenantInterface.php create mode 100644 src/Infrastructure/Organization/Tenant/TenantOrganization.php create mode 100644 src/Infrastructure/Shared/Doctrine/RoleType.php create mode 100644 src/Infrastructure/Shared/Migration/2020/09/Version20200923061249.php create mode 100644 src/Infrastructure/User/Doctrine/Orm/Mapping/UserRole.orm.xml diff --git a/config/api_platform/User/User.yaml b/config/api_platform/User/User.yaml index 3bb6fef..a676544 100644 --- a/config/api_platform/User/User.yaml +++ b/config/api_platform/User/User.yaml @@ -51,6 +51,42 @@ Acme\Domain\User\User: description: Request has been received and will be treated later 400: description: Invalid input + signup_in_organization: + method: POST + path: 'organization/signup' + messenger: true + input: Acme\Application\UseCase\Command\User\SignUpInOrganization\SignUpInOrganizationInput + output: false + validate: false + status: 202 + openapi_context: + summary: Create a user account + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + format: uuid + required: true + description: Uuid Version 4 https://www.uuidgenerator.net/ + email: + type: string + format: email + required: true + password: + type: string + format: password + required: true + example: yourstrongpassword + responses: + 202: + description: Request has been received and will be treated later + 400: + description: Invalid input itemOperations: change_email: method: PUT diff --git a/config/packages/doctrine/dbal/type/string.yaml b/config/packages/doctrine/dbal/type/string.yaml index 9b7e9c1..888e353 100644 --- a/config/packages/doctrine/dbal/type/string.yaml +++ b/config/packages/doctrine/dbal/type/string.yaml @@ -4,7 +4,9 @@ doctrine: email: Acme\Infrastructure\Shared\Doctrine\EmailType hashed_password: Acme\Infrastructure\Shared\Doctrine\HashedPasswordType phone_number: Acme\Infrastructure\Shared\Doctrine\PhoneNumberType + role: Acme\Infrastructure\Shared\Doctrine\RoleType mapping_types: email: string hashed_password: string phone_number: string + role: string diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 487f58b..f22542c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -36,6 +36,11 @@ security: stateless: true anonymous: true + api_signup_in_organization: + pattern: ^/api/organization/signup + stateless: true + anonymous: true + api_secured: pattern: ^/api provider: users @@ -64,7 +69,7 @@ security: - { path: ^/profile/*, roles: ROLE_USER } - { path: ^/api/healthz, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/api/auth, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/signup, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/organization/signup, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Application/UseCase/Command/User/SignUp/SignUpInputDataTransformer.php b/src/Application/UseCase/Command/User/SignUp/SignUpInputDataTransformer.php index a4b68f5..496629a 100644 --- a/src/Application/UseCase/Command/User/SignUp/SignUpInputDataTransformer.php +++ b/src/Application/UseCase/Command/User/SignUp/SignUpInputDataTransformer.php @@ -6,6 +6,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use ApiPlatform\Core\Validator\ValidatorInterface; +use InvalidArgumentException; final class SignUpInputDataTransformer implements DataTransformerInterface { @@ -18,8 +19,8 @@ public function __construct(ValidatorInterface $validator) public function transform($object, string $to, array $context = []) { - if (!$object instanceof SignUpInput) { - throw new \InvalidArgumentException(\sprintf('Object is not an instance of %s', SignUpInput::class)); + if (false === $object instanceof SignUpInput) { + throw new InvalidArgumentException(sprintf('Object is not an instance of %s', SignUpInput::class)); } $this->validator->validate($object, $context); diff --git a/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationCommand.php b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationCommand.php new file mode 100644 index 0000000..3987fed --- /dev/null +++ b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationCommand.php @@ -0,0 +1,44 @@ +uuid = Uuid::fromString($uuid); + $this->credentials = new Credentials(Email::fromString($email), HashedPassword::encode($plainPassword)); + $this->organization = $organization; + } + + public function uuid(): UuidInterface + { + return $this->uuid; + } + + public function credentials(): Credentials + { + return $this->credentials; + } + + public function organization(): Organization + { + return $this->organization; + } +} diff --git a/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationDataTransformer.php b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationDataTransformer.php new file mode 100644 index 0000000..0160895 --- /dev/null +++ b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationDataTransformer.php @@ -0,0 +1,53 @@ +validator = $validator; + $this->tenantContext = $tenantContext; + } + + public function transform($object, string $to, array $context = []) + { + if (false === $object instanceof SignUpInOrganizationInput) { + throw new InvalidArgumentException(sprintf('Object is not an instance of %s', SignUpInOrganizationInput::class)); + } + + $this->validator->validate($object, $context); + + $tenant = $this->tenantContext->getTenant(); + if (false === $tenant instanceof TenantOrganization) { + throw new InvalidArgumentException(/*todo: must be organization*/); + } + + return new SignUpInOrganizationCommand( + $object->uuid, + $object->email, + $object->password, + $tenant->getOrganization() + ); + } + + public function supportsTransformation($data, string $to, array $context = []): bool + { + return SignUpInOrganizationInput::class === ($context['input']['class'] ?? null); + } +} diff --git a/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationHandler.php b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationHandler.php new file mode 100644 index 0000000..7d5e74e --- /dev/null +++ b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationHandler.php @@ -0,0 +1,32 @@ +userRepository = $userRepository; + $this->customerEmailUniquenessChecker = $customerEmailUniquenessChecker; + } + + public function __invoke(SignUpInOrganizationCommand $command): void + { + $user = User::createInOrganization($command->uuid(), $command->credentials(), $command->organization(), $this->customerEmailUniquenessChecker); + + $this->userRepository->store($user); + } +} diff --git a/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationInput.php b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationInput.php new file mode 100644 index 0000000..fa9feaa --- /dev/null +++ b/src/Application/UseCase/Command/User/SignUpInOrganization/SignUpInOrganizationInput.php @@ -0,0 +1,28 @@ +profile->getName(); + } + public function updateBillingInformation( string $companyName, Email $email, diff --git a/src/Domain/Organization/ValueObject/OrganizationProfile.php b/src/Domain/Organization/ValueObject/OrganizationProfile.php index ecdb768..c692c70 100644 --- a/src/Domain/Organization/ValueObject/OrganizationProfile.php +++ b/src/Domain/Organization/ValueObject/OrganizationProfile.php @@ -21,4 +21,9 @@ public function __construct( $this->description = $description; $this->shorDescription = $shorDescription; } + + public function getName(): string + { + return $this->name; + } } diff --git a/src/Domain/User/Event/OrganizationHasBeenAssigned.php b/src/Domain/User/Event/OrganizationHasBeenAssigned.php new file mode 100644 index 0000000..a9cdd88 --- /dev/null +++ b/src/Domain/User/Event/OrganizationHasBeenAssigned.php @@ -0,0 +1,25 @@ +organization = $organization; + $this->role = $role; + $this->updatedAt = $updatedAt; + } +} diff --git a/src/Domain/User/Event/UserRoleChanged.php b/src/Domain/User/Event/UserRoleChanged.php new file mode 100644 index 0000000..637e787 --- /dev/null +++ b/src/Domain/User/Event/UserRoleChanged.php @@ -0,0 +1,25 @@ +organization = $organization; + $this->role = $role; + $this->updatedAt = $updatedAt; + } +} diff --git a/src/Domain/User/Event/UserWasCreatedInOrganization.php b/src/Domain/User/Event/UserWasCreatedInOrganization.php new file mode 100644 index 0000000..4fbd93a --- /dev/null +++ b/src/Domain/User/Event/UserWasCreatedInOrganization.php @@ -0,0 +1,34 @@ +uuid = $uuid; + $this->credentials = $credentials; + $this->organization = $organization; + $this->createdAt = $createdAt; + } +} diff --git a/src/Domain/User/User.php b/src/Domain/User/User.php index 8e76aa2..53365fe 100644 --- a/src/Domain/User/User.php +++ b/src/Domain/User/User.php @@ -6,15 +6,24 @@ use Acme\Domain\AggregateRootBehaviourTrait; use Acme\Domain\AggregateRootInterface; +use Acme\Domain\Organization\Organization; use Acme\Domain\Shared\ValueObject\DateTime; +use Acme\Domain\User\Event\OrganizationHasBeenAssigned; +use Acme\Domain\User\Event\UserRoleChanged; use Acme\Domain\User\Event\UserEmailChanged; use Acme\Domain\User\Event\UserSignedIn; use Acme\Domain\User\Event\UserWasCreated; +use Acme\Domain\User\Event\UserWasCreatedInOrganization; use Acme\Domain\User\Exception\InvalidCredentialsException; use Acme\Domain\User\Specification\Checker\CustomerEmailUniquenessCheckerInterface; use Acme\Domain\User\Specification\Rule\CustomerEmailMustBeUniqueRule; use Acme\Domain\User\ValueObject\Auth\Credentials; use Acme\Domain\User\ValueObject\Email; +use Acme\Domain\User\ValueObject\Role; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Expr\Comparison; use Ramsey\Uuid\UuidInterface; use Webmozart\Assert\Assert; @@ -24,12 +33,20 @@ final class User implements AggregateRootInterface private Credentials $credentials; + /** @var Collection|UserRole[] */ + private Collection $userRoles; + private DateTime $createdAt; private DateTime $updatedAt; private DateTime $loggedAt; + public function __construct() + { + $this->userRoles = new ArrayCollection(); + } + public static function create( UuidInterface $uuid, Credentials $credentials, @@ -43,6 +60,20 @@ public static function create( return $user; } + public static function createInOrganization( + UuidInterface $uuid, + Credentials $credentials, + Organization $organization, + CustomerEmailUniquenessCheckerInterface $customerEmailUniquenessChecker + ): self { + static::checkRule(new CustomerEmailMustBeUniqueRule($customerEmailUniquenessChecker, $credentials->email)); + + $user = new static(); + $user->addDomainEvent(new UserWasCreatedInOrganization($uuid, $credentials, $organization, DateTime::now())); + + return $user; + } + public function signIn(string $plainPassword): void { if (!$this->credentials->password->match($plainPassword)) { @@ -60,6 +91,17 @@ public function changeEmail( $this->addDomainEvent(new UserEmailChanged($this->uuid, $email, DateTime::now())); } + public function assignOrganization(Organization $organization, Role $role): void + { + //todo: must check if organization has been assigned already + $this->addDomainEvent(new OrganizationHasBeenAssigned($organization, $role, DateTime::now())); + } + + public function changeUserRole(Organization $organization, Role $role): void + { + $this->addDomainEvent(new UserRoleChanged($organization, $role, DateTime::now())); + } + protected function applyUserWasCreated(UserWasCreated $event): void { $this->uuid = $event->uuid; @@ -68,6 +110,23 @@ protected function applyUserWasCreated(UserWasCreated $event): void $this->setCreatedAt($event->createdAt); } + protected function applyUserWasCreatedInOrganization(UserWasCreatedInOrganization $event): void + { + $this->uuid = $event->uuid; + + $this->setCredentials($event->credentials); + + $userRole = new UserRole(); + $userRole->setRole(Role::create()); + $userRole->setOrganization($event->organization); + $userRole->setUser($this); + + $this->userRoles->add($userRole); + + $this->setCreatedAt($event->createdAt); + $this->setUpdatedAt($event->createdAt); + } + protected function applyUserSignedIn(UserSignedIn $event): void { // When we will add Event Sourcing support we will record UserSignedIn event to the Event Store @@ -89,6 +148,47 @@ protected function applyUserEmailChanged(UserEmailChanged $event): void $this->setUpdatedAt($event->updatedAt); } + protected function applyUserRoleChanged(UserRoleChanged $event): void + { + //todo: complete this event + $this->changeUserRoleInOrganization($event->organization, $event->role); + $this->setUpdatedAt($event->updatedAt); + } + + protected function applyOrganizationHasBeenAssigned(OrganizationHasBeenAssigned $event): void + { + $userRole = new UserRole(); + $userRole->setRole($event->role); + $userRole->setOrganization($event->organization); + $userRole->setUser($this); + + $this->userRoles->add($userRole); + + $this->setUpdatedAt($event->updatedAt); + } + + protected function changeUserRoleInOrganization(Organization $organization, Role $role): void + { + $criteria = Criteria::create() + ->where(new Comparison('organization', Comparison::EQ, $organization)); + + $result = $this->userRoles->matching($criteria); + + if (0 === $result->count()) { + throw new InvalidCredentialsException(/* todo: User is not a member of the organization */); + } + + if (1 === $result->count()) { + /** @var UserRole $userRole */ + $userRole = $result->first(); + $userRole->setRole($role); + + return; + } + + throw new InvalidCredentialsException(/* todo: something wen't wrong */); + } + public function getCredentials(): Credentials { return $this->credentials; diff --git a/src/Domain/User/UserRole.php b/src/Domain/User/UserRole.php new file mode 100644 index 0000000..26de5a3 --- /dev/null +++ b/src/Domain/User/UserRole.php @@ -0,0 +1,41 @@ +uuid = Uuid::uuid4(); + } + + public function setRole(Role $role): void + { + $this->role = $role; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function setOrganization(Organization $organization): void + { + $this->organization = $organization; + } +} diff --git a/src/Domain/User/ValueObject/Role.php b/src/Domain/User/ValueObject/Role.php new file mode 100644 index 0000000..fef6186 --- /dev/null +++ b/src/Domain/User/ValueObject/Role.php @@ -0,0 +1,47 @@ +name = $name; + } + + public static function create(): self + { + return self::fromString(self::ROLE_USER); + } + + public static function fromString(string $name): self + { + Assert::inArray($name, self::ROLES); + + return new self($name); + } + + public function toString(): string + { + return $this->name; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Infrastructure/Organization/Tenant/Context/TenantContext.php b/src/Infrastructure/Organization/Tenant/Context/TenantContext.php new file mode 100644 index 0000000..647f3fa --- /dev/null +++ b/src/Infrastructure/Organization/Tenant/Context/TenantContext.php @@ -0,0 +1,49 @@ +tenantResolver = $tenantResolver; + $this->requestStack = $requestStack; + } + + public function getTenant(): ?TenantInterface + { + if (null !== $this->tenant) { + return $this->tenant; + } + + $currentRequest = $this->requestStack->getCurrentRequest(); + $tenant = $this->tenantResolver->resolve($currentRequest); + + if (null === $tenant) { + return null; + } + + $this->setTenant($tenant); + + return $tenant; + } + + public function setTenant(TenantInterface $tenant): void + { + $this->tenant = $tenant; + } +} diff --git a/src/Infrastructure/Organization/Tenant/Context/TenantContextInterface.php b/src/Infrastructure/Organization/Tenant/Context/TenantContextInterface.php new file mode 100644 index 0000000..566ae69 --- /dev/null +++ b/src/Infrastructure/Organization/Tenant/Context/TenantContextInterface.php @@ -0,0 +1,14 @@ +organizationStore = $organizationStore; + } + + public function resolve(?Request $request): TenantInterface + { + $tenantUuid = $request->headers->get('X-Organization'); + + $organization = $this->organizationStore->get(Uuid::fromString($tenantUuid)); + + return TenantOrganization::createFromOrganization($organization); + } +} diff --git a/src/Infrastructure/Organization/Tenant/Resolver/TenantResolverInterface.php b/src/Infrastructure/Organization/Tenant/Resolver/TenantResolverInterface.php new file mode 100644 index 0000000..528d48e --- /dev/null +++ b/src/Infrastructure/Organization/Tenant/Resolver/TenantResolverInterface.php @@ -0,0 +1,13 @@ +code = $code; + } + + public function setCode(string $code): void + { + $this->code = $code; + } + + public function getCode(): string + { + return $this->code; + } + + public function __toString(): string + { + return $this->getCode(); + } +} diff --git a/src/Infrastructure/Organization/Tenant/TenantInterface.php b/src/Infrastructure/Organization/Tenant/TenantInterface.php new file mode 100644 index 0000000..966c963 --- /dev/null +++ b/src/Infrastructure/Organization/Tenant/TenantInterface.php @@ -0,0 +1,14 @@ +getUuid()->toString()); + $tenant->setOrganization($organization); + + return $tenant; + } + + public function getOrganization(): Organization + { + return $this->organization; + } + + public function setOrganization(Organization $organization): void + { + $this->organization = $organization; + } +} diff --git a/src/Infrastructure/Shared/Doctrine/DateTimeType.php b/src/Infrastructure/Shared/Doctrine/DateTimeType.php index eddb3a8..3b533c0 100644 --- a/src/Infrastructure/Shared/Doctrine/DateTimeType.php +++ b/src/Infrastructure/Shared/Doctrine/DateTimeType.php @@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\DateTimeImmutableType; -class DateTimeType extends DateTimeImmutableType +final class DateTimeType extends DateTimeImmutableType { public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { diff --git a/src/Infrastructure/Shared/Doctrine/RoleType.php b/src/Infrastructure/Shared/Doctrine/RoleType.php new file mode 100644 index 0000000..d2540ac --- /dev/null +++ b/src/Infrastructure/Shared/Doctrine/RoleType.php @@ -0,0 +1,55 @@ +getName(), ['null', Role::class]); + } + + return $value->toString(); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if (null === $value || $value instanceof Role) { + return $value; + } + + try { + $phoneNumber = Role::fromString($value); + } catch (Throwable $e) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), + $platform->getDateTimeFormatString()); + } + + return $phoneNumber; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } + + public function getName() + { + return self::TYPE; + } +} diff --git a/src/Infrastructure/Shared/Migration/2020/09/Version20200923061249.php b/src/Infrastructure/Shared/Migration/2020/09/Version20200923061249.php new file mode 100644 index 0000000..549659d --- /dev/null +++ b/src/Infrastructure/Shared/Migration/2020/09/Version20200923061249.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE user_role (uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', organization_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid_binary)\', user_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid_binary)\', role VARCHAR(255) NOT NULL COMMENT \'(DC2Type:role)\', INDEX IDX_2DE8C6A332C8A3DE (organization_id), INDEX IDX_2DE8C6A3A76ED395 (user_id), UNIQUE INDEX role_per_organization_idx (organization_id, user_id), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE user_role ADD CONSTRAINT FK_2DE8C6A332C8A3DE FOREIGN KEY (organization_id) REFERENCES organization (uuid)'); + $this->addSql('ALTER TABLE user_role ADD CONSTRAINT FK_2DE8C6A3A76ED395 FOREIGN KEY (user_id) REFERENCES user (uuid)'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user_role'); + } +} diff --git a/src/Infrastructure/User/Doctrine/Orm/Mapping/User.orm.xml b/src/Infrastructure/User/Doctrine/Orm/Mapping/User.orm.xml index d768f0a..23ded9c 100644 --- a/src/Infrastructure/User/Doctrine/Orm/Mapping/User.orm.xml +++ b/src/Infrastructure/User/Doctrine/Orm/Mapping/User.orm.xml @@ -8,5 +8,10 @@ + + + + + diff --git a/src/Infrastructure/User/Doctrine/Orm/Mapping/UserRole.orm.xml b/src/Infrastructure/User/Doctrine/Orm/Mapping/UserRole.orm.xml new file mode 100644 index 0000000..53f5f52 --- /dev/null +++ b/src/Infrastructure/User/Doctrine/Orm/Mapping/UserRole.orm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + +