From 743971aeab54159c2a986839138817f1b04746f6 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:30:39 +0200 Subject: [PATCH] feat(symfony): add mercure asserts --- .github/workflows/ci.yml | 90 +++++++++++++++++++ behat.yml.dist | 50 ++++++++++- composer.json | 1 + features/graphql/subscription.feature | 4 +- features/mercure/discover.feature | 2 +- phpunit.xml.dist | 1 + phpunit10.xml.dist | 1 + .../MercureSubscriptionIriGeneratorTest.php | 2 +- .../Bundle/Test/ApiTestAssertionsTrait.php | 49 ++++++++++ .../TestBundle/Document/DirectMercure.php | 27 ++++++ .../TestBundle/Entity/DirectMercure.php | 29 ++++++ tests/Fixtures/app/config/config_common.yml | 14 +-- tests/Fixtures/app/config/config_mercure.yml | 2 + tests/Fixtures/app/config/routing_mercure.yml | 2 + .../AddLinkHeaderListenerTest.php | 2 +- tests/Symfony/Bundle/Test/ApiTestCaseTest.php | 63 ++++++++++++- 16 files changed, 322 insertions(+), 17 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Document/DirectMercure.php create mode 100644 tests/Fixtures/TestBundle/Entity/DirectMercure.php create mode 100644 tests/Fixtures/app/config/config_mercure.yml create mode 100644 tests/Fixtures/app/config/routing_mercure.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8ddb29fc1a..065a8152f48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -497,6 +497,96 @@ jobs: php-coveralls --coverage_clover=build/logs/behat/clover.xml continue-on-error: true + mercure: + name: PHPUnit + Behat (PHP ${{ matrix.php }}) (Mercure) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - '8.1' + - '8.2' + fail-fast: false + env: + APP_ENV: mercure + MERCURE_URL: 'http://localhost:1337/.well-known/mercure' + MERCURE_JWT_SECRET: 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' + services: + mercure: + image: dunglas/mercure + env: + SERVER_NAME: :1337 + MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!' + MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!' + MERCURE_EXTRA_DIRECTIVES: | + # Custom directives, see https://mercure.rocks/docs/hub/config + anonymous + cors_origins * + ports: + - 1337:1337 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring, mongodb + coverage: pcov + ini-values: memory_limit=-1 + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Update project dependencies + run: composer update --no-interaction --no-progress --ansi + - name: Install PHPUnit + run: vendor/bin/simple-phpunit --version + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure + - name: Run Behat tests + run: | + mkdir -p build/logs/behat + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mercure-coverage --no-interaction + - name: Merge code coverage reports + run: | + wget -qO /usr/local/bin/phpcov https://phar.phpunit.de/phpcov.phar + chmod +x /usr/local/bin/phpcov + mkdir -p build/coverage + phpcov merge --clover build/logs/behat/clover.xml build/coverage + continue-on-error: true + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: behat-logs-php${{ matrix.php }} + path: build/logs/behat + continue-on-error: true + - name: Upload coverage results to Codecov + uses: codecov/codecov-action@v3 + with: + directory: build/logs/behat + name: behat-php${{ matrix.php }} + flags: behat + fail_ci_if_error: true + continue-on-error: true + - name: Upload coverage results to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls + export PATH="$PATH:$HOME/.composer/vendor/bin" + php-coveralls --coverage_clover=build/logs/behat/clover.xml + continue-on-error: true + elasticsearch: name: Behat (PHP ${{ matrix.php }}) (Elasticsearch) runs-on: ubuntu-latest diff --git a/behat.yml.dist b/behat.yml.dist index 6c40f85f2e5..5bb9d10b525 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -16,7 +16,7 @@ default: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@controller' + tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure' extensions: 'FriendsOfBehat\SymfonyExtension': bootstrap: 'tests/Fixtures/app/bootstrap.php' @@ -52,7 +52,7 @@ postgres: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@controller' + tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure' mongodb: suites: @@ -73,7 +73,28 @@ mongodb: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@sqlite&&~@elasticsearch&&~@!mongodb' + tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@mercure' + +mercure: + suites: + default: false + mercure: &mercure-suite + contexts: + - 'ApiPlatform\Tests\Behat\CommandContext' + - 'ApiPlatform\Tests\Behat\DoctrineContext' + - 'ApiPlatform\Tests\Behat\GraphqlContext' + - 'ApiPlatform\Tests\Behat\JsonContext' + - 'ApiPlatform\Tests\Behat\HydraContext' + - 'ApiPlatform\Tests\Behat\OpenApiContext' + - 'ApiPlatform\Tests\Behat\HttpCacheContext' + - 'ApiPlatform\Tests\Behat\JsonApiContext' + - 'ApiPlatform\Tests\Behat\JsonHalContext' + - 'ApiPlatform\Tests\Behat\MercureContext' + - 'ApiPlatform\Tests\Behat\XmlContext' + - 'Behat\MinkExtension\Context\MinkContext' + - 'behatch:context:rest' + filters: + tags: '@mercure' elasticsearch: suites: @@ -88,7 +109,7 @@ elasticsearch: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '@elasticsearch' + tags: '@elasticsearch&&~@mercure' default-coverage: suites: @@ -130,6 +151,27 @@ mongodb-coverage: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' +mercure-coverage: + suites: + default: false + mongodb: &mercure-coverage-suite + <<: *mercure-suite + contexts: + - 'ApiPlatform\Tests\Behat\CommandContext' + - 'ApiPlatform\Tests\Behat\DoctrineContext' + - 'ApiPlatform\Tests\Behat\GraphqlContext' + - 'ApiPlatform\Tests\Behat\JsonContext' + - 'ApiPlatform\Tests\Behat\HydraContext' + - 'ApiPlatform\Tests\Behat\OpenApiContext' + - 'ApiPlatform\Tests\Behat\HttpCacheContext' + - 'ApiPlatform\Tests\Behat\JsonApiContext' + - 'ApiPlatform\Tests\Behat\JsonHalContext' + - 'ApiPlatform\Tests\Behat\MercureContext' + - 'ApiPlatform\Tests\Behat\CoverageContext' + - 'ApiPlatform\Tests\Behat\XmlContext' + - 'Behat\MinkExtension\Context\MinkContext' + - 'behatch:context:rest' + elasticsearch-coverage: suites: default: false diff --git a/composer.json b/composer.json index 8d955dbc0b9..df41c85092e 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ "symfony/routing": "^6.1", "symfony/security-bundle": "^6.1", "symfony/security-core": "^6.1", + "symfony/stopwatch": "^6.1", "symfony/twig-bundle": "^6.1", "symfony/uid": "^6.1", "symfony/validator": "^6.1", diff --git a/features/graphql/subscription.feature b/features/graphql/subscription.feature index 82d8fa47dd0..75863ec04bf 100644 --- a/features/graphql/subscription.feature +++ b/features/graphql/subscription.feature @@ -164,7 +164,7 @@ Feature: GraphQL subscription support And the header "Content-Type" should be equal to "application/json" And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.id" should be equal to "/dummy_mercures/1" And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.name" should be equal to "Dummy Mercure #1" - And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks/hub\?topic=http://example.com/subscriptions/[a-f0-9]+$@" + And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks\?topic=http://example.com/subscriptions/[a-f0-9]+$@" And the JSON node "data.updateDummyMercureSubscribe.clientSubscriptionId" should be equal to "myId" When I send the following GraphQL request: @@ -182,7 +182,7 @@ Feature: GraphQL subscription support And the response should be in JSON And the header "Content-Type" should be equal to "application/json" And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.id" should be equal to "/dummy_mercures/2" - And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks/hub\?topic=http://example.com/subscriptions/[a-f0-9]+$@" + And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks\?topic=http://example.com/subscriptions/[a-f0-9]+$@" Scenario: Receive Mercure updates with different payloads from subscriptions (legacy PUT in non-standard mode) When I add "Accept" header equal to "application/ld+json" diff --git a/features/mercure/discover.feature b/features/mercure/discover.feature index dc302e50043..fecc057506f 100644 --- a/features/mercure/discover.feature +++ b/features/mercure/discover.feature @@ -6,7 +6,7 @@ Feature: Mercure discovery support @createSchema Scenario: Checks that the Mercure Link is added Given I send a "GET" request to "/dummy_mercures" - Then the header "Link" should contain '; rel="mercure"' + Then the header "Link" should contain '; rel="mercure"' Scenario: Checks that the Mercure Link is not added on endpoints where updates are not dispatched Given I send a "GET" request to "/" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4cbdc54122d..f77e7b83d32 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -39,6 +39,7 @@ mongodb + mercure diff --git a/phpunit10.xml.dist b/phpunit10.xml.dist index b2aa1ce9ead..5ada730253f 100644 --- a/phpunit10.xml.dist +++ b/phpunit10.xml.dist @@ -21,6 +21,7 @@ mongodb + mercure diff --git a/src/GraphQl/Tests/Subscription/MercureSubscriptionIriGeneratorTest.php b/src/GraphQl/Tests/Subscription/MercureSubscriptionIriGeneratorTest.php index b5ff9735183..1f0440e20e7 100644 --- a/src/GraphQl/Tests/Subscription/MercureSubscriptionIriGeneratorTest.php +++ b/src/GraphQl/Tests/Subscription/MercureSubscriptionIriGeneratorTest.php @@ -35,7 +35,7 @@ class MercureSubscriptionIriGeneratorTest extends TestCase */ protected function setUp(): void { - $this->defaultHub = new Hub('https://demo.mercure.rocks/hub', new StaticTokenProvider('xx')); + $this->defaultHub = new Hub('https://demo.mercure.rocks', new StaticTokenProvider('xx')); $this->managedHub = new Hub('https://demo.mercure.rocks/managed', new StaticTokenProvider('xx')); $this->registry = new HubRegistry($this->defaultHub, ['default' => $this->defaultHub, 'managed' => $this->managedHub]); diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 6a1f54d1c58..d1b27b3cb24 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -23,6 +23,9 @@ use PHPUnit\Framework\ExpectationFailedException; use Symfony\Bundle\FrameworkBundle\Test\BrowserKitAssertionsTrait; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\Mercure\Debug\TraceableHub; +use Symfony\Component\Mercure\HubRegistry; +use Symfony\Component\Mercure\Update; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; @@ -135,6 +138,32 @@ public static function assertMatchesResourceItemJsonSchema(string $resourceClass static::assertMatchesJsonSchema($schema->getArrayCopy()); } + /** + * @return Update[] + */ + public static function getMercureMessages(string $hubName = null): array + { + return array_map(fn (array $update) => $update['object'], self::getMercureHub($hubName)->getMessages()); + } + + public static function getMercureMessage(int $index = 0, string $hubName = null): ?Update + { + return static::getMercureMessages($hubName)[$index] ?? null; + } + + /** + * @throws \JsonException + */ + public static function assertMercureUpdateMatchesJsonSchema(Update $update, array $topics, array|object|string $jsonSchema = '', bool $private = false, string $id = null, string $type = null, int $retry = null, string $message = ''): void + { + static::assertSame($topics, $update->getTopics(), $message); + static::assertThat(json_decode($update->getData(), true, \JSON_THROW_ON_ERROR), new MatchesJsonSchema($jsonSchema), $message); + static::assertSame($private, $update->isPrivate(), $message); + static::assertSame($id, $update->getId(), $message); + static::assertSame($type, $update->getType(), $message); + static::assertSame($retry, $update->getRetry(), $message); + } + private static function getHttpClient(Client $newClient = null): ?Client { static $client; @@ -185,4 +214,24 @@ private static function getResourceMetadataCollectionFactory(): ?ResourceMetadat return $resourceMetadataFactoryCollection; } + + private static function getMercureRegistry(): HubRegistry + { + $container = static::getContainer(); + if ($container->has(HubRegistry::class)) { + return $container->get(HubRegistry::class); + } + + static::fail('A client must have Mercure enabled to make update assertions. Did you forget to require symfony/mercure?'); + } + + private static function getMercureHub(string $name = null): TraceableHub + { + $hub = self::getMercureRegistry()->getHub($name); + if (!$hub instanceof TraceableHub) { + static::fail('You must enabled the profiler to make Mercure update assertions.'); + } + + return $hub; + } } diff --git a/tests/Fixtures/TestBundle/Document/DirectMercure.php b/tests/Fixtures/TestBundle/Document/DirectMercure.php new file mode 100644 index 00000000000..1f856a44b27 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/DirectMercure.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource(mercure: true, extraProperties: ['standard_put' => false])] +#[ODM\Document] +class DirectMercure +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + public $id; + #[ODM\Field(type: 'string')] + public $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/DirectMercure.php b/tests/Fixtures/TestBundle/Entity/DirectMercure.php new file mode 100644 index 00000000000..0d0292b418e --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DirectMercure.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource(mercure: ['enable_async_update' => false])] +#[ORM\Entity] +class DirectMercure +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public $id; + #[ORM\Column] + public $name; +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 2c0ce7d31c8..ec7cf370031 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -1,3 +1,9 @@ +parameters: + container.autowiring.strict_mode: true + .container.dumper.inline_class_loader: true + env(MERCURE_URL): https://demo.mercure.rocks + env(MERCURE_JWT_SECRET): eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM + doctrine: dbal: driver: 'pdo_sqlite' @@ -19,8 +25,8 @@ web_profiler: mercure: hubs: default: - url: https://demo.mercure.rocks/hub - jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyJmb28iLCJiYXIiXSwicHVibGlzaCI6WyJmb28iXX19.LRLvirgONK13JgacQ_VbcjySbVhkSmHy3IznH3tA9PM + url: '%env(MERCURE_URL)%' + jwt: '%env(MERCURE_JWT_SECRET)%' api_platform: title: 'My Dummy API' @@ -88,10 +94,6 @@ api_platform: mercure: include_type: true -parameters: - container.autowiring.strict_mode: true - .container.dumper.inline_class_loader: true - services: test.client: class: ApiPlatform\Tests\Fixtures\TestBundle\BrowserKit\Client diff --git a/tests/Fixtures/app/config/config_mercure.yml b/tests/Fixtures/app/config/config_mercure.yml new file mode 100644 index 00000000000..3cc6d1aa082 --- /dev/null +++ b/tests/Fixtures/app/config/config_mercure.yml @@ -0,0 +1,2 @@ +imports: + - { resource: config_test.yml } diff --git a/tests/Fixtures/app/config/routing_mercure.yml b/tests/Fixtures/app/config/routing_mercure.yml new file mode 100644 index 00000000000..a53938bb9c0 --- /dev/null +++ b/tests/Fixtures/app/config/routing_mercure.yml @@ -0,0 +1,2 @@ +_main: + resource: routing_test.yml diff --git a/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php b/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php index cad163b65e1..2d69c112492 100644 --- a/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php +++ b/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php @@ -55,7 +55,7 @@ public function testAddLinkHeader(string $expected, Request $request): void public static function provider(): \Iterator { yield ['; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', new Request()]; - yield ['; rel="mercure",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', new Request([], [], ['_links' => new GenericLinkProvider([new Link('mercure', 'https://demo.mercure.rocks/hub')])])]; + yield ['; rel="mercure",; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', new Request([], [], ['_links' => new GenericLinkProvider([new Link('mercure', 'https://demo.mercure.rocks')])])]; } public function testSkipWhenPreflightRequest(): void diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index a337a80247b..5a2984a59c5 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -209,9 +209,68 @@ public function testFindIriBy(): void $this->assertNull(self::findIriBy($resource, ['name' => 'not-exist'])); } - private function recreateSchema(): void + /** + * @group mercure + */ + public function testGetMercureMessages(): void { - self::bootKernel(); + // debug mode is required to get Mercure TraceableHub + $this->recreateSchema(['debug' => true, 'environment' => 'mercure']); + + self::createClient()->request('POST', '/direct_mercures', [ + 'headers' => [ + 'content-type' => 'application/ld+json', + 'accept' => 'application/ld+json', + ], + 'body' => '{"name": "Hello World!"}', + ]); + $this->assertResponseIsSuccessful(); + $this->assertCount(1, self::getMercureMessages()); + self::assertMercureUpdateMatchesJsonSchema( + update: self::getMercureMessage(), + topics: ['http://localhost/direct_mercures/1'], + jsonSchema: <<get('doctrine')->getManager();