[FEATURE] Add symfony dependency injection for core and extbase 55/58255/71
authorBenjamin Franzke <bfr@qbus.de>
Thu, 11 Jul 2019 08:13:04 +0000 (10:13 +0200)
committerBenjamin Franzke <bfr@qbus.de>
Thu, 11 Jul 2019 09:35:11 +0000 (11:35 +0200)
This feature is a combined approach using two containers/concepts:

1) symfony/dependency-injection (now exposed as official TYPO3 API):
   Supports: autoconfiguration, autowiring using Service.{yaml,php}
   files

2) service providers (fork of the experimental interfaces in
   container-interop/service-provider, sometimes called PSR-11+)
   Supports: factory-style configuration using plain php code
   provided for internal use and possible future public use.

1) Symfony dependency injection is provided to be used by extensions
and throughout the TYPO3 core for (auto)wiring of services (classes).
Extensions can control symfony's dependency injection use a file
located in Configuration/Services.yaml, if they need more flexibility
they may also use Configuration/Services.php which can
use symfony's ContainerConfigurator or ContainerBuilder.

2) Service providers are used (within TYPO3 core) to provide
dependency injection for services used in the install tool which require
failsafe boot. This is based on container-interop/service-provider.
container-interop/service-provider is supposed to become a PSR but is
still marked experimental, therefore this commit adds an implementation
to TYPO3 provided for internal use only – it is not public API.
We do still include it (as @interal fork) right now, to adapt to this
upcoming PSR from the very beginning, in order to actually push the
development of this proposal forward.

Service providers in failsafe mode are consumed by a simplistic,
non-compiled container.
Symfony's container is too slow when running uncached (as the compilation,
recursive directory lookups and autowiring needs to be performed on every
invocation). We do not want caching for recovery maintenance tasks
which would not be able to recover from scenarios with broken caches.

Services that are configured through service providers, are both available
in the non compiled install tool container and in the symfony container.
They do *not* need to be defined twice. The two "worlds" are bridged
using a ServiceProviderCompilerPass which creates symfony DI definitions
from service-provider factories. The code is a derivative of the code from
the (outdated) symfony bundle
thecodingmachine/service-provider-bridge-bundle.

Features
--------
 * inject* methods are now supported for all classes (not just Extbase)
   It is suggested to use constructor based injection, this feature
   is available to provide maximum compatibility to the old Extbase DI.
 * Autoconfiguration based on interface implementation.
   (current) autoconfigured interface are:
     SingletonInterface,  MiddlewareInterface, RequestHandlerInterface,
     LoggerAwareInterface, ControllerInterface, ViewHelperInterface
     (we expect more to follow in subsequent commits)

Included adaptions
------------------
In order to demonstrate and to be able to verify the features of this
patch, some classes are adapted to use or support dependency injection:
 * RedirectHandler/RedirectService autowired/autoconfigured
 * Application classes and their dependencies are manually wired in
   service providers. This is required as the application runs in fail-
   safe mode when there is no configuration available (first install)
 * MiddlewareStack is composed in service providers and injected
   as a RequestHandler into the Http\Application classes (in order to
   prevent injecting the Container, which would be an anti pattern).
 * Middleware configuration is treated as service (like a registry),
   and retrieval is provided through service providers (the service
   providers default to the existing middleware configuration format)
 * The Middleware dispatcher now first tries to retrieve classes from
   the PSR-11 container, falling back to makeInstance if not available
 * Dependency Injection using symfony for all core Extbase Controllers
 * A Services.yaml is added for each core extension which contains
   classes that are used in frontend/backend context
 * A LateBoot service is added for install tool

DI vs ext_localconf – loading order, legacy makeInstance support
----------------------------------------------------------------
Dependency Injection containers solve most of the tasks that
ext_localconf.php and GeneralUtility::makeInstance solve for Singletons:
Service configuration and instance management.

(Symfony) DI could therefore be used to replace both in the long run.
Both are doing similar tasks when talking about Singletons:
They create and configure services that are stored and shared in a
central memory storage (container for DI, GU::$singletonInstances for
our legacy TYPO3 case). That means they both actually conflict right
now – in terms of: Who is responsible for creating a class?

We've made a decision to connect these both: GeneralUtility::makeInstance
will offload instantiation to the Container if the class is a) available
and b) no runtime constructor arguments have been passed to makeInstance
(which symfony DI doesn't support).

The DI container itself does provides backwards compatibility to XCLASSES
and class aliases use a new internal helper method makeInstanceForDi.

ext_localconf's main design flaw is the mixture of initialization
and configuration composition without assurance that all configuration
is available before a configuration-dependent class is instantiated.
Proper DI first reads all configuration and then performs all service
injections on demand and pre calculated using a dependency tree – and
therefore always in proper sequence.

What we *don't* want (+ reasons)
-------------------------------
 * Symfony Bundles
   pro: provide a defined way for container configuration)
   con: Requires the Symfony HTTP kernel which is not compatible with PSR7
 * Usage of Symfony\DependencyInjection\ExtensionInterface
   con: It is not useful as it cannot add new compiler passes
 * Handling of prototypes that need dependency injection *and* a way to
   to get custom constructor arguments passed (individual per class)
   While symfony DI can handle prototypes (called 'shared: false'), it
   does not support passing constructor arguments like the Extbase object
   manager does. We'll advocate to using a factory pattern for those
   prototypes instead.
 * Circular dependencies: This is not supported by symfony DI and isn't
   possible with service providers either

Future changes (left out for brevity)
-------------------------------------

 * (cli) Build tool to warmup DI cache/state during deployment
 * Adaptions to (all) core dispatchers to prefer a PSR-11
   container over GeneralUtility::makeInstance

Dependency changes
------------------
composer require symfony/dependency-injection:^4.1 symfony/config:^4.1

Releases: master
Resolves: #88689
Resolves: #84112
Change-Id: I8efd817066b528a5953c56fdd027cb94b2bb8eb3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/58255
Tested-by: Tobi Kretschmann <tobi@tobishome.de>
Tested-by: Jörg Bösche <typo3@joergboesche.de>
Tested-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Jörg Bösche <typo3@joergboesche.de>
Reviewed-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
119 files changed:
composer.json
composer.lock
typo3/sysext/about/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/adminpanel/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/backend/Classes/Http/Application.php
typo3/sysext/backend/Classes/Http/RequestHandler.php
typo3/sysext/backend/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/backend/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/backend/composer.json
typo3/sysext/belog/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/beuser/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/DependencyInjection/AutowireInjectMethodsPass.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/ContainerException.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/EnvVarProcessor.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/FailsafeContainer.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/LoggerAwarePass.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/NotFoundException.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/PublicServicePass.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/ServiceProviderCompilationPass.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/ServiceProviderInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/ServiceProviderRegistry.php [new file with mode: 0644]
typo3/sysext/core/Classes/DependencyInjection/SingletonPass.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/AbstractApplication.php
typo3/sysext/core/Classes/Http/MiddlewareDispatcher.php
typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php
typo3/sysext/core/Classes/Package/AbstractServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Classes/Package/Package.php
typo3/sysext/core/Classes/Package/PseudoServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Classes/Utility/GeneralUtility.php
typo3/sysext/core/Configuration/Services.php [new file with mode: 0644]
typo3/sysext/core/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-84112-SymfonyDependencyInjectionForCoreAndExtbase.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/FailsafeContainerTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestRegistryServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderFactoryOverride.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride2.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestStatefulServiceProvider.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderCompilationPassTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderRegistryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/MiddlewareDispatcherTest.php
typo3/sysext/core/Tests/Unit/Http/MiddlewareStackResolverTest.php
typo3/sysext/core/Tests/Unit/Package/AbstractServiceProviderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Package/Mocks/Package1ServiceProviderMock.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Package/Mocks/Package2ServiceProviderMock.php [new file with mode: 0644]
typo3/sysext/core/composer.json
typo3/sysext/extbase/Classes/Core/Bootstrap.php
typo3/sysext/extbase/Classes/Mvc/Dispatcher.php
typo3/sysext/extbase/Classes/Object/Container/Container.php
typo3/sysext/extbase/Classes/Object/ObjectManager.php
typo3/sysext/extbase/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/extbase/Classes/SignalSlot/Dispatcher.php
typo3/sysext/extbase/Configuration/Services.php [new file with mode: 0644]
typo3/sysext/extbase/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Object/Container/ContainerTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/PersistenceManagerTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/SessionTest.php
typo3/sysext/extbase/Tests/Unit/SignalSlot/DispatcherTest.php
typo3/sysext/extbase/composer.json
typo3/sysext/extbase/ext_localconf.php
typo3/sysext/extensionmanager/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/felogin/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/filelist/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/fluid/Configuration/Services.php [new file with mode: 0644]
typo3/sysext/fluid/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/fluid/composer.json
typo3/sysext/fluid_styled_content/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/form/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Http/Application.php
typo3/sysext/frontend/Classes/Middleware/EidHandler.php
typo3/sysext/frontend/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/frontend/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/frontend/composer.json
typo3/sysext/impexp/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/indexed_search/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/info/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
typo3/sysext/install/Classes/Controller/AbstractController.php
typo3/sysext/install/Classes/Controller/InstallerController.php
typo3/sysext/install/Classes/Controller/MaintenanceController.php
typo3/sysext/install/Classes/Controller/UpgradeController.php
typo3/sysext/install/Classes/Http/Application.php
typo3/sysext/install/Classes/Middleware/Maintenance.php
typo3/sysext/install/Classes/Service/ClearCacheService.php
typo3/sysext/install/Classes/Service/LateBootService.php [new file with mode: 0644]
typo3/sysext/install/Classes/Service/LoadTcaService.php
typo3/sysext/install/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/install/Tests/Unit/Controller/UpgradeControllerTest.php
typo3/sysext/install/composer.json
typo3/sysext/linkvalidator/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/lowlevel/Classes/Controller/ConfigurationController.php
typo3/sysext/lowlevel/Classes/ServiceProvider.php [new file with mode: 0644]
typo3/sysext/lowlevel/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/lowlevel/composer.json
typo3/sysext/opendocs/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/recordlist/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/recycler/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/redirects/Classes/Http/Middleware/RedirectHandler.php
typo3/sysext/redirects/Classes/Service/RedirectService.php
typo3/sysext/redirects/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php
typo3/sysext/reports/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/rte_ckeditor/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/scheduler/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/seo/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/setup/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/sys_note/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/t3editor/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/tstemplate/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/viewpage/Configuration/Services.yaml [new file with mode: 0644]
typo3/sysext/workspaces/Configuration/Services.yaml [new file with mode: 0644]

index d60e048..7790276 100644 (file)
@@ -49,7 +49,9 @@
                "psr/http-message": "~1.0",
                "psr/http-server-middleware": "^1.0",
                "psr/log": "~1.0.0",
+               "symfony/config": "^4.1",
                "symfony/console": "^4.1",
+               "symfony/dependency-injection": "^4.1",
                "symfony/expression-language": "^4.1",
                "symfony/finder": "^4.1",
                "symfony/mailer": "^4.3",
index 92026b1..151f892 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7b9b7abce9b3a820cfda31f71cb538c2",
+    "content-hash": "a46e7f1e82efeff6223502b47a7cf998",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2019-05-22T12:23:29+00:00"
         },
         {
+            "name": "symfony/config",
+            "version": "v4.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/config.git",
+                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/config/zipball/9198eea354be75794a7b1064de00d9ae9ae5090f",
+                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "symfony/filesystem": "~3.4|~4.0",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/finder": "<3.4"
+            },
+            "require-dev": {
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/event-dispatcher": "~3.4|~4.0",
+                "symfony/finder": "~3.4|~4.0",
+                "symfony/messenger": "~4.1",
+                "symfony/yaml": "~3.4|~4.0"
+            },
+            "suggest": {
+                "symfony/yaml": "To use the yaml reference dumper"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Config\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Config Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-06-08T06:33:08+00:00"
+        },
+        {
             "name": "symfony/console",
             "version": "v4.3.1",
             "source": {
             "time": "2019-06-05T13:25:51+00:00"
         },
         {
+            "name": "symfony/dependency-injection",
+            "version": "v4.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/dependency-injection.git",
+                "reference": "b851928be349c065197fdc0832f78d85139e3903"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b851928be349c065197fdc0832f78d85139e3903",
+                "reference": "b851928be349c065197fdc0832f78d85139e3903",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "psr/container": "^1.0",
+                "symfony/service-contracts": "^1.1.2"
+            },
+            "conflict": {
+                "symfony/config": "<4.3",
+                "symfony/finder": "<3.4",
+                "symfony/proxy-manager-bridge": "<3.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "psr/container-implementation": "1.0",
+                "symfony/service-implementation": "1.0"
+            },
+            "require-dev": {
+                "symfony/config": "^4.3",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/yaml": "~3.4|~4.0"
+            },
+            "suggest": {
+                "symfony/config": "",
+                "symfony/expression-language": "For using expressions in service container configuration",
+                "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required",
+                "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them",
+                "symfony/yaml": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\DependencyInjection\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony DependencyInjection Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-06-15T04:08:07+00:00"
+        },
+        {
             "name": "symfony/event-dispatcher",
             "version": "v4.3.1",
             "source": {
             "time": "2019-05-30T16:10:05+00:00"
         },
         {
+            "name": "symfony/filesystem",
+            "version": "v4.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/filesystem.git",
+                "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
+                "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Filesystem\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Filesystem Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-06-03T20:27:40+00:00"
+        },
+        {
             "name": "symfony/finder",
             "version": "v4.3.1",
             "source": {
             "time": "2019-05-31T18:55:30+00:00"
         },
         {
-            "name": "symfony/filesystem",
-            "version": "v4.3.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/filesystem.git",
-                "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
-                "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1.3",
-                "symfony/polyfill-ctype": "~1.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.3-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Filesystem\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Filesystem Component",
-            "homepage": "https://symfony.com",
-            "time": "2019-06-03T20:27:40+00:00"
-        },
-        {
             "name": "symfony/options-resolver",
             "version": "v4.3.1",
             "source": {
diff --git a/typo3/sysext/about/Configuration/Services.yaml b/typo3/sysext/about/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..4d4d1fd
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\About\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/adminpanel/Configuration/Services.yaml b/typo3/sysext/adminpanel/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..15ca606
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Adminpanel\:
+    resource: '../Classes/*'
index e61f0a7..e13a28e 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
@@ -32,25 +33,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class Application extends AbstractApplication
 {
     /**
-     * @var string
-     */
-    protected $requestHandler = RequestHandler::class;
-
-    /**
-     * @var string
-     */
-    protected $middlewareStack = 'backend';
-
-    /**
      * @var ConfigurationManager
      */
     protected $configurationManager;
 
     /**
+     * @param RequestHandlerInterface $requestHandler
      * @param ConfigurationManager $configurationManager
      */
-    public function __construct(ConfigurationManager $configurationManager)
+    public function __construct(RequestHandlerInterface $requestHandler, ConfigurationManager $configurationManager)
     {
+        $this->requestHandler = $requestHandler;
         $this->configurationManager = $configurationManager;
     }
 
index 85e1639..8c9e451 100644 (file)
@@ -36,6 +36,19 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class RequestHandler implements RequestHandlerInterface
 {
     /**
+     * @var RouteDispatcher
+     */
+    protected $dispatcher;
+
+    /**
+     * @param RouteDispatcher $dispatcher
+     */
+    public function __construct(RouteDispatcher $dispatcher)
+    {
+        $this->dispatcher = $dispatcher;
+    }
+
+    /**
      * Sets the global GET and POST to the values, so if people access $_GET and $_POST
      * Within hooks starting NOW (e.g. cObject), they get the "enriched" data from query params.
      *
@@ -76,8 +89,7 @@ class RequestHandler implements RequestHandlerInterface
         $this->resetGlobalsToCurrentRequest($request);
         try {
             // Check if the router has the available route and dispatch.
-            $dispatcher = GeneralUtility::makeInstance(RouteDispatcher::class);
-            return $dispatcher->dispatch($request);
+            return $this->dispatcher->dispatch($request);
         } catch (InvalidRequestTokenException $e) {
             // When token was invalid redirect to login
             $loginPage = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('login');
diff --git a/typo3/sysext/backend/Classes/ServiceProvider.php b/typo3/sysext/backend/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..f22ae77
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Backend;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
+use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Exception as CoreException;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Http\Application::class => [ static::class, 'getApplication' ],
+            Http\RequestHandler::class => [ static::class, 'getRequestHandler' ],
+            Http\RouteDispatcher::class => [ static::class, 'getRouteDispatcher' ],
+            'backend.middlewares' => [ static::class, 'getBackendMiddlewares' ],
+        ];
+    }
+
+    public static function getApplication(ContainerInterface $container): Http\Application
+    {
+        $requestHandler = new MiddlewareDispatcher(
+            $container->get(Http\RequestHandler::class),
+            $container->get('backend.middlewares'),
+            $container
+        );
+        return new Http\Application($requestHandler, $container->get(ConfigurationManager::class));
+    }
+
+    public static function getRequestHandler(ContainerInterface $container): Http\RequestHandler
+    {
+        return new Http\RequestHandler($container->get(Http\RouteDispatcher::class));
+    }
+
+    public static function getRouteDispatcher(ContainerInterface $container): Http\RouteDispatcher
+    {
+        return self::new($container, Http\RouteDispatcher::class, [$container]);
+    }
+
+    /**
+     * @param ContainerInterface $container
+     * @return array
+     * @throws InvalidDataException
+     * @throws CoreException
+     */
+    public static function getBackendMiddlewares(ContainerInterface $container): array
+    {
+        return $container->get(MiddlewareStackResolver::class)->resolve('backend');
+    }
+}
diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..1e41e82
--- /dev/null
@@ -0,0 +1,13 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Backend\:
+    resource: '../Classes/*'
+
+  # Temporary workaround until testing framework loads EXT:fluid in functional tests
+  # @todo: Fix typo3/testing-framework and remove this
+  TYPO3\CMS\Backend\View\BackendTemplateView:
+    autoconfigure: false
index d6f1585..aa1f4a8 100644 (file)
@@ -33,6 +33,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Backend\\ServiceProvider",
                                "protected": true,
                                "partOfFactoryDefault": true,
                                "partOfMinimalUsableSystem": true
diff --git a/typo3/sysext/belog/Configuration/Services.yaml b/typo3/sysext/belog/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..3dea960
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Belog\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/beuser/Configuration/Services.yaml b/typo3/sysext/beuser/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..2415fd4
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Beuser\:
+    resource: '../Classes/*'
index 36c481f..75f657c 100644 (file)
@@ -18,7 +18,6 @@ use Composer\Autoload\ClassLoader;
 use Doctrine\Common\Annotations\AnnotationReader;
 use Doctrine\Common\Annotations\AnnotationRegistry;
 use Psr\Container\ContainerInterface;
-use Psr\Container\NotFoundExceptionInterface;
 use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
 use TYPO3\CMS\Core\Cache\Backend\NullBackend;
 use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
@@ -28,6 +27,7 @@ use TYPO3\CMS\Core\Cache\Exception\InvalidCacheException;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
 use TYPO3\CMS\Core\Imaging\IconRegistry;
 use TYPO3\CMS\Core\IO\PharStreamWrapperInterceptor;
 use TYPO3\CMS\Core\Log\LogManager;
@@ -79,128 +79,74 @@ class Bootstrap
             $failsafe = true;
         }
         static::populateLocalConfiguration($configurationManager);
+
+        $logManager = new LogManager($requestId);
+        // LogManager is used by the core ErrorHandler (using GeneralUtility::makeInstance),
+        // therefore we have to push the LogManager to GeneralUtility, in case there
+        // happen errors before we call GeneralUtility::setContainer().
+        GeneralUtility::setSingletonInstance(LogManager::class, $logManager);
+
         static::initializeErrorHandling();
         static::initializeIO();
 
         $disableCaching = $failsafe ? true : false;
-
-        $logManager = new LogManager($requestId);
         $coreCache = static::createCache('core', $disableCaching);
         $packageManager = static::createPackageManager(
             $failsafe ? FailsafePackageManager::class : PackageManager::class,
             $coreCache
         );
 
-        // Push singleton instances to GeneralUtility and ExtensionManagementUtility
-        // They should be fetched through a container (later) but currently a PackageManager
+        // Push PackageManager instance to ExtensionManagementUtility
+        // Should be fetched through the container (later) but currently a PackageManager
         // singleton instance is required by PackageManager->activePackageDuringRuntime
-        GeneralUtility::setSingletonInstance(LogManager::class, $logManager);
         GeneralUtility::setSingletonInstance(PackageManager::class, $packageManager);
         ExtensionManagementUtility::setPackageManager($packageManager);
-
         static::initializeRuntimeActivatedPackagesFromConfiguration($packageManager);
 
         static::setDefaultTimezone();
         static::setMemoryLimit();
 
         $assetsCache = static::createCache('assets', $disableCaching);
-        if (!$failsafe) {
-            IconRegistry::setCache($assetsCache);
-            PageRenderer::setCache($assetsCache);
-            static::loadTypo3LoadedExtAndExtLocalconf(true, $coreCache);
-            static::unsetReservedGlobalVariables();
-            static::loadBaseTca(true, $coreCache);
-            static::checkEncryptionKey();
-        }
 
-        // Create the global CacheManager singleton instance and inject early cache instances.
-        // This can be removed once we have a system wide dependency injection container, where
-        // the CacheManager instance could be created on demand (early cache instances would
-        // be injected as dependency from $defaultContainerEntries)
-        $cacheManager = static::createCacheManager($disableCaching, [$coreCache, $assetsCache]);
-        GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManager);
+        $bootState = new \stdClass;
+        $bootState->done = false;
+
+        $builder = new ContainerBuilder([
+            'env.is_unix' => Environment::isUnix(),
+            'env.is_windows' => Environment::isWindows(),
+            'env.is_cli' => Environment::isCli(),
+            'env.is_composer_mode' => Environment::isComposerMode(),
 
-        $defaultContainerEntries = [
             ClassLoader::class => $classLoader,
-            'request.id' => $requestId,
+            ApplicationContext::class => Environment::getContext(),
             ConfigurationManager::class => $configurationManager,
             LogManager::class => $logManager,
+            'cache.disabled' => $disableCaching,
             'cache.core' => $coreCache,
             'cache.assets' => $assetsCache,
-            CacheManager::class => $cacheManager,
             PackageManager::class => $packageManager,
-        ];
-
-        return new class($defaultContainerEntries) implements ContainerInterface {
-            /**
-             * @var array
-             */
-            private $entries;
-
-            /**
-             * @param array $entries
-             */
-            public function __construct(array $entries)
-            {
-                $this->entries = $entries;
-            }
 
-            /**
-             * @param string $id Identifier of the entry to look for.
-             * @return bool
-             */
-            public function has($id)
-            {
-                if (isset($this->entries[$id])) {
-                    return true;
-                }
+            // @internal
+            'boot.state' => $bootState,
+        ]);
 
-                switch ($id) {
-                case \TYPO3\CMS\Frontend\Http\Application::class:
-                case \TYPO3\CMS\Backend\Http\Application::class:
-                case \TYPO3\CMS\Install\Http\Application::class:
-                case \TYPO3\CMS\Core\Console\CommandApplication::class:
-                    return true;
-                }
+        $container = $builder->createDependencyInjectionContainer($packageManager, $coreCache, $failsafe);
 
-                return false;
-            }
+        // Push the container to GeneralUtility as we want to make sure its
+        // makeInstance() method creates classes using the container from now on.
+        GeneralUtility::setContainer($container);
 
-            /**
-             * Method get() as specified in ContainerInterface
-             *
-             * @param string $id
-             * @return mixed
-             * @throws NotFoundExceptionInterface
-             */
-            public function get($id)
-            {
-                $entry = null;
-
-                if (isset($this->entries[$id])) {
-                    return $this->entries[$id];
-                }
-
-                switch ($id) {
-                case \TYPO3\CMS\Frontend\Http\Application::class:
-                case \TYPO3\CMS\Backend\Http\Application::class:
-                case \TYPO3\CMS\Install\Http\Application::class:
-                    $entry = new $id($this->get(ConfigurationManager::class));
-                    break;
-                case \TYPO3\CMS\Core\Console\CommandApplication::class:
-                    $entry = new $id;
-                    break;
-                default:
-                    throw new class($id . ' not found', 1518638338) extends \Exception implements NotFoundExceptionInterface {
-                    };
-                    break;
-                }
-
-                $this->entries[$id] = $entry;
+        if (!$failsafe) {
+            IconRegistry::setCache($assetsCache);
+            PageRenderer::setCache($assetsCache);
+            static::loadTypo3LoadedExtAndExtLocalconf(true, $coreCache);
+            static::unsetReservedGlobalVariables();
+            static::loadBaseTca(true, $coreCache);
+            static::checkEncryptionKey();
+        }
+        $bootState->done = true;
 
-                return $entry;
-            }
-        };
+        return $container;
     }
 
     /**
@@ -354,7 +300,7 @@ class Bootstrap
      * @return FrontendInterface
      * @internal
      */
-    protected static function createCache(string $identifier, bool $disableCaching = false): FrontendInterface
+    public static function createCache(string $identifier, bool $disableCaching = false): FrontendInterface
     {
         $configuration = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$identifier] ?? [];
 
@@ -387,26 +333,6 @@ class Bootstrap
     }
 
     /**
-     * Initialize caching framework, and re-initializes it (e.g. in the install tool) by recreating the instances
-     * again despite the Singleton instance
-     *
-     * @param bool $disableCaching
-     * @param array $defaultCaches
-     * @return CacheManager
-     * @internal This is not a public API method, do not use in own extensions
-     */
-    public static function createCacheManager(bool $disableCaching = false, array $defaultCaches = []): CacheManager
-    {
-        $cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'];
-        $cacheManager = new CacheManager($disableCaching);
-        $cacheManager->setCacheConfigurations($cacheConfigurations);
-        foreach ($defaultCaches as $cache) {
-            $cacheManager->registerCache($cache, $cacheConfigurations[$cache->getIdentifier()]['groups'] ?? ['all']);
-        }
-        return $cacheManager;
-    }
-
-    /**
      * Set default timezone
      */
     protected static function setDefaultTimezone()
diff --git a/typo3/sysext/core/Classes/DependencyInjection/AutowireInjectMethodsPass.php b/typo3/sysext/core/Classes/DependencyInjection/AutowireInjectMethodsPass.php
new file mode 100644 (file)
index 0000000..eac3daa
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass;
+use Symfony\Component\DependencyInjection\Definition;
+
+/**
+ * Looks for definitions with autowiring enabled and registers their corresponding "inject*" methods as setters.
+ */
+class AutowireInjectMethodsPass extends AbstractRecursivePass
+{
+    /**
+     * @param mixed $value
+     * @param bool $isRoot
+     * @return mixed
+     */
+    protected function processValue($value, $isRoot = false)
+    {
+        $value = parent::processValue($value, $isRoot);
+
+        if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
+            return $value;
+        }
+        if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
+            return $value;
+        }
+
+        $alreadyCalledMethods = [];
+
+        foreach ($value->getMethodCalls() as list($method)) {
+            $alreadyCalledMethods[strtolower($method)] = true;
+        }
+
+        $addInitCall = false;
+
+        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
+            $r = $reflectionMethod;
+
+            if ($r->isConstructor() || isset($alreadyCalledMethods[strtolower($r->name)])) {
+                continue;
+            }
+
+            if ($reflectionMethod->isPublic() && strpos($reflectionMethod->name, 'inject') === 0) {
+                if ($reflectionMethod->name === 'injectSettings') {
+                    continue;
+                }
+
+                $value->addMethodCall($reflectionMethod->name);
+            }
+
+            if ($reflectionMethod->name === 'initializeObject' && $reflectionMethod->isPublic()) {
+                $addInitCall = true;
+            }
+        }
+
+        if ($addInitCall) {
+            // Add call to initializeObject() which is required by classes that need to perform
+            // constructions tasks after the inject* method based injection of dependencies.
+            $value->addMethodCall('initializeObject');
+        }
+
+        return $value;
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php b/typo3/sysext/core/Classes/DependencyInjection/ContainerBuilder.php
new file mode 100644 (file)
index 0000000..d9414c0
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder;
+use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
+use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
+use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * @internal
+ */
+class ContainerBuilder
+{
+    /**
+     * @var string
+     */
+    protected $cacheIdentifier;
+
+    /**
+     * @var array
+     */
+    protected $defaultServices;
+
+    /**
+     * @var string
+     */
+    protected $serviceProviderRegistryServiceName = 'service_provider_registry';
+
+    /**
+     * @param array $earlyInstances
+     */
+    public function __construct(array $earlyInstances)
+    {
+        $this->defaultServices = $earlyInstances + [ self::class => $this ];
+    }
+
+    /**
+     * @param PackageManager $packageManager
+     * @param FrontendInterface $cache
+     * @param bool $failsafe
+     * @return ContainerInterface
+     */
+    public function createDependencyInjectionContainer(PackageManager $packageManager, FrontendInterface $cache, bool $failsafe = false): ContainerInterface
+    {
+        $serviceProviderRegistry = new ServiceProviderRegistry($packageManager, $failsafe);
+
+        if ($failsafe) {
+            return new FailsafeContainer($serviceProviderRegistry, $this->defaultServices);
+        }
+
+        $container = null;
+
+        $cacheIdentifier = $this->getCacheIdentifier();
+        $containerClassName = $cacheIdentifier;
+
+        if ($cache->has($cacheIdentifier)) {
+            $cache->requireOnce($cacheIdentifier);
+        } else {
+            $containerBuilder = $this->buildContainer($packageManager, $serviceProviderRegistry);
+            $code = $this->dumpContainer($containerBuilder, $cache);
+
+            // In theory we could use the $containerBuilder directly as $container,
+            // but as we patch the compiled source to use
+            // GeneralUtility::makeInstanceForDi, we need to use the compiled container.
+            // Once we remove support for singletons configured in ext_localconf.php
+            // and $GLOBALS['TYPO_CONF_VARS']['SYS']['Objects'], we can remove this,
+            // and use `$container = $containerBuilder` directly
+            if ($cache->has($cacheIdentifier)) {
+                $cache->requireOnce($cacheIdentifier);
+            } else {
+                // $cacheIdentifier may be unavailable if the 'core' cache iis configured to
+                // use the NullBackend
+                eval($code);
+            }
+        }
+        $fullyQualifiedContainerClassName = '\\' . $containerClassName;
+        $container = new $fullyQualifiedContainerClassName();
+
+        foreach ($this->defaultServices as $id => $service) {
+            $container->set('_early.' . $id, $service);
+        }
+
+        $container->set($this->serviceProviderRegistryServiceName, $serviceProviderRegistry);
+
+        return $container;
+    }
+
+    /**
+     * @param PackageManager $packageManager
+     * @param ServiceProviderRegistry $registry
+     * @return SymfonyContainerBuilder
+     */
+    protected function buildContainer(PackageManager $packageManager, ServiceProviderRegistry $registry): SymfonyContainerBuilder
+    {
+        $containerBuilder = new SymfonyContainerBuilder();
+
+        $containerBuilder->addCompilerPass(new ServiceProviderCompilationPass($registry, $this->serviceProviderRegistryServiceName));
+
+        $packages = $packageManager->getActivePackages();
+        foreach ($packages as $package) {
+            $diConfigDir = $package->getPackagePath() . 'Configuration/';
+            if (file_exists($diConfigDir . 'Services.php')) {
+                $phpFileLoader = new PhpFileLoader($containerBuilder, new FileLocator($diConfigDir));
+                $phpFileLoader->load('Services.php');
+            }
+            if (file_exists($diConfigDir . 'Services.yaml')) {
+                $yamlFileLoader = new YamlFileLoader($containerBuilder, new FileLocator($diConfigDir));
+                $yamlFileLoader->load('Services.yaml');
+            }
+        }
+        // Store defaults entries in the DIC container
+        // We need to use a workaround using aliases for synthetic services
+        // But that's common in symfony (same technique is used to provide the
+        // symfony container interface as well.
+        foreach (array_keys($this->defaultServices) as $id) {
+            $syntheticId = '_early.' . $id;
+            $containerBuilder->register($syntheticId)->setSynthetic(true)->setPublic(true);
+            $containerBuilder->setAlias($id, $syntheticId)->setPublic(true);
+        }
+
+        $containerBuilder->compile();
+
+        return $containerBuilder;
+    }
+
+    /**
+     * @param SymfonyContainerBuilder $containerBuilder
+     * @param FrontendInterface $cache
+     * @return string
+     */
+    protected function dumpContainer(SymfonyContainerBuilder $containerBuilder, FrontendInterface $cache): string
+    {
+        $cacheIdentifier = $this->getCacheIdentifier();
+        $containerClassName = $cacheIdentifier;
+
+        $phpDumper = new PhpDumper($containerBuilder);
+        $code = $phpDumper->dump(['class' => $containerClassName]);
+        $code = str_replace('<?php', '', $code);
+        // We need to patch the generated source code to use GeneralUtility::makeInstanceForDi() instead of `new`.
+        // This is ugly, but has to stay, as long as we support SingletonInstances to be created/retrieved
+        // through GeneralUtility::makeInstance.
+        $code = str_replace(', )', ')', preg_replace('/new ([^\(]+)\(/', '\\TYPO3\\CMS\\Core\\Utility\\GeneralUtility::makeInstanceForDi(\\1::class, ', $code));
+
+        $cache->set($cacheIdentifier, $code);
+
+        return $code;
+    }
+
+    /**
+     * @return string
+     */
+    protected function getCacheIdentifier(): string
+    {
+        return $this->cacheIdentifier ?? $this->createCacheIdentifier();
+    }
+
+    /**
+     * @return string
+     */
+    protected function createCacheIdentifier(): string
+    {
+        return $this->cacheIdentifier = 'DependencyInjectionContainer_' . sha1(TYPO3_version . Environment::getProjectPath() . 'DependencyInjectionContainer');
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ContainerException.php b/typo3/sysext/core/Classes/DependencyInjection/ContainerException.php
new file mode 100644 (file)
index 0000000..a7b7774
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerExceptionInterface;
+use TYPO3\CMS\Core\Exception;
+
+/**
+ * @internal
+ */
+class ContainerException extends Exception implements ContainerExceptionInterface
+{
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/EnvVarProcessor.php b/typo3/sysext/core/Classes/DependencyInjection/EnvVarProcessor.php
new file mode 100644 (file)
index 0000000..5264434
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
+use TYPO3\CMS\Core\Core\Environment;
+
+/**
+ * @internal
+ */
+class EnvVarProcessor implements EnvVarProcessorInterface
+{
+    /**
+     * @param string $prefix The namespace of the variable
+     * @param string $name The name of the variable within the namespace
+     * @param \Closure $getEnv A closure that allows fetching more env vars
+     * @return mixed
+     * @throws RuntimeException on error
+     */
+    public function getEnv($prefix, $name, \Closure $getEnv)
+    {
+        $callable = [Environment::class, 'get' . ucfirst($name)];
+        if (!is_callable($callable)) {
+            $callable = [Environment::class, 'is' . ucfirst($name)];
+            if (!is_callable($callable)) {
+                throw new \RuntimeException('Environment ' . $name . ' not available in ' . Environment::class, 1562314987);
+            }
+        }
+        return $callable();
+    }
+
+    /**
+     * @return string[] The PHP-types managed by getEnv(), keyed by prefixes
+     */
+    public static function getProvidedTypes()
+    {
+        return [
+            'TYPO3' => 'string|bool',
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/FailsafeContainer.php b/typo3/sysext/core/Classes/DependencyInjection/FailsafeContainer.php
new file mode 100644 (file)
index 0000000..4a01f48
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * @internal
+ */
+class FailsafeContainer implements ContainerInterface
+{
+    /**
+     * @var array
+     */
+    private $entries = [];
+
+    /**
+     * @var array
+     */
+    private $factories = [];
+
+    /**
+     * Instantiate the container.
+     *
+     * Objects and parameters can be passed as argument to the constructor.
+     *
+     * @param iterable $providers The service providers to register.
+     * @param array $entries The default parameters or objects.
+     */
+    public function __construct(iterable $providers = [], array $entries = [])
+    {
+        $this->entries = $entries;
+
+        $factories = [];
+        foreach ($providers as $provider) {
+            $factories = $provider->getFactories() + $factories;
+            foreach ($provider->getExtensions() as $id => $extension) {
+                // Decorate a previously defined extension or if that is not available,
+                // create a lazy lookup to a factory from the list of vanilla factories.
+                // Lazy because we currently can not know whether a factory will only
+                // become available due to a subsequent provider.
+                $innerFactory = $this->factories[$id] ?? function (ContainerInterface $c) use (&$factories, $id) {
+                    return isset($factories[$id]) ? $factories[$id]($c) : null;
+                };
+
+                $this->factories[$id] = function (ContainerInterface $container) use ($extension, $innerFactory) {
+                    $previous = $innerFactory($container);
+                    return $extension($container, $previous);
+                };
+            }
+        }
+
+        // Add factories to the list of factories for services that were not extended.
+        // (i.e those that have not been specified in getExtensions)
+        $this->factories += $factories;
+    }
+
+    /**
+     * @param string $id
+     * @return bool
+     */
+    public function has($id)
+    {
+        return array_key_exists($id, $this->entries) || array_key_exists($id, $this->factories);
+    }
+
+    /**
+     * @param string $id
+     * @return mixed
+     */
+    private function create(string $id)
+    {
+        $factory = $this->factories[$id] ?? null;
+
+        if ((bool)$factory) {
+            // Remove factory as it is no longer required.
+            // Set factory to false to be able to detect
+            // cyclic dependency loops.
+            $this->factories[$id] = false;
+
+            return $this->entries[$id] = $factory($this);
+        }
+        if (array_key_exists($id, $this->entries)) {
+            // This condition is triggered in the unlikely case that the entry is null
+            // Note: That is because the coalesce operator used in get() can not handle that
+            return $this->entries[$id];
+        }
+        if ($factory === null) {
+            throw new NotFoundException('Container entry "' . $id . '" is not available.', 1519978105);
+        }
+        // if ($factory === false)
+        throw new ContainerException('Container entry "' . $id . '" is part of a cyclic dependency chain.', 1520175002);
+    }
+
+    /**
+     * @param string $id
+     * @return mixed
+     */
+    public function get($id)
+    {
+        return $this->entries[$id] ?? $this->create($id);
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/LoggerAwarePass.php b/typo3/sysext/core/Classes/DependencyInjection/LoggerAwarePass.php
new file mode 100644 (file)
index 0000000..0aae824
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+use TYPO3\CMS\Core\Log\Logger;
+use TYPO3\CMS\Core\Log\LogManager;
+
+/**
+ * @internal
+ */
+final class LoggerAwarePass implements CompilerPassInterface
+{
+    /**
+     * @var string
+     */
+    private $tagName;
+
+    /**
+     * @param string $tagName
+     */
+    public function __construct(string $tagName)
+    {
+        $this->tagName = $tagName;
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     */
+    public function process(ContainerBuilder $container)
+    {
+        foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
+            $definition = $container->findDefinition($id);
+            if (!$definition->isAutowired() || $definition->isAbstract()) {
+                continue;
+            }
+
+            $logger = new Definition(Logger::class);
+            $logger->setFactory([new Reference(LogManager::class), 'getLogger']);
+            $logger->setArguments([$id]);
+            $logger->setShared(false);
+
+            $definition->addMethodCall('setLogger', [$logger]);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/NotFoundException.php b/typo3/sysext/core/Classes/DependencyInjection/NotFoundException.php
new file mode 100644 (file)
index 0000000..0145bce
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\NotFoundExceptionInterface;
+use TYPO3\CMS\Core\Exception;
+
+/**
+ * @internal
+ */
+class NotFoundException extends Exception implements NotFoundExceptionInterface
+{
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/PublicServicePass.php b/typo3/sysext/core/Classes/DependencyInjection/PublicServicePass.php
new file mode 100644 (file)
index 0000000..a71ac69
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * @internal
+ */
+final class PublicServicePass implements CompilerPassInterface
+{
+    /**
+     * @var string
+     */
+    private $tagName;
+
+    /**
+     * @param string $tagName
+     */
+    public function __construct(string $tagName)
+    {
+        $this->tagName = $tagName;
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     */
+    public function process(ContainerBuilder $container)
+    {
+        foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
+            $definition = $container->findDefinition($id);
+            if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
+                continue;
+            }
+
+            $definition->setPublic(true);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderCompilationPass.php b/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderCompilationPass.php
new file mode 100644 (file)
index 0000000..5851dea
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * @internal
+ */
+class ServiceProviderCompilationPass implements CompilerPassInterface
+{
+    /**
+     * @var Registry
+     */
+    private $registry;
+
+    /**
+     * @var string
+     */
+    private $registryServiceName;
+
+    /**
+     * @param ServiceProviderRegistry $registry
+     * @param string $registryServiceName
+     */
+    public function __construct(ServiceProviderRegistry $registry, string $registryServiceName = 'service_provider_registry')
+    {
+        $this->registry = $registry;
+        $this->registryServiceName = $registryServiceName;
+    }
+
+    /**
+     * You can modify the container here before it is dumped to PHP code.
+     *
+     * @param ContainerBuilder $container
+     */
+    public function process(ContainerBuilder $container): void
+    {
+        // Now, let's store the registry in the container (an empty version of it... it has to be added dynamically at runtime):
+        $this->registerRegistry($container);
+
+        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
+            $this->registerFactories($container, $serviceProviderKey);
+        }
+
+        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
+            $this->registerExtensions($container, $serviceProviderKey);
+        }
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     */
+    private function registerRegistry(ContainerBuilder $container): void
+    {
+        $definition = new Definition(ServiceProviderRegistry::class);
+        $definition->setSynthetic(true);
+        $definition->setPublic(true);
+
+        $container->setDefinition($this->registryServiceName, $definition);
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     * @param string $serviceProviderKey
+     */
+    private function registerFactories(ContainerBuilder $container, string $serviceProviderKey): void
+    {
+        $serviceFactories = $this->registry->getFactories($serviceProviderKey);
+
+        foreach ($serviceFactories as $serviceName => $callable) {
+            $this->registerService($container, $serviceName, $serviceProviderKey, $callable);
+        }
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     * @param string $serviceProviderKey
+     */
+    private function registerExtensions(ContainerBuilder $container, string $serviceProviderKey): void
+    {
+        $serviceExtensions = $this->registry->getExtensions($serviceProviderKey);
+
+        foreach ($serviceExtensions as $serviceName => $callable) {
+            $this->extendService($container, $serviceName, $serviceProviderKey, $callable);
+        }
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     * @param string $serviceName
+     * @param string $serviceProviderKey
+     * @param callable $callable
+     */
+    private function registerService(
+        ContainerBuilder $container,
+        string $serviceName,
+        string $serviceProviderKey,
+        callable $callable
+    ): void {
+        if (!$container->has($serviceName)) {
+            // Create a new definition
+            $factoryDefinition = new Definition();
+            $container->setDefinition($serviceName, $factoryDefinition);
+        } else {
+            // Merge into an existing definition to keep possible addMethodCall/properties configurations
+            // (which act like a service extension)
+            // Retrieve the existing factory and overwrite it.
+            $factoryDefinition = $container->findDefinition($serviceName);
+            if ($factoryDefinition->isAutowired()) {
+                $factoryDefinition->setAutowired(false);
+            }
+        }
+
+        $className = $this->getReturnType($this->getReflection($callable), $serviceName);
+        $factoryDefinition->setClass($className);
+        $factoryDefinition->setPublic(true);
+
+        $staticallyCallable = $this->getStaticallyCallable($callable);
+        if ($staticallyCallable !== null) {
+            $factoryDefinition->setFactory($staticallyCallable);
+            $factoryDefinition->setArguments([
+                new Reference('service_container')
+            ]);
+        } else {
+            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'createService' ]);
+            $factoryDefinition->setArguments([
+                $serviceProviderKey,
+                $serviceName,
+                new Reference('service_container')
+            ]);
+        }
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     * @param string $serviceName
+     * @param string $serviceProviderKey
+     * @param callable $callable
+     */
+    private function extendService(ContainerBuilder $container, string $serviceName, string $serviceProviderKey, callable $callable): void
+    {
+        $finalServiceName = $serviceName;
+        $innerName = null;
+
+        $reflection = $this->getReflection($callable);
+        $className = $this->getReturnType($reflection, $serviceName);
+
+        $factoryDefinition = new Definition($className);
+        $factoryDefinition->setClass($className);
+        $factoryDefinition->setPublic(true);
+
+        if ($container->has($serviceName)) {
+            [$finalServiceName, $previousServiceName] = $this->getDecoratedServiceName($container, $serviceName);
+            $innerName = $finalServiceName . '.inner';
+
+            $factoryDefinition->setDecoratedService($previousServiceName, $innerName);
+        } elseif ($reflection->getNumberOfRequiredParameters() > 1) {
+            throw new \Exception('A registered extension for the service "' . $serviceName . '" requires the service to be available, which is missing.', 1550092654);
+        }
+
+        $staticallyCallable = $this->getStaticallyCallable($callable);
+        if ($staticallyCallable !== null) {
+            $factoryDefinition->setFactory($staticallyCallable);
+            $factoryDefinition->setArguments([
+                new Reference('service_container')
+            ]);
+        } else {
+            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'extendService' ]);
+            $factoryDefinition->setArguments([
+                $serviceProviderKey,
+                $serviceName,
+                new Reference('service_container')
+            ]);
+        }
+
+        if ($innerName !== null) {
+            $factoryDefinition->addArgument(new Reference($innerName));
+        }
+
+        $container->setDefinition($finalServiceName, $factoryDefinition);
+    }
+
+    /**
+     * @param callable $callable
+     * @return callable|null
+     */
+    private function getStaticallyCallable(callable $callable): ?callable
+    {
+        if (is_string($callable)) {
+            return $callable;
+        }
+        if (is_array($callable) && isset($callable[0]) && is_string($callable[0])) {
+            return $callable;
+        }
+
+        return null;
+    }
+
+    /**
+     * @param \ReflectionFunctionAbstract $reflection
+     * @param string $serviceName
+     * @return string
+     */
+    private function getReturnType(\ReflectionFunctionAbstract $reflection, string $serviceName): string
+    {
+        return (string)($reflection->getReturnType() ?: $serviceName);
+    }
+
+    /**
+     * @param callable $callable
+     * @return \ReflectionFunctionAbstract
+     */
+    private function getReflection(callable $callable): \ReflectionFunctionAbstract
+    {
+        if (is_array($callable) && count($callable) === 2) {
+            return new \ReflectionMethod($callable[0], $callable[1]);
+        }
+        if (is_object($callable) && !$callable instanceof \Closure) {
+            return new \ReflectionMethod($callable, '__invoke');
+        }
+
+        return new \ReflectionFunction($callable);
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     * @param string $serviceName
+     * @return array
+     */
+    private function getDecoratedServiceName(ContainerBuilder $container, string $serviceName): array
+    {
+        $counter = 1;
+        while ($container->has($serviceName . '_decorated_' . $counter)) {
+            $counter++;
+        }
+        return [
+            $serviceName . '_decorated_' . $counter,
+            $counter === 1 ? $serviceName : $serviceName . '_decorated_' . ($counter-1)
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderInterface.php b/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderInterface.php
new file mode 100644 (file)
index 0000000..968acda
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * A service provider provides entries to a container. Inspired by:
+ * https://github.com/container-interop/service-provider/blob/v0.4.0/src/ServiceProviderInterface.php
+ * @internal
+ */
+interface ServiceProviderInterface
+{
+    /**
+     * Returns a list of all container entries registered by this service provider.
+     *
+     * - the key is the entry name
+     * - the value is a callable that will return the entry, aka the **factory**
+     *
+     * Factories have the following signature:
+     *        function(\Psr\Container\ContainerInterface $container)
+     *
+     * @return callable[]
+     */
+    public function getFactories();
+
+    /**
+     * Returns a list of all container entries extended by this service provider.
+     *
+     * - the key is the entry name
+     * - the value is a callable that will return the modified entry
+     *
+     * Callables have the following signature:
+     *        function(Psr\Container\ContainerInterface $container, $previous)
+     *     or function(Psr\Container\ContainerInterface $container, $previous = null)
+     *
+     * About factories parameters:
+     *
+     * - the container (instance of `Psr\Container\ContainerInterface`)
+     * - the entry to be extended. If the entry to be extended does not exist and the parameter is nullable, `null` will be passed.
+     *
+     * @return callable[]
+     */
+    public function getExtensions();
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderRegistry.php b/typo3/sysext/core/Classes/DependencyInjection/ServiceProviderRegistry.php
new file mode 100644 (file)
index 0000000..56e451f
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Package\Package;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * A class that holds the list of service providers of a project.
+ * This class is designed so that service provider do not need to be instantiated each time the registry is filled.
+ * They can be lazily instantiated if needed.
+ * @internal
+ */
+class ServiceProviderRegistry implements \IteratorAggregate
+{
+    /**
+     * @var PackageManager
+     */
+    private $packageManager;
+
+    /**
+     * @var bool
+     */
+    private $failsafe;
+
+    /**
+     * The array with constructed values.
+     *
+     * @var array<packageKey, ServiceProviderInterface>
+     */
+    private $instances;
+
+    /**
+     * An array of service factories (the result of the call to 'getFactories'),
+     * indexed by service provider.
+     *
+     * @var array An array<packageKey, array<servicename, callable>>
+     */
+    private $serviceFactories = [];
+
+    /**
+     * An array of service extensions (the result of the call to 'getExtensions'),
+     * indexed by service provider.
+     *
+     * @var array An array<packageKey, array<servicename, callable>>
+     */
+    private $serviceExtensions = [];
+
+    /**
+     * Initializes the registry from a list of service providers.
+     * This list of service providers can be passed as ServiceProvider instances, class name string,
+     * or an array of ['class name', [constructor params...]].
+     *
+     * @param PackageManager $packageManager
+     * @param bool $failsafe
+     */
+    public function __construct(PackageManager $packageManager, bool $failsafe = false)
+    {
+        $this->packageManager = $packageManager;
+        $this->failsafe = $failsafe;
+    }
+
+    /**
+     * Whether an id exists.
+     *
+     * @param string $packageKey Key of the service provider in the registry
+     * @return bool true on success or false on failure.
+     */
+    public function has(string $packageKey): bool
+    {
+        if (isset($this->instances[$packageKey])) {
+            return true;
+        }
+
+        if ($this->packageManager->isPackageActive($packageKey)) {
+            if ($this->failsafe && $this->packageManager->getPackage($packageKey)->isPartOfMinimalUsableSystem() === false) {
+                return false;
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns service provider by id.
+     *
+     * @param string $packageKey Key of the service provider in the registry
+     * @return ServiceProviderInterface
+     */
+    public function get(string $packageKey): ServiceProviderInterface
+    {
+        return $this->instances[$packageKey] ?? $this->create($packageKey);
+    }
+
+    /**
+     * Returns service provider by id.
+     *
+     * @param string $packageKey Key of the service provider in the registry
+     * @param Package $package
+     * @return ServiceProviderInterface
+     */
+    private function create(string $packageKey, Package $package = null): ServiceProviderInterface
+    {
+        if ($package === null) {
+            if (!$this->packageManager->isPackageActive($packageKey)) {
+                throw new \InvalidArgumentException('Package ' . $packageKey . ' is not active', 1550351445);
+            }
+            $package = $this->packageManager->getPackage($packageKey);
+        }
+        $serviceProviderClassName = $package->getServiceProvider();
+        $instance = new $serviceProviderClassName($package);
+
+        if (!$instance instanceof ServiceProviderInterface) {
+            throw new \InvalidArgumentException('Service providers need to implement ' . ServiceProviderInterface::class, 1550302554);
+        }
+
+        return $this->instances[$packageKey] = $instance;
+    }
+
+    /**
+     * Returns the result of the getFactories call on service provider whose key in the registry is $packageKey.
+     * The result is cached in the registry so two successive calls will trigger `getFactories` only once.
+     *
+     * @param string $packageKey Key of the service provider in the registry
+     * @return array
+     */
+    public function getFactories(string $packageKey): array
+    {
+        return $this->serviceFactories[$packageKey] ?? ($this->serviceFactories[$packageKey] = $this->get($packageKey)->getFactories());
+    }
+
+    /**
+     * Returns the result of the getExtensions call on service provider whose key in the registry is $packageKey.
+     * The result is cached in the registry so two successive calls will trigger `getExtensions` only once.
+     *
+     * @param string $packageKey Key of the service provider in the registry
+     * @return array
+     */
+    public function getExtensions(string $packageKey): array
+    {
+        return $this->serviceExtensions[$packageKey] ?? ($this->serviceExtensions[$packageKey] = $this->get($packageKey)->getExtensions());
+    }
+
+    /**
+     * @param string $packageKey Key of the service provider in the registry
+     * @param string $serviceName Name of the service to fetch
+     * @param ContainerInterface $container
+     * @return mixed
+     */
+    public function createService(string $packageKey, string $serviceName, ContainerInterface $container)
+    {
+        $factory = $this->getFactories($packageKey)[$serviceName];
+        return $factory($container);
+    }
+
+    /**
+     * @param string $packageKey Key of the service provider in the registry
+     * @param string $serviceName Name of the service to fetch
+     * @param ContainerInterface $container
+     * @param mixed $previous
+     *
+     * @return mixed
+     */
+    public function extendService(string $packageKey, string $serviceName, ContainerInterface $container, $previous = null)
+    {
+        $extension = $this->getExtensions($packageKey)[$serviceName];
+        return $extension($container, $previous);
+    }
+
+    /**
+     * @return \Generator
+     */
+    public function getIterator(): \Generator
+    {
+        foreach ($this->packageManager->getActivePackages() as $package) {
+            if ($this->failsafe && $package->isPartOfMinimalUsableSystem() === false) {
+                continue;
+            }
+            $packageKey = $package->getPackageKey();
+            yield $packageKey => ($this->instances[$packageKey] ?? $this->create($packageKey, $package));
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/DependencyInjection/SingletonPass.php b/typo3/sysext/core/Classes/DependencyInjection/SingletonPass.php
new file mode 100644 (file)
index 0000000..cedf4da
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * @internal
+ */
+final class SingletonPass implements CompilerPassInterface
+{
+    /**
+     * @var string
+     */
+    private $tagName;
+
+    /**
+     * @param string $tagName
+     */
+    public function __construct(string $tagName)
+    {
+        $this->tagName = $tagName;
+    }
+
+    /**
+     * @param ContainerBuilder $container
+     */
+    public function process(ContainerBuilder $container)
+    {
+        foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
+            $definition = $container->findDefinition($id);
+            if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
+                continue;
+            }
+
+            // Singletons need to be shared (that's symfony's configuration for singletons)
+            // They need to be public to be available for legacy makeInstance usage.
+            $definition->setShared(true)->setPublic(true);
+        }
+    }
+}
index 788bedd..7bdf18f 100644 (file)
@@ -19,7 +19,6 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Core\ApplicationInterface;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * @internal
@@ -31,30 +30,9 @@ abstract class AbstractApplication implements ApplicationInterface
     ];
 
     /**
-     * @var string
+     * @var RequestHandlerInterface|null
      */
-    protected $requestHandler = '';
-
-    /**
-     * @var string
-     */
-    protected $middlewareStack = '';
-
-    /**
-     * @param RequestHandlerInterface $requestHandler
-     * @return MiddlewareDispatcher
-     */
-    protected function createMiddlewareDispatcher(RequestHandlerInterface $requestHandler): MiddlewareDispatcher
-    {
-        $resolver = new MiddlewareStackResolver(
-            GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class),
-            GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\DependencyOrderingService::class),
-            GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('core')
-        );
-        $middlewares = $resolver->resolve($this->middlewareStack);
-
-        return new MiddlewareDispatcher($requestHandler, $middlewares);
-    }
+    protected $requestHandler;
 
     /**
      * Outputs content
@@ -102,10 +80,7 @@ abstract class AbstractApplication implements ApplicationInterface
      */
     protected function handle(ServerRequestInterface $request): ResponseInterface
     {
-        $requestHandler = GeneralUtility::makeInstance($this->requestHandler);
-        $dispatcher = $this->createMiddlewareDispatcher($requestHandler);
-
-        return $dispatcher->handle($request);
+        return $this->requestHandler->handle($request);
     }
 
     /**
index 2084962..bf66adc 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Core\Http;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
@@ -38,13 +39,21 @@ class MiddlewareDispatcher implements RequestHandlerInterface
     protected $tip;
 
     /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
      * @param RequestHandlerInterface $kernel
      * @param array $middlewares
+     * @param ContainerInterface $container
      */
     public function __construct(
         RequestHandlerInterface $kernel,
-        array $middlewares = []
+        array $middlewares = [],
+        ContainerInterface $container = null
     ) {
+        $this->container = $container;
         $this->seedMiddlewareStack($kernel);
 
         foreach ($middlewares as $middleware) {
@@ -115,22 +124,28 @@ class MiddlewareDispatcher implements RequestHandlerInterface
      *
      * @param string $middleware
      */
-    public function lazy(string $middleware)
+    public function lazy(string $middleware): void
     {
         $next = $this->tip;
-        $this->tip = new class($middleware, $next) implements RequestHandlerInterface {
+        $this->tip = new class($middleware, $next, $this->container) implements RequestHandlerInterface {
             private $middleware;
             private $next;
+            private $container;
 
-            public function __construct(string $middleware, RequestHandlerInterface $next)
+            public function __construct(string $middleware, RequestHandlerInterface $next, ContainerInterface $container = null)
             {
                 $this->middleware = $middleware;
                 $this->next = $next;
+                $this->container = $container;
             }
 
             public function handle(ServerRequestInterface $request): ResponseInterface
             {
-                $middleware = GeneralUtility::makeInstance($this->middleware);
+                if ($this->container !== null && $this->container->has($this->middleware)) {
+                    $middleware = $this->container->get($this->middleware);
+                } else {
+                    $middleware = GeneralUtility::makeInstance($this->middleware);
+                }
 
                 if (!$middleware instanceof MiddlewareInterface) {
                     throw new \InvalidArgumentException(get_class($middleware) . ' does not implement ' . MiddlewareInterface::class, 1516821342);
index 930f9d4..95d5a90 100644 (file)
@@ -15,9 +15,9 @@ namespace TYPO3\CMS\Core\Http;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend as PhpFrontendCache;
 use TYPO3\CMS\Core\Core\Environment;
-use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 
 /**
@@ -28,9 +28,9 @@ use TYPO3\CMS\Core\Service\DependencyOrderingService;
 class MiddlewareStackResolver
 {
     /**
-     * @var PackageManager
+     * @var ContainerInterface
      */
-    protected $packageManager;
+    protected $container;
 
     /**
      * @var DependencyOrderingService
@@ -43,11 +43,11 @@ class MiddlewareStackResolver
     protected $cache;
 
     public function __construct(
-        PackageManager $packageManager,
+        ContainerInterface $container,
         DependencyOrderingService $dependencyOrderingService,
         PhpFrontendCache $cache
     ) {
-        $this->packageManager = $packageManager;
+        $this->container = $container;
         $this->dependencyOrderingService = $dependencyOrderingService;
         $this->cache = $cache;
     }
@@ -85,24 +85,13 @@ class MiddlewareStackResolver
     }
 
     /**
-     * Loop over all packages and check for a Configuration/RequestMiddlewares.php file
+     * Lazy load configuration from the container
      *
      * @return array
      */
     protected function loadConfiguration(): array
     {
-        $packages = $this->packageManager->getActivePackages();
-        $allMiddlewares = [[]];
-        foreach ($packages as $package) {
-            $packageConfiguration = $package->getPackagePath() . 'Configuration/RequestMiddlewares.php';
-            if (file_exists($packageConfiguration)) {
-                $middlewaresInPackage = require $packageConfiguration;
-                if (is_array($middlewaresInPackage)) {
-                    $allMiddlewares[] = $middlewaresInPackage;
-                }
-            }
-        }
-        return array_replace_recursive(...$allMiddlewares);
+        return $this->container->get('middlewares');
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php b/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
new file mode 100644 (file)
index 0000000..38c4794
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Package;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerAwareInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal
+ */
+abstract class AbstractServiceProvider implements ServiceProviderInterface
+{
+    /**
+     * Return the path to the package location, including trailing slash
+     * should return the value of: __DIR__ . '/../'
+     * for ServiceProviders located in the Classes/ directory
+     *
+     * @return string
+     */
+    abstract protected static function getPackagePath(): string;
+
+    /**
+     * @return array
+     */
+    abstract public function getFactories(): array;
+
+    /**
+     * @return array
+     */
+    public function getExtensions(): array
+    {
+        return [
+            'middlewares' => [ static::class, 'configureMiddlewares' ],
+        ];
+    }
+
+    /**
+     * @param ContainerInterface $container
+     * @param array $middlewares
+     * @param string $path supplied when invoked internally through PseudoServiceProvider
+     * @return array
+     */
+    public static function configureMiddlewares(ContainerInterface $container, array $middlewares, string $path = null): array
+    {
+        $packageConfiguration = ($path ?? static::getPackagePath()) . 'Configuration/RequestMiddlewares.php';
+        if (file_exists($packageConfiguration)) {
+            $middlewaresInPackage = require $packageConfiguration;
+            if (is_array($middlewaresInPackage)) {
+                $middlewares = array_replace_recursive($middlewares, $middlewaresInPackage);
+            }
+        }
+
+        return $middlewares;
+    }
+
+    /**
+     * Create an instance of a class. Supports auto injection of the logger.
+     *
+     * @param ContainerInterface $container
+     * @param string $className name of the class to instantiate, must not be empty and not start with a backslash
+     * @param array $constructorArguments Arguments for the constructor
+     * @return mixed
+     */
+    protected static function new(ContainerInterface $container, string $className, array $constructorArguments = [])
+    {
+        // Support $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'] (xclasses) and class alias maps
+        $instance = GeneralUtility::makeInstanceForDi($className, ...$constructorArguments);
+
+        if ($instance instanceof LoggerAwareInterface) {
+            $instance->setLogger($container->get(LogManager::class)->getLogger($className));
+        }
+        return $instance;
+    }
+}
index 95dcf5c..1917bea 100644 (file)
@@ -41,6 +41,15 @@ class Package implements PackageInterface
     protected $partOfMinimalUsableSystem = false;
 
     /**
+     * ServiceProvider class name. This property and the corresponding
+     * composer.json setting is internal and therefore no api (yet).
+     *
+     * @var string
+     * @internal
+     */
+    protected $serviceProvider;
+
+    /**
      * Unique key of this package.
      * @var string
      */
@@ -148,6 +157,17 @@ class Package implements PackageInterface
     }
 
     /**
+     * Get the Service Provider class name
+     *
+     * @return string
+     * @internal
+     */
+    public function getServiceProvider(): string
+    {
+        return $this->serviceProvider ?? PseudoServiceProvider::class;
+    }
+
+    /**
      * @return bool
      * @internal
      */
diff --git a/typo3/sysext/core/Classes/Package/PseudoServiceProvider.php b/typo3/sysext/core/Classes/Package/PseudoServiceProvider.php
new file mode 100644 (file)
index 0000000..1768b20
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Package;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * @internal
+ */
+final class PseudoServiceProvider extends AbstractServiceProvider
+{
+    /**
+     * @var PackageInterface
+     */
+    private $package;
+
+    /**
+     * @param PackageInterface $package
+     */
+    public function __construct(PackageInterface $package)
+    {
+        $this->package = $package;
+    }
+
+    /**
+     * @return string
+     */
+    protected static function getPackagePath(): string
+    {
+        throw new \BadMethodCallException('PseudoServiceProvider does not support the getPackagePath() method.', 1562354465);
+    }
+
+    /**
+     * @return array
+     */
+    public function getFactories(): array
+    {
+        return [];
+    }
+
+    /**
+     * @return array
+     */
+    public function getExtensions(): array
+    {
+        $packagePath = $this->package->getPackagePath();
+        $extensions = parent::getExtensions();
+
+        // The static configure*() methods in AbstractServiceProvider use the
+        // static getPackagePath() method to retrieve the package path.
+        // We can not provide a static package path for pseudo service providers,
+        // therefore we dynamically inject the package path to the static service
+        // configure methods by wrapping these in a Closure.
+        // AbstractServiceProvider configure methods are aware of this and
+        // provide an optional thrid parameter which is forwarded as
+        // dynamic path to getPackagePath().
+        foreach ($extensions as $serviceName => $previousCallable) {
+            $extensions[$serviceName] = function (ContainerInterface $container, $value) use ($previousCallable, $packagePath) {
+                return ($previousCallable)($container, $value, $packagePath);
+            };
+        }
+
+        return $extensions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..1dccda1
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Cache\CacheManager::class => [ static::class, 'getCacheManager' ],
+            Console\CommandApplication::class => [ static::class, 'getConsoleCommandApplication' ],
+            Http\MiddlewareStackResolver::class => [ static::class, 'getMiddlewareStackResolver' ],
+            Service\DependencyOrderingService::class => [ static::class, 'getDependencyOrderingService' ],
+            'middlewares' => [ static::class, 'getMiddlewares' ],
+        ];
+    }
+
+    public static function getCacheManager(ContainerInterface $container): Cache\CacheManager
+    {
+        if (!$container->get('boot.state')->done) {
+            throw new \LogicException(Cache\CacheManager::class . ' can not be injected/instantiated during ext_localconf.php loading. Use lazy loading instead.', 1549446998);
+        }
+
+        $cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
+        $disableCaching = $container->get('cache.disabled');
+        $defaultCaches = [
+            $container->get('cache.core'),
+            $container->get('cache.assets'),
+        ];
+
+        $cacheManager = self::new($container, Cache\CacheManager::class, [$disableCaching]);
+        $cacheManager->setCacheConfigurations($cacheConfigurations);
+        foreach ($defaultCaches as $cache) {
+            $cacheManager->registerCache($cache, $cacheConfigurations[$cache->getIdentifier()]['groups'] ?? ['all']);
+        }
+
+        return $cacheManager;
+    }
+
+    public static function getConsoleCommandApplication(ContainerInterface $container): Console\CommandApplication
+    {
+        return new Console\CommandApplication;
+    }
+
+    public static function getDependencyOrderingService(ContainerInterface $container): Service\DependencyOrderingService
+    {
+        return new Service\DependencyOrderingService;
+    }
+
+    public static function getMiddlewareStackResolver(ContainerInterface $container): Http\MiddlewareStackResolver
+    {
+        return new Http\MiddlewareStackResolver(
+            $container,
+            $container->get(Service\DependencyOrderingService::class),
+            $container->get('cache.core')
+        );
+    }
+
+    public static function getMiddlewares(ContainerInterface $container): array
+    {
+        return [];
+    }
+}
index 455e2ab..0a206d0 100644 (file)
@@ -15,10 +15,12 @@ namespace TYPO3\CMS\Core\Utility;
  */
 
 use GuzzleHttp\Exception\RequestException;
+use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Core\ApplicationContext;
+use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Http\RequestFactory;
@@ -52,6 +54,11 @@ class GeneralUtility
     protected static $allowHostHeaderValue = false;
 
     /**
+     * @var ContainerInterface|null
+     */
+    protected static $container;
+
+    /**
      * Singleton instances returned by makeInstance, using the class names as
      * array keys
      *
@@ -3358,6 +3365,27 @@ class GeneralUtility
     }
 
     /**
+     * @param ContainerInterface $container
+     * @internal
+     */
+    public static function setContainer(ContainerInterface $container): void
+    {
+        self::$container = $container;
+    }
+
+    /**
+     * @return ContainerInterface
+     * @internal
+     */
+    public static function getContainer(): ContainerInterface
+    {
+        if (self::$container === null) {
+            throw new \LogicException('PSR-11 Container is not available', 1549404144);
+        }
+        return self::$container;
+    }
+
+    /**
      * Creates an instance of a class taking into account the class-extensions
      * API of TYPO3. USE THIS method instead of the PHP "new" keyword.
      * Eg. "$obj = new myclass;" should be "$obj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance("myclass")" instead!
@@ -3405,6 +3433,15 @@ class GeneralUtility
         ) {
             return array_shift(self::$nonSingletonInstances[$finalClassName]);
         }
+
+        // Read service and prototypes from the DI container, this is required to
+        // support classes that require dependency injection.
+        // We operate on the original class name on purpose, as class overrides
+        // are resolved inside the container
+        if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
+            return self::$container->get($className);
+        }
+
         // Create new instance and call constructor with parameters
         $instance = new $finalClassName(...$constructorArguments);
         // Register new singleton instance
@@ -3418,6 +3455,29 @@ class GeneralUtility
     }
 
     /**
+     * Creates a class taking implementation settings and class aliases into account.
+     *
+     * Intended to be used to create objects by the dependency injection
+     * container.
+     *
+     * @param string $className name of the class to instantiate
+     * @param array<int, mixed> $constructorArguments Arguments for the constructor
+     * @return object the created instance
+     * @internal
+     */
+    public static function makeInstanceForDi(string $className, ...$constructorArguments): object
+    {
+        $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
+
+        // Return singleton instance if it is already registered (currently required for unit and functional tests)
+        if (isset(self::$singletonInstances[$finalClassName])) {
+            return self::$singletonInstances[$finalClassName];
+        }
+        // Create new instance and call constructor with parameters
+        return new $finalClassName(...$constructorArguments);
+    }
+
+    /**
      * Returns the class name for a new instance, taking into account
      * registered implementations for this class
      *
@@ -3616,6 +3676,7 @@ class GeneralUtility
      */
     public static function purgeInstances()
     {
+        self::$container = null;
         self::$singletonInstances = [];
         self::$nonSingletonInstances = [];
     }
diff --git a/typo3/sysext/core/Configuration/Services.php b/typo3/sysext/core/Configuration/Services.php
new file mode 100644 (file)
index 0000000..61fd425
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core;
+
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
+    $containerBuilder->registerForAutoconfiguration(SingletonInterface::class)->addTag('typo3.singleton');
+    $containerBuilder->registerForAutoconfiguration(LoggerAwareInterface::class)->addTag('psr.logger_aware');
+
+    // Services, to be read from container-aware dispatchers (on demand), therefore marked 'public'
+    $containerBuilder->registerForAutoconfiguration(MiddlewareInterface::class)->addTag('typo3.middleware');
+    $containerBuilder->registerForAutoconfiguration(RequestHandlerInterface::class)->addTag('typo3.request_handler');
+
+    $containerBuilder->addCompilerPass(new DependencyInjection\SingletonPass('typo3.singleton'));
+    $containerBuilder->addCompilerPass(new DependencyInjection\LoggerAwarePass('psr.logger_aware'));
+    $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.middleware'));
+    $containerBuilder->addCompilerPass(new DependencyInjection\PublicServicePass('typo3.request_handler'));
+    $containerBuilder->addCompilerPass(new DependencyInjection\AutowireInjectMethodsPass());
+};
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..74016a6
--- /dev/null
@@ -0,0 +1,30 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Core\:
+    resource: '../Classes/*'
+
+  TYPO3\CMS\Core\DependencyInjection\EnvVarProcessor:
+    tags: ['container.env_var_processor']
+
+  TYPO3\CMS\Core\Configuration\SiteConfiguration:
+    arguments:
+      $configPath: "%env(TYPO3:configPath)%/sites"
+
+  TYPO3\CMS\Core\Package\PackageManager:
+    autoconfigure: false
+
+  TYPO3\CMS\Core\Package\FailsafePackageManager:
+    autoconfigure: false
+
+  TYPO3\CMS\Core\Package\UnitTestPackageManager:
+    autoconfigure: false
+
+  TYPO3\CMS\Core\Http\MiddlewareDispatcher:
+    autoconfigure: false
+
+  TYPO3\CMS\Core\Database\Schema\SqlReader:
+    public: true
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84112-SymfonyDependencyInjectionForCoreAndExtbase.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84112-SymfonyDependencyInjectionForCoreAndExtbase.rst
new file mode 100644 (file)
index 0000000..6f8fcbe
--- /dev/null
@@ -0,0 +1,216 @@
+.. include:: ../../Includes.txt
+
+===================================================================
+Feature: #84112 - Symfony dependency injection for core and extbase
+===================================================================
+
+See :issue:`84112`
+
+Description
+===========
+
+The PHP library `symfony/dependency-injection` has been integrated and is used
+to manage system wide dependency management and injection for classes.
+With the integration provided in TYPO3 the symfony dependency injection container
+features support for Extbase and non-Extbase classes and is thus intended to
+replace the Extbase dependency injection container and object manager.
+The symfony container implements :php:`\Psr\Conteiner\ContainerInterface`
+as specified by PSR-11. This interface should be used when requiring access
+to the container.
+Therefore :php:`\TYPO3\CMS\Extbase\Object\ObjectManager` now resorts to this
+new dependency injection container and prioritizes its entries over classical
+Extbase dependency injection (which is still available), also
+:php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()` has been adapted
+to retrieve instances from the container, if possible.
+
+Classes should be adapted to avoid both, :php:`ObjectManager` and
+:php:`GeneralUtility::makeInstance()` whenever possible.
+Service dependencies should be injected via custructor injection or
+setter methods (inject methods as in Extbase are supported).
+
+Configuration
+^^^^^^^^^^^^^
+
+Extensions are encouraged to configure their classes to make use of the new
+dependency injection. A symfony flavored yaml (or, for advanced functionality,
+php) service configuration file may be used to do so. That means symfony
+dependency injection is not applied automatically, extensions need to
+define the desired dependency injection strategies. Extensions that do not
+configure dependency injection will keep working – the legacy instance
+management in :php:`\TYPO3\CMS\Extbase\Object\ObjectManager` and
+:php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()` is
+still available.
+
+Whenever service configuration or class dependencies change, the core cache needs
+to be flushed to rebuild the compiled symfony container.
+
+Autowiring
+----------
+
+A :file:`Configuration/Services.yaml` which uses autowiring pretty much
+reflects the current feature set of Extbase DI. The configuration looks like:
+
+.. code-block:: yaml
+
+    # Configuration/Services.yaml
+    services:
+      _defaults:
+        autowire: true
+        autoconfigure: true
+        public: false
+
+      Your\Namespace\:
+        resource: '../Classes/*'
+
+Extensions which have used Extbase dependency injection in the past, will want
+to enable `autowire` for a smooth migration. `autowire: true` instructs symfony
+to calculate the required dependencies from type declarations of the constructor
+and inject methods. This calculation yields to a service initialization recipe
+which is cached in php code (in TYPO3 core cache).
+Note: An extension doesn't need to use autowiring, it is free to manually
+wire dependencies in the service configuration file.
+
+It is suggested to enable `autoconfigure: true` as this will automatically
+add symfony service tags based on implemented interfaces or base classes.
+An Example: autoconfiguration ensured that classes which implement
+:php:`\TYPO3\CMS\Core\SingletonInterface` will be publicly available from the
+symfony container (which is required for legacy singleton lookups through
+:php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()`).
+
+`public: false` is a performance optimization and is therefore suggested to be
+enabled in extensions (symfony does not enable this by default for backwards
+compatibility reasons only). This settings controls which services are available
+through `\Psr\Container\ContainerInterface->get()`. Services that need to be public
+(e.g. Singletons, because they need to be shared with legacy code that uses
+:php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()`, or Extbase controllers)
+will be marked public automatically due to `autoconfigure: true` by custom
+TYPO3 provided symfony compiler passes.
+
+
+Manual wiring
+-------------
+
+Manual dependency wiring and service configuration can be used instead of
+autowiring (it can actually be combined). This speeds up container compilation
+and allows for custom service configuration/wiring. It has the drawback of
+having to write some boilerplate.
+
+.. code-block:: yaml
+
+    # Configuration/Services.yaml
+    services:
+      _defaults:
+        autoconfigure: false
+        public: false
+
+      Your\Namespace\Service\ExampleService:
+       # mark public – means this service should be accessible from $container->get()
+       # and (often more important), both GeneralUtility::makeInstance() and the Extbase
+       # ObjectManager will be able to use the Symfony DI managed service
+       public: true
+       # Defining a service to be shared is equal to TYPO3's SingletonInterface behaviour
+       shared: true
+       # Configure constructor arguments
+       arguments:
+         $siteConfiguration: '@TYPO3\CMS\Core\Configuration\SiteConfiguration'
+
+      # Example Extbase controller
+      Your\Namespace\Controller\ExampleController
+       # mark public to be dispatchable
+       public: true
+       # Defining to be a prototype, as Extbase controllers are stateful (i. e. could not be defined as singleton)
+       shared: false
+       # Configure constructor arguments
+       arguments:
+         $exampleService: '@Your\Namespace\Service\ExampleService'
+
+
+For more information please refer to the official documentation:
+https://symfony.com/doc/4.3/service_container.html
+
+
+Advanced functionality
+----------------------
+
+Container compilation and configuration can be enhanced using
+a callback function returned from :file:`Configuration/Services.php`.
+Here is an example: Given an interface :php:`MyCustomInterface`,
+you can automatically add a symfony tag for (autoconfigured) services that
+implement this interface. A compiler pass can use that tag and configure
+autoregistration into a registry service :php:`MyRegistry`.
+
+.. code-block:: php
+
+    # Configuration/Services.php
+    <?php
+    declare(strict_types = 1);
+    namespace Your\Namespace;
+
+    use Symfony\Component\DependencyInjection\ContainerBuilder;
+    use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+    return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
+        $containerBuilder->registerForAutoconfiguration(MyCustomInterface::class)->addTag('my.custom.interface');
+
+        $containerBuilder->addCompilerPass(new DependencyInjection\MyCustomPass('my.custom.interface'));
+    };
+
+.. code-block:: php
+
+    # Classes/DependencyInjection/MyCustomPass.php
+    <?php
+    declare(strict_types = 1);
+    namespace Your\Namespace\DependencyInjection;
+
+    use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+    use Symfony\Component\DependencyInjection\ContainerBuilder;
+    use Your\Namespace\MyRegistry;
+
+    final class MyCustomPass implements CompilerPassInterface
+    {
+        public function process(ContainerBuilder $container)
+        {
+            $myRegistry = $container->findDefinition(MyRegistry::class);
+
+            foreach ($container->findTaggedServiceIds('my.custom.interface') as $id => $tags) {
+                $definition = $container->findDefinition($id);
+                if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
+                    continue;
+                }
+
+                // Services that implement MyCustomInterface need to be public,
+                // to be lazy loadable by the registry via $container->get()
+                $container->findDefinition($id)->setPublic(true);
+                // Add a method call to the registry class to the (auto-generated) factory for
+                // the registry service.
+                // This supersedes explicit registrations in ext_localconf.php (which're
+                // still possible and can be combined with this autoconfiguration).
+                $myRegistry->addMethodCall('registerMyCustomInterfaceImplementation', [$id]);
+            }
+        }
+    }
+
+Impact
+======
+
+ * Symfony automatically resolves interfaces to classes when only one class
+   implementing an interface is available. Otherwise an explicit alias is required.
+   That means you SHOULD define an alias for interface to class mappings where
+   the implementation currently defaults to the interface minus the trailing Interface
+   suffix (which is the default for Extbase).
+
+ * Dependency Injection can be added to constructors of existing services
+   without being breaking. `GeneralUtility::makeInstance(ServiceName::class)`
+   will keep working,  as `makeInstance` has been adapted to resort to the
+   symfony container.
+
+ * Cyclic dependencies are not supported with Symfony DI (Extbase DI did so).
+
+ * Prototypes/Data classes (non singletons, e.g. models) that need both,
+   runtime constructor arguments (as passed to
+   :php:`\TYPO3\CMS\Extbase\Object\ObjectManager->get()`) and injected dependencies
+   are not supported in :php:`\Psr\Container\ContainerInterface->get()`.
+   It is suggested to switch to factories or stick with the object manager for now.
+
+
+.. index:: PHP-API, ext:core
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/FailsafeContainerTest.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/FailsafeContainerTest.php
new file mode 100644 (file)
index 0000000..85ab943
--- /dev/null
@@ -0,0 +1,434 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Prophecy\ObjectProphecy;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use stdClass as Service;
+use TYPO3\CMS\Core\DependencyInjection\FailsafeContainer as Container;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Testcase for the FailsafeContainer class
+ */
+class FailsafeContainerTest extends UnitTestCase
+{
+    /**
+     * @var ObjectProphecy
+     */
+    protected $providerProphecy;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->providerProphecy = $this->createServiceProviderProphecy();
+    }
+
+    protected function createServiceProviderProphecy(array $extensions = [], array $factories = []): ObjectProphecy
+    {
+        $prophecy = $this->prophesize();
+        $prophecy->willImplement(ServiceProviderInterface::class);
+        $prophecy->getFactories()->willReturn($extensions);
+        $prophecy->getExtensions()->willReturn($factories);
+        return $prophecy;
+    }
+
+    public function testImplementsInterface(): void
+    {
+        self::assertInstanceOf(ContainerInterface::class, new Container);
+    }
+
+    public function testWithString(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'param' => function () {
+                return 'value';
+            }
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertTrue($container->has('param'));
+        self::assertEquals('value', $container->get('param'));
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testGet($factory): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'service' => $factory,
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertTrue($container->has('service'));
+        self::assertInstanceOf(Service::class, $container->get('service'));
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testMultipleGetServicesShouldBeEqual($factory): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([ 'service' => $factory ]);
+        // A factory can also be used as extension, as it's based on the same signature
+        $this->providerProphecy->getExtensions()->willReturn([ 'extension' => $factory ]);
+
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        $serviceOne = $container->get('service');
+        $serviceTwo = $container->get('service');
+
+        $extensionOne = $container->get('extension');
+        $extensionTwo = $container->get('extension');
+
+        self::assertSame($serviceOne, $serviceTwo);
+        self::assertSame($extensionOne, $extensionTwo);
+    }
+
+    public function testPassesContainerAsParameter(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'service' => function () {
+                return new Service();
+            },
+            'container' => function (ContainerInterface $container) {
+                return $container;
+            }
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertNotSame($container, $container->get('service'));
+        self::assertSame($container, $container->get('container'));
+    }
+
+    public function testNullValueEntry(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'null' => function () {
+                return null;
+            }
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertTrue($container->has('null'));
+        self::assertNull($container->get('null'));
+    }
+
+    public function testNullValueEntryCallsFactoryOnlyOnce(): void
+    {
+        $calledCount = 0;
+        $factory = function () use (&$calledCount) {
+            $calledCount++;
+            return null;
+        };
+        $this->providerProphecy->getFactories()->willReturn([
+            'null' => $factory,
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertTrue($container->has('null'));
+        self::assertNull($container->get('null'));
+        self::assertTrue($container->has('null'));
+        self::assertNull($container->get('null'));
+        self::assertEquals($calledCount, 1);
+    }
+
+    public function testHas(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'service' => function () {
+                return new Service();
+            },
+            'param' => function () {
+                return 'value';
+            },
+            'int' => function () {
+                return 2;
+            },
+            'bool' => function () {
+                return false;
+            },
+            'null' => function () {
+                return null;
+            },
+            '0' => function () {
+                return 0;
+            }
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertTrue($container->has('param'));
+        self::assertTrue($container->has('service'));
+        self::assertTrue($container->has('int'));
+        self::assertTrue($container->has('bool'));
+        self::assertTrue($container->has('null'));
+        self::assertFalse($container->has('non_existent'));
+    }
+
+    public function testDefaultEntry(): void
+    {
+        $default = ['param' => 'value'];
+        $container = new Container([], $default);
+
+        self::assertSame('value', $container->get('param'));
+    }
+
+    public function testGetValidatesKeyIsPresent(): void
+    {
+        $container = new Container();
+
+        $this->expectException(NotFoundExceptionInterface::class);
+        $this->expectExceptionMessage('Container entry "foo" is not available.');
+        $container->get('foo');
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testExtension($factory): void
+    {
+        $providerA = $this->providerProphecy;
+        $providerA->getFactories()->willReturn(['service' => $factory]);
+
+        $providerB = $this->createServiceProviderProphecy();
+        $providerB->getExtensions()->willReturn([
+            'service' => function (ContainerInterface $c, Service $s) {
+                $s->value = 'value';
+                return $s;
+            },
+        ]);
+        $iterator = (function () use ($providerA, $providerB): iterable {
+            yield $providerA->reveal();
+            yield $providerB->reveal();
+        })();
+        $container = new Container($iterator);
+
+        self::assertSame('value', $container->get('service')->value);
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testExtendingLaterProvider($factory): void
+    {
+        $providerA = $this->providerProphecy;
+        $providerA->getFactories()->willReturn(['service' => $factory]);
+
+        $providerB = $this->createServiceProviderProphecy();
+        $providerB->getExtensions()->willReturn([
+            'service' => function (ContainerInterface $c, Service $s) {
+                $s->value = 'value';
+                return $s;
+            },
+        ]);
+        $container = new Container([$providerB->reveal(), $providerA->reveal()]);
+
+        self::assertSame('value', $container->get('service')->value);
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testExtendingOwnFactory($factory): void
+    {
+        $this->providerProphecy->getFactories()->willReturn(['service' => $factory]);
+        $this->providerProphecy->getExtensions()->willReturn(
+            [
+                'service' => function (ContainerInterface $c, Service $s) {
+                    $s->value = 'value';
+                    return $s;
+                },
+            ]
+        );
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertSame('value', $container->get('service')->value);
+    }
+
+    public function testExtendingNonExistingFactory(): void
+    {
+        $this->providerProphecy->getExtensions()->willReturn([
+            'service' => function (ContainerInterface $c, Service $s = null) {
+                if ($s === null) {
+                    $s = new Service();
+                }
+                $s->value = 'value';
+                return $s;
+            },
+        ]);
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        self::assertSame('value', $container->get('service')->value);
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testMultipleExtensions($factory): void
+    {
+        $providerA = $this->providerProphecy;
+        $providerA->getFactories()->willReturn(['service' => $factory]);
+
+        $providerB = $this->createServiceProviderProphecy();
+        $providerB->getExtensions()->willReturn([
+            'service' => function (ContainerInterface $c, Service $s) {
+                $s->value = '1';
+                return $s;
+            },
+        ]);
+
+        $providerC = $this->createServiceProviderProphecy();
+        $providerC->getExtensions()->willReturn([
+            'service' => function (ContainerInterface $c, Service $s) {
+                $s->value .= '2';
+                return $s;
+            },
+        ]);
+        $container = new Container([$providerA->reveal(), $providerB->reveal(), $providerC->reveal()]);
+
+        self::assertSame('12', $container->get('service')->value);
+    }
+
+    /**
+     * @dataProvider objectFactories
+     * @param mixed $factory
+     */
+    public function testEntryOverriding($factory): void
+    {
+        $providerA = $this->providerProphecy;
+        $providerA->getFactories()->willReturn(['service' => $factory]);
+
+        $providerB = $this->createServiceProviderProphecy();
+        $providerB->getFactories()->willReturn(['service' => function () {
+            return 'value';
+        }]);
+
+        $container = new Container([$providerA->reveal(), $providerB->reveal()]);
+
+        self::assertNotInstanceOf(Service::class, $container->get('service'));
+        self::assertEquals('value', $container->get('service'));
+    }
+
+    public function testCyclicDependency(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'A' => function (ContainerInterface $container) {
+                return $container->get('B');
+            },
+            'B' => function (ContainerInterface $container) {
+                return $container->get('A');
+            },
+        ]);
+
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        $this->expectException(ContainerExceptionInterface::class);
+        $this->expectExceptionMessage('Container entry "A" is part of a cyclic dependency chain.');
+        $container->get('A');
+    }
+
+    public function testCyclicDependencyRetrievedTwice(): void
+    {
+        $this->providerProphecy->getFactories()->willReturn([
+            'A' => function (ContainerInterface $container) {
+                return $container->get('B');
+            },
+            'B' => function (ContainerInterface $container) {
+                return $container->get('A');
+            },
+        ]);
+
+        $container = new Container([$this->providerProphecy->reveal()]);
+
+        $this->expectException(ContainerExceptionInterface::class);
+        $this->expectExceptionMessage('Container entry "A" is part of a cyclic dependency chain.');
+        try {
+            $container->get('A');
+        } catch (ContainerExceptionInterface $e) {
+        }
+        self::assertTrue($container->has('A'));
+        $container->get('A');
+    }
+
+    public function testNullContainer(): void
+    {
+        $container = new Container;
+        self::assertFalse($container->has('foo'));
+    }
+
+    public function testNullContainerWithDefaultEntries(): void
+    {
+        $container = new Container([], ['foo' => 'bar']);
+        self::assertTrue($container->has('foo'));
+    }
+
+    public static function factory(): Service
+    {
+        return new Service();
+    }
+
+    /**
+     * Provider for ServerProvider callables.
+     * Either a closure, a static callable or invokable.
+     */
+    public function objectFactories(): array
+    {
+        return [
+            [
+                // Static callback
+                [ self::class, 'factory']
+            ],
+            [
+                // Closure
+                function () {
+                    return new Service();
+                }
+            ],
+            [
+                // Invokable
+                new class {
+                    public function __invoke(): Service
+                    {
+                        return new Service();
+                    }
+                }
+            ],
+            [
+                // Non static factory
+                [
+                    new class {
+                        public function factory(): Service
+                        {
+                            return new Service();
+                        }
+                    },
+                    'factory'
+                ]
+            ],
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestRegistryServiceProvider.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestRegistryServiceProvider.php
new file mode 100644 (file)
index 0000000..92559ca
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+class TestRegistryServiceProvider implements ServiceProviderInterface
+{
+    public function getFactories()
+    {
+        return [
+            'serviceA' => function () {
+                return new \stdClass();
+            },
+            'param' => function () {
+                return 42;
+            },
+        ];
+    }
+
+    public function getExtensions()
+    {
+        return [
+            'serviceB' => function () {
+                return new \stdClass();
+            },
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProvider.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProvider.php
new file mode 100644 (file)
index 0000000..557c600
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+function myFunctionFactory()
+{
+    return 42;
+}
+
+class TestServiceProvider implements ServiceProviderInterface
+{
+    public function getFactories()
+    {
+        return [
+            'serviceA' => function (ContainerInterface $container): \stdClass {
+                $instance = new \stdClass();
+                $instance->serviceB = $container->get('serviceB');
+
+                return $instance;
+            },
+            'serviceB' => [ TestServiceProvider::class, 'createServiceB' ],
+            'serviceC' => function (ContainerInterface $container): \stdClass {
+                return new \stdClass();
+            },
+            'serviceD' => new class {
+                public function __invoke(ContainerInterface $container): \stdClass
+                {
+                    return new \stdClass();
+                }
+            },
+            'function' => 'TYPO3\\CMS\\Core\\Tests\\Unit\\DependencyInjection\\Fixtures\\myFunctionFactory'
+        ];
+    }
+
+    public static function createServiceB(ContainerInterface $container): \stdClass
+    {
+        $instance = new \stdClass();
+        $instance->parameter = 'localhost';
+        return $instance;
+    }
+
+    public function getExtensions()
+    {
+        return [];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderFactoryOverride.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderFactoryOverride.php
new file mode 100644 (file)
index 0000000..167e294
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+class TestServiceProviderFactoryOverride implements ServiceProviderInterface
+{
+    public function getFactories()
+    {
+        return [
+            'serviceB' => [ self::class, 'createServiceB' ],
+        ];
+    }
+
+    public static function createServiceB(ContainerInterface $container): \stdClass
+    {
+        $instance = new \stdClass();
+        $instance->parameter = 'remotehost';
+        return $instance;
+    }
+
+    public function getExtensions()
+    {
+        return [];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride.php
new file mode 100644 (file)
index 0000000..cbd60f4
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+class TestServiceProviderOverride implements ServiceProviderInterface
+{
+    public function getFactories()
+    {
+        return [];
+    }
+
+    public static function overrideServiceA(ContainerInterface $container, \stdClass $serviceA): \stdClass
+    {
+        $serviceA->newProperty = 'foo';
+        return $serviceA;
+    }
+
+    public function getExtensions()
+    {
+        return [
+            'serviceA' => [ TestServiceProviderOverride::class, 'overrideServiceA' ]
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride2.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestServiceProviderOverride2.php
new file mode 100644 (file)
index 0000000..f3063c2
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+class TestServiceProviderOverride2 implements ServiceProviderInterface
+{
+    public function getFactories()
+    {
+        return [];
+    }
+
+    public static function overrideServiceA(ContainerInterface $container, $serviceA = null): \stdClass
+    {
+        $serviceA->newProperty2 = 'bar';
+
+        return $serviceA;
+    }
+
+    public function getExtensions()
+    {
+        return [
+            'serviceA' => [self::class, 'overrideServiceA'],
+            'serviceC' => function (ContainerInterface $container, \stdClass $instance): \stdClass {
+                $instance->serviceB = $container->get('serviceB');
+
+                return $instance;
+            },
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestStatefulServiceProvider.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/Fixtures/TestStatefulServiceProvider.php
new file mode 100644 (file)
index 0000000..e34e0d6
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+
+class TestStatefulServiceProvider implements ServiceProviderInterface
+{
+    public $package;
+
+    public function __construct(\TYPO3\CMS\Core\Package\Package $package)
+    {
+        $this->package = $package;
+    }
+
+    public function getFactories()
+    {
+        return [];
+    }
+
+    public function getExtensions()
+    {
+        return [];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderCompilationPassTest.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderCompilationPassTest.php
new file mode 100644 (file)
index 0000000..501f266
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Psr\Container\ContainerInterface;
+use Psr\Log\NullLogger;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderCompilationPass;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderRegistry;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestServiceProvider;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestServiceProviderFactoryOverride;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestServiceProviderOverride;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestServiceProviderOverride2;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class ServiceProviderCompilationPassTest extends UnitTestCase
+{
+    protected function getServiceProviderRegistry(array $serviceProviders)
+    {
+        $serviceProviderRegistryProphecy = $this->prophesize(ServiceProviderRegistry::class);
+        $serviceProviderRegistryProphecy->getIterator()->will(function () use ($serviceProviders): \Generator {
+            foreach ($serviceProviders as $id => $serviceProvider) {
+                yield (string)$id => new $serviceProvider;
+            }
+        });
+
+        foreach ($serviceProviders as $id => $serviceProvider) {
+            $packageKey = (string)$id;
+
+            $instance = new $serviceProvider;
+            $factories = $instance->getFactories();
+            $extensions = $instance->getExtensions();
+
+            $serviceProviderRegistryProphecy->getFactories($id)->willReturn($factories);
+            $serviceProviderRegistryProphecy->getExtensions($id)->willReturn($extensions);
+
+            foreach ($factories as $serviceName => $factory) {
+                $serviceProviderRegistryProphecy->createService($packageKey, $serviceName, Argument::type(ContainerInterface::class))->will(function ($args) use ($factory) {
+                    return $factory($args[2]);
+                });
+            }
+            foreach ($extensions as $serviceName => $extension) {
+                $serviceProviderRegistryProphecy->extendService($packageKey, $serviceName, Argument::type(ContainerInterface::class), Argument::cetera())->will(function ($args) use ($extension) {
+                    return $extension($args[2], $args[3] ?? null);
+                });
+            }
+        }
+
+        return $serviceProviderRegistryProphecy->reveal();
+    }
+
+    protected function getContainer(array $serviceProviders, callable $configure = null)
+    {
+        static $id = 0;
+
+        $registry = $this->getServiceProviderRegistry($serviceProviders);
+        $registryServiceName = 'service_provider_registry_' . ++$id;
+
+        $container = new ContainerBuilder();
+        if ($configure !== null) {
+            $configure($container);
+        }
+        $logger = new Definition(NullLogger::class);
+        $logger->setPublic(true);
+        $container->setDefinition('logger', $logger);
+
+        $container->addCompilerPass(new ServiceProviderCompilationPass($registry, $registryServiceName));
+        $container->compile();
+        $container->set($registryServiceName, $registry);
+
+        return $container;
+    }
+
+    public function testSimpleServiceProvider()
+    {
+        $container = $this->getContainer([
+            TestServiceProvider::class
+        ]);
+
+        $serviceA = $container->get('serviceA');
+        $serviceD = $container->get('serviceD');
+
+        $this->assertInstanceOf(\stdClass::class, $serviceA);
+        $this->assertInstanceOf(\stdClass::class, $serviceD);
+        $this->assertEquals(42, $container->get('function'));
+    }
+
+    public function testServiceProviderOverrides()
+    {
+        $container = $this->getContainer([
+            TestServiceProvider::class,
+            TestServiceProviderOverride::class,
+            TestServiceProviderOverride2::class
+        ]);
+
+        $serviceA = $container->get('serviceA');
+        $serviceC = $container->get('serviceC');
+
+        $this->assertInstanceOf(\stdClass::class, $serviceA);
+        $this->assertEquals('foo', $serviceA->newProperty);
+        $this->assertEquals('bar', $serviceA->newProperty2);
+        $this->assertEquals('localhost', $serviceC->serviceB->parameter);
+    }
+
+    public function testServiceProviderFactoryOverrides()
+    {
+        $container = $this->getContainer([
+            TestServiceProvider::class,
+            TestServiceProviderFactoryOverride::class,
+        ]);
+
+        $serviceA = $container->get('serviceA');
+
+        $this->assertInstanceOf(\stdClass::class, $serviceA);
+        $this->assertEquals('remotehost', $serviceA->serviceB->parameter);
+    }
+
+    public function testServiceProviderFactoryOverridesForSymfonyDefinedServices()
+    {
+        $container = $this->getContainer(
+            [
+                TestServiceProvider::class,
+                TestServiceProviderFactoryOverride::class,
+            ],
+            function (ContainerBuilder $container) {
+                $definition = new \Symfony\Component\DependencyInjection\Definition('stdClass');
+                // property should be overriden by service provider
+                $definition->setProperty('parameter', 'remotehost');
+                // property should not be "deleted" by service provider
+                $definition->setProperty('symfony_defined_parameter', 'foobar');
+                $container->setDefinition('serviceB', $definition);
+            }
+        );
+
+        $serviceA = $container->get('serviceA');
+
+        $this->assertInstanceOf(\stdClass::class, $serviceA);
+        $this->assertEquals('remotehost', $serviceA->serviceB->parameter);
+        $this->assertEquals('foobar', $serviceA->serviceB->symfony_defined_parameter);
+    }
+
+    public function testServiceProviderFactoryOverrideResetsAutowiring()
+    {
+        $container = $this->getContainer(
+            [
+                TestServiceProvider::class,
+                TestServiceProviderFactoryOverride::class,
+            ],
+            function (ContainerBuilder $container) {
+                $definition = new \Symfony\Component\DependencyInjection\Definition('stdClass');
+                // property should be overriden by service provider
+                $definition->setProperty('parameter', 'remotehost');
+                // property should not be "deleted" by service provider
+                $definition->setProperty('symfony_defined_parameter', 'foobar');
+                $definition->setAutowired(true);
+                $container->setDefinition('serviceB', $definition);
+            }
+        );
+
+        $serviceA = $container->get('serviceA');
+
+        $this->assertInstanceOf(\stdClass::class, $serviceA);
+        $this->assertEquals('remotehost', $serviceA->serviceB->parameter);
+        $this->assertEquals('foobar', $serviceA->serviceB->symfony_defined_parameter);
+        $this->assertFalse($container->getDefinition('serviceB')->isAutowired());
+    }
+
+    public function testExceptionForNonNullableExtensionArgument()
+    {
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('A registered extension for the service "serviceA" requires the service to be available, which is missing.');
+
+        $container = $this->getContainer([
+            TestServiceProviderOverride::class,
+        ]);
+    }
+
+    public function testExceptionForInvalidFactories()
+    {
+        $this->expectException(\TypeError::class);
+
+        $registry = new ServiceProviderRegistry([
+            new class implements ServiceProviderInterface {
+                public function getFactories()
+                {
+                    return [
+                        'invalid' => 2
+                    ];
+                }
+                public function getExtensions()
+                {
+                    return [];
+                }
+            }
+        ]);
+        $container = new ContainerBuilder();
+        $registryServiceName = 'service_provider_registry_test';
+        $container->addCompilerPass(new ServiceProviderCompilationPass($registry, $registryServiceName));
+        $container->compile();
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderRegistryTest.php b/typo3/sysext/core/Tests/Unit/DependencyInjection/ServiceProviderRegistryTest.php
new file mode 100644 (file)
index 0000000..e633217
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\DependencyInjection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\DependencyInjection\ServiceProviderRegistry;
+use TYPO3\CMS\Core\Package\Package;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestRegistryServiceProvider;
+use TYPO3\CMS\Core\Tests\Unit\DependencyInjection\Fixtures\TestStatefulServiceProvider;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class ServiceProviderRegistryTest extends UnitTestCase
+{
+    /**
+     * @var PackageManager|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $packageManagerProphecy;
+
+    /**
+     * Set up this testcase
+     */
+    protected function setUp(): void
+    {
+        $this->packageManagerProphecy = $this->prophesize(PackageManager::class);
+        $this->packageManagerProphecy->isPackageActive(Argument::any())->willReturn(false);
+        $this->packageManagerProphecy->getActivePackages()->willReturn([]);
+    }
+
+    protected function mockPackages($packages)
+    {
+        $active = [];
+        foreach ($packages as $packageKey => $serviceProvider) {
+            $this->packageManagerProphecy->isPackageActive($packageKey)->willReturn(true);
+
+            $package = $this->prophesize(Package::class);
+            $package->getPackageKey()->willReturn($packageKey);
+            $package->getServiceProvider()->willReturn($serviceProvider);
+
+            $this->packageManagerProphecy->getPackage($packageKey)->willReturn($package->reveal());
+            $active[] = $package->reveal();
+        }
+
+        $this->packageManagerProphecy->getActivePackages()->willReturn($active);
+    }
+
+    public function testRegistry()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $this->assertEquals(new TestRegistryServiceProvider(), $registry->get('core'));
+    }
+
+    public function testRegistryCaches()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $this->assertEquals(new TestRegistryServiceProvider(), $registry->get('core'));
+        $this->assertSame($registry->get('core'), $registry->get('core'));
+    }
+
+    public function testRegistryPassesPackageAsConstructorArgument()
+    {
+        $this->mockPackages(['core' => TestStatefulServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $this->assertInstanceOf(TestStatefulServiceProvider::class, $registry->get('core'));
+        $this->assertInstanceOf(Package::class, $registry->get('core')->package);
+    }
+
+    public function testGetException()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $registry->get('backend');
+    }
+
+    public function testGetServices()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $services = $registry->getFactories('core');
+        $this->assertArrayHasKey('serviceA', $services);
+
+        $services2 = $registry->getFactories('core');
+
+        $this->assertSame($services['serviceA'], $services2['serviceA']);
+    }
+
+    public function testExtendServices()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $services = $registry->getExtensions('core');
+        $this->assertArrayHasKey('serviceB', $services);
+
+        $services2 = $registry->getExtensions('core');
+
+        $this->assertSame($services['serviceB'], $services2['serviceB']);
+    }
+
+    public function testGetServiceFactory()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $containerProphecy = $this->prophesize(ContainerInterface::class);
+        $service = $registry->createService('core', 'param', $containerProphecy->reveal());
+
+        $this->assertEquals(42, $service);
+    }
+
+    public function testGetServiceExtension()
+    {
+        $this->mockPackages(['core' => TestRegistryServiceProvider::class]);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $containerProphecy = $this->prophesize(ContainerInterface::class);
+        $service = $registry->extendService('core', 'serviceB', $containerProphecy->reveal(), null);
+
+        $this->assertInstanceOf(\stdClass::class, $service);
+    }
+
+    public function testIterator()
+    {
+        $packages = [
+            'core' => TestRegistryServiceProvider::class,
+            'backend' => TestRegistryServiceProvider::class
+        ];
+        $this->mockPackages($packages);
+        $registry = new ServiceProviderRegistry($this->packageManagerProphecy->reveal());
+
+        $i = 0;
+        foreach ($registry as $key => $provider) {
+            $this->assertEquals(array_keys($packages)[$i], $key);
+            $this->assertInstanceOf(TestRegistryServiceProvider::class, $registry->get($key));
+            $i++;
+        }
+    }
+}
index b5fab5f..92f8282 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Http;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
@@ -207,4 +208,34 @@ class MiddlewareDispatcherTest extends UnitTestCase
         $this->assertSame(204, $response->getStatusCode());
         $this->assertSame(['nested', 'outer'], $response->getHeader('X-TRACE'));
     }
+
+    /**
+     * @test
+     */
+    public function fetchesMiddlewareFromContainer()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return new Response;
+            }
+        };
+
+        $middleware = new class implements MiddlewareInterface {
+            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+            {
+                return (new Response)->withStatus(404);
+            }
+        };
+
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+        $containerProphecy->has('somemiddlewarename')->willReturn(true);
+        $containerProphecy->get('somemiddlewarename')->willReturn($middleware);
+
+        $dispatcher = new MiddlewareDispatcher($kernel, ['somemiddlewarename'], $containerProphecy->reveal());
+        $response = $dispatcher->handle(new ServerRequest);
+
+        $this->assertSame(404, $response->getStatusCode());
+    }
 }
index e82312a..bc3e77c 100644 (file)
@@ -16,10 +16,9 @@ namespace TYPO3\CMS\Core\Tests\Unit\Http;
  */
 
 use Prophecy\Argument;
+use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
-use TYPO3\CMS\Core\Package\Package;
-use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -33,12 +32,14 @@ class MiddlewareStackResolverTest extends UnitTestCase
      */
     public function resolveReturnsMiddlewareStack()
     {
-        $package1 = $this->prophesize(Package::class);
-        $package1->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package1/');
-        $package2 = $this->prophesize(Package::class);
-        $package2->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package2/');
-        $packageManagerProphecy = $this->prophesize(PackageManager::class);
-        $packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
+        $middlewares =  array_replace_recursive(
+            [],
+            require __DIR__ . '/Fixtures/Package1/Configuration/RequestMiddlewares.php',
+            require __DIR__ . '/Fixtures/Package2/Configuration/RequestMiddlewares.php'
+        );
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+        $containerProphecy->get('middlewares')->willReturn($middlewares);
         $dependencyOrderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
         $dependencyOrderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
         $phpFrontendCacheProphecy = $this->prophesize(PhpFrontend::class);
@@ -46,7 +47,7 @@ class MiddlewareStackResolverTest extends UnitTestCase
         $phpFrontendCacheProphecy->set(Argument::cetera())->willReturn(false);
 
         $subject = new MiddlewareStackResolver(
-            $packageManagerProphecy->reveal(),
+            $containerProphecy->reveal(),
             $dependencyOrderingServiceProphecy->reveal(),
             $phpFrontendCacheProphecy->reveal()
         );
@@ -62,8 +63,10 @@ class MiddlewareStackResolverTest extends UnitTestCase
      */
     public function resolveReturnsEmptyMiddlewareStackForZeroPackages()
     {
-        $packageManagerProphecy = $this->prophesize(PackageManager::class);
-        $packageManagerProphecy->getActivePackages()->willReturn([]);
+        $middlewares = [];
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+        $containerProphecy->get('middlewares')->willReturn($middlewares);
         $dependencyOrderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
         $dependencyOrderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
         $phpFrontendCacheProphecy = $this->prophesize(PhpFrontend::class);
@@ -71,7 +74,7 @@ class MiddlewareStackResolverTest extends UnitTestCase
         $phpFrontendCacheProphecy->set(Argument::cetera())->willReturn(false);
 
         $subject = new MiddlewareStackResolver(
-            $packageManagerProphecy->reveal(),
+            $containerProphecy->reveal(),
             $dependencyOrderingServiceProphecy->reveal(),
             $phpFrontendCacheProphecy->reveal()
         );
@@ -85,12 +88,14 @@ class MiddlewareStackResolverTest extends UnitTestCase
      */
     public function resolveAllowsDisablingAMiddleware()
     {
-        $package1 = $this->prophesize(Package::class);
-        $package1->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package1/');
-        $package2 = $this->prophesize(Package::class);
-        $package2->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package2Disables1/');
-        $packageManagerProphecy = $this->prophesize(PackageManager::class);
-        $packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
+        $middlewares =  array_replace_recursive(
+            [],
+            require __DIR__ . '/Fixtures/Package1/Configuration/RequestMiddlewares.php',
+            require __DIR__ . '/Fixtures/Package2Disables1/Configuration/RequestMiddlewares.php'
+        );
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+        $containerProphecy->get('middlewares')->willReturn($middlewares);
         $dependencyOrderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
         $dependencyOrderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
         $phpFrontendCacheProphecy = $this->prophesize(PhpFrontend::class);
@@ -98,7 +103,7 @@ class MiddlewareStackResolverTest extends UnitTestCase
         $phpFrontendCacheProphecy->set(Argument::cetera())->willReturn(false);
 
         $subject = new MiddlewareStackResolver(
-            $packageManagerProphecy->reveal(),
+            $containerProphecy->reveal(),
             $dependencyOrderingServiceProphecy->reveal(),
             $phpFrontendCacheProphecy->reveal()
         );
@@ -114,12 +119,14 @@ class MiddlewareStackResolverTest extends UnitTestCase
      */
     public function resolveAllowsReplacingAMiddleware()
     {
-        $package1 = $this->prophesize(Package::class);
-        $package1->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package1/');
-        $package2 = $this->prophesize(Package::class);
-        $package2->getPackagePath()->willReturn(__DIR__ . '/Fixtures/Package2Replaces1/');
-        $packageManagerProphecy = $this->prophesize(PackageManager::class);
-        $packageManagerProphecy->getActivePackages()->willReturn([$package1->reveal(), $package2->reveal()]);
+        $middlewares =  array_replace_recursive(
+            [],
+            require __DIR__ . '/Fixtures/Package1/Configuration/RequestMiddlewares.php',
+            require __DIR__ . '/Fixtures/Package2Replaces1/Configuration/RequestMiddlewares.php'
+        );
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+        $containerProphecy->get('middlewares')->willReturn($middlewares);
         $dependencyOrderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
         $dependencyOrderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
         $phpFrontendCacheProphecy = $this->prophesize(PhpFrontend::class);
@@ -127,7 +134,7 @@ class MiddlewareStackResolverTest extends UnitTestCase
         $phpFrontendCacheProphecy->set(Argument::cetera())->willReturn(false);
 
         $subject = new MiddlewareStackResolver(
-            $packageManagerProphecy->reveal(),
+            $containerProphecy->reveal(),
             $dependencyOrderingServiceProphecy->reveal(),
             $phpFrontendCacheProphecy->reveal()
         );
diff --git a/typo3/sysext/core/Tests/Unit/Package/AbstractServiceProviderTest.php b/typo3/sysext/core/Tests/Unit/Package/AbstractServiceProviderTest.php
new file mode 100644 (file)
index 0000000..bfa09ac
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Package;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+use TYPO3\CMS\Core\Package\Package;
+use TYPO3\CMS\Core\Package\PseudoServiceProvider;
+use TYPO3\CMS\Core\Tests\Unit\Utility\Fixtures\GeneralUtilityMakeInstanceInjectLoggerFixture;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class AbstractServiceProviderTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function configureMiddlewaresReturnsMergedMiddlewares()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $middlewares = [];
+        $middlewares = Mocks\Package1ServiceProviderMock::configureMiddlewares(
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+        $middlewares = Mocks\Package2ServiceProviderMock::configureMiddlewares(
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+
+        $expected = [
+            'testStack' => [
+                'firstMiddleware' => [
+                    'target' => 'aClassName',
+                ],
+                'secondMiddleware' => [
+                    'target' => 'anotherClassName',
+                ],
+            ],
+        ];
+        $this->assertEquals($expected, $middlewares);
+    }
+
+    /**
+     * @test
+     */
+    public function configureMiddlewaresReturnsMergedMiddlewaresWithPseudoServiceProvider()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $package2 = $this->prophesize(Package::class);
+        $package2->getPackagePath()->willReturn(__DIR__ . '/../Http/Fixtures/Package2/');
+        $package2ServiceProvider = new PseudoServiceProvider($package2->reveal());
+
+        $middlewares = [];
+        $middlewares = Mocks\Package1ServiceProviderMock::configureMiddlewares(
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+        $middlewares = $package2ServiceProvider->getExtensions()['middlewares'](
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+
+        $expected = [
+            'testStack' => [
+                'firstMiddleware' => [
+                    'target' => 'aClassName',
+                ],
+                'secondMiddleware' => [
+                    'target' => 'anotherClassName',
+                ],
+            ],
+        ];
+        $this->assertEquals($expected, $middlewares);
+    }
+
+    /**
+     * @test
+     */
+    public function configureMiddlewaresReturnsMergedMiddlewaresWithOverrides()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $package2 = $this->prophesize(Package::class);
+        $package2->getPackagePath()->willReturn(__DIR__ . '/../Http/Fixtures/Package2Disables1/');
+        $package2ServiceProvider = new PseudoServiceProvider($package2->reveal());
+
+        $middlewares = [];
+        $middlewares = Mocks\Package1ServiceProviderMock::configureMiddlewares(
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+        $middlewares = $package2ServiceProvider->getExtensions()['middlewares'](
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+
+        $expected = [
+            'testStack' => [
+                'firstMiddleware' => [
+                    'target' => 'aClassName',
+                    'disabled' => true,
+                ],
+                'secondMiddleware' => [
+                    'target' => 'anotherClassName',
+                ],
+            ],
+        ];
+        $this->assertEquals($expected, $middlewares);
+    }
+
+    /**
+     * @test
+     */
+    public function configureMiddlewaresReturnsMergedMiddlewaresWithReplacements()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $package2 = $this->prophesize(Package::class);
+        $package2->getPackagePath()->willReturn(__DIR__ . '/../Http/Fixtures/Package2Replaces1/');
+        $package2ServiceProvider = new PseudoServiceProvider($package2->reveal());
+
+        $middlewares = [];
+        $middlewares = Mocks\Package1ServiceProviderMock::configureMiddlewares(
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+        $middlewares = $package2ServiceProvider->getExtensions()['middlewares'](
+            $containerProphecy->reveal(),
+            $middlewares
+        );
+
+        $expected = [
+            'testStack' => [
+                'firstMiddleware' => [
+                    'target' => 'replacedClassName',
+                ],
+                'secondMiddleware' => [
+                    'target' => 'anotherClassName',
+                ],
+            ],
+        ];
+        $this->assertEquals($expected, $middlewares);
+    }
+
+    /**
+     * @test
+     */
+    public function newReturnsClassInstance()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $className = get_class($this->getMockBuilder('foo')->getMock());
+        $newClosure = $this->getClosureForNew();
+        $instance = $newClosure($containerProphecy->reveal(), $className);
+        $this->assertTrue($instance instanceof $className);
+    }
+
+    /**
+     * @test
+     */
+    public function newInjectsLogger()
+    {
+        $containerProphecy = $this->prophesize();
+        $containerProphecy->willImplement(ContainerInterface::class);
+
+        $loggerProphecy = $this->prophesize();
+        $loggerProphecy->willImplement(LoggerInterface::class);
+
+        $logManagerProphecy = $this->prophesize(LogManager::class);
+        $logManagerProphecy->getLogger(GeneralUtilityMakeInstanceInjectLoggerFixture::class)->willReturn($loggerProphecy->reveal());
+
+        $containerProphecy->get(LogManager::class)->willReturn($logManagerProphecy->reveal());
+        $className = GeneralUtilityMakeInstanceInjectLoggerFixture::class;
+        $newClosure = $this->getClosureForNew();
+        $instance = $newClosure($containerProphecy->reveal(), $className);
+        $this->assertInstanceOf(LoggerInterface::class, $instance->getLogger());
+    }
+
+    /**
+     * @return \Closure
+     */
+    protected function getClosureForNew(): \Closure
+    {
+        return \Closure::bind(
+            function ($container, $className, $arguments = []) {
+                return AbstractServiceProvider::new($container, $className, $arguments);
+            },
+            null,
+            AbstractServiceProvider::class
+        );
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Package/Mocks/Package1ServiceProviderMock.php b/typo3/sysext/core/Tests/Unit/Package/Mocks/Package1ServiceProviderMock.php
new file mode 100644 (file)
index 0000000..6f6b7fc
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Package\Mocks;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+class Package1ServiceProviderMock extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../../Http/Fixtures/Package1/';
+    }
+
+    public function getFactories(): array
+    {
+        return [];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Package/Mocks/Package2ServiceProviderMock.php b/typo3/sysext/core/Tests/Unit/Package/Mocks/Package2ServiceProviderMock.php
new file mode 100644 (file)
index 0000000..0aa1e75
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Package\Mocks;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+class Package2ServiceProviderMock extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../../Http/Fixtures/Package2/';
+    }
+
+    public function getFactories(): array
+    {
+        return [];
+    }
+}
index d9795c1..b38736a 100644 (file)
@@ -31,7 +31,9 @@
                "psr/http-server-handler": "^1.0",
                "psr/http-server-middleware": "^1.0",
                "psr/log": "~1.0.0",
+               "symfony/config": "^4.1",
                "symfony/console": "^4.1",
+               "symfony/dependency-injection": "^4.1",
                "symfony/expression-language": "^4.1",
                "symfony/finder": "^4.1",
                "symfony/mailer": "^4.3",
@@ -81,6 +83,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Core\\ServiceProvider",
                                "protected": true,
                                "partOfFactoryDefault": true,
                                "partOfMinimalUsableSystem": true
index d7259c1..46a6b24 100644 (file)
@@ -16,15 +16,20 @@ namespace TYPO3\CMS\Extbase\Core;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Routing\Route;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
 use TYPO3\CMS\Extbase\Configuration\RequestHandlersConfigurationFactory;
+use TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver;
 use TYPO3\CMS\Extbase\Mvc\Web\Response as ExtbaseResponse;
 use TYPO3\CMS\Extbase\Persistence\ClassesConfigurationFactory;
+use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
+use TYPO3\CMS\Extbase\Service\CacheService;
 
 /**
  * Creates a request an dispatches it to the controller which was specified
@@ -48,21 +53,45 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
     public $cObj;
 
     /**
-     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManager
+     * @var ContainerInterface
      */
-    protected $configurationManager;
+    protected $container;
 
     /**
-     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
+     * @var ConfigurationManagerInterface
      */
-    protected $objectManager;
+    protected $configurationManager;
 
     /**
-     * @var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
+     * @var PersistenceManager
      */
     protected $persistenceManager;
 
     /**
+     * @var \TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver
+     */
+    protected $requestHandlerResolver;
+
+    /**
+     * @var \TYPO3\CMS\Extbase\Service\CacheService
+     */
+    protected $cacheService;
+
+    public function __construct(
+        ContainerInterface $container,
+        ConfigurationManagerInterface $configurationManager,
+        PersistenceManagerInterface $persistenceManager,
+        RequestHandlerResolver $requestHandlerResolver,
+        CacheService $cacheService
+    ) {
+        $this->container = $container;
+        $this->configurationManager = $configurationManager;
+        $this->persistenceManager = $persistenceManager;
+        $this->requestHandlerResolver = $requestHandlerResolver;
+        $this->cacheService = $cacheService;
+    }
+
+    /**
      * Explicitly initializes all necessary Extbase objects by invoking the various initialize* methods.
      *
      * Usually this method is only called from unit tests or other applications which need a more fine grained control over
@@ -82,21 +111,9 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
                 throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027);
             }
         }
-        $this->initializeObjectManager();
         $this->initializeConfiguration($configuration);
         $this->initializePersistenceClassesConfiguration();
         $this->initializeRequestHandlersConfiguration();
-        $this->initializePersistence();
-    }
-
-    /**
-     * Initializes the Object framework.
-     *
-     * @see initialize()
-     */
-    protected function initializeObjectManager(): void
-    {
-        $this->objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
     }
 
     /**
@@ -108,9 +125,8 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
      */
     public function initializeConfiguration(array $configuration): void
     {
-        $this->configurationManager = $this->objectManager->get(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::class);
         /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObject */
-        $contentObject = $this->cObj ?? \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
+        $contentObject = $this->cObj ?? $this->container->get(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
         $this->configurationManager->setContentObject($contentObject);
         $this->configurationManager->setConfiguration($configuration);
         // todo: Shouldn't the configuration manager object – which is a singleton – be stateless?
@@ -142,17 +158,6 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
     }
 
     /**
-     * Initializes the persistence framework
-     *
-     * @see initialize()
-     * @internal
-     */
-    public function initializePersistence(): void
-    {
-        $this->persistenceManager = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class);
-    }
-
-    /**
      * Runs the the Extbase Framework by resolving an appropriate Request Handler and passing control to it.
      * If the Framework is not initialized yet, it will be initialized.
      *
@@ -171,9 +176,7 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
      */
     protected function handleRequest(): string
     {
-        /** @var \TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver $requestHandlerResolver */
-        $requestHandlerResolver = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver::class);
-        $requestHandler = $requestHandlerResolver->resolveRequestHandler();
+        $requestHandler = $this->requestHandlerResolver->resolveRequestHandler();
 
         $response = $requestHandler->handleRequest();
         // If response is NULL after handling the request we need to stop
@@ -184,7 +187,7 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
         } else {
             $content = $response->shutdown();
             $this->resetSingletons();
-            $this->objectManager->get(\TYPO3\CMS\Extbase\Service\CacheService::class)->clearCachesOfRegisteredPageIds();
+            $this->cacheService->clearCachesOfRegisteredPageIds();
         }
 
         return $content;
@@ -210,16 +213,14 @@ class Bootstrap implements \TYPO3\CMS\Extbase\Core\BootstrapInterface
 
         $this->initialize($configuration);
 
-        /** @var \TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver $requestHandlerResolver */
-        $requestHandlerResolver = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\RequestHandlerResolver::class);
-        $requestHandler = $requestHandlerResolver->resolveRequestHandler();
+        $requestHandler = $this->requestHandlerResolver->resolveRequestHandler();
         /** @var ExtbaseResponse $extbaseResponse */
         $extbaseResponse = $requestHandler->handleRequest();
 
         // Convert to PSR-7 response and hand it back to TYPO3 Core
         $response = $this->convertExtbaseResponseToPsr7Response($extbaseResponse);
         $this->resetSingletons();
-        $this->objectManager->get(\TYPO3\CMS\Extbase\Service\CacheService::class)->clearCachesOfRegisteredPageIds();
+        $this->cacheService->clearCachesOfRegisteredPageIds();
         return $response;
     }
 
index fbe2b07..aa5dbf0 100644 (file)
@@ -1,6 +1,10 @@
 <?php
 namespace TYPO3\CMS\Extbase\Mvc;
 
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
+use TYPO3\CMS\Extbase\SignalSlot\Dispatcher as SignalSlotDispatcher;
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -22,36 +26,40 @@ namespace TYPO3\CMS\Extbase\Mvc;
 class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
 {
     /**
-     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface A reference to the object manager
+     * @var ObjectManagerInterface A reference to the object manager
      */
     protected $objectManager;
 
     /**
-     * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
+     * @var ContainerInterface
      */
-    protected $signalSlotDispatcher;
+    private $container;
 
     /**
-     * @var array
+     * @var SignalSlotDispatcher
      */
-    protected $settings = [];
+    protected $signalSlotDispatcher;
 
     /**
-     * @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
+     * @var array
      */
-    public function injectSignalSlotDispatcher(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher)
-    {
-        $this->signalSlotDispatcher = $signalSlotDispatcher;
-    }
+    protected $settings = [];
 
     /**
      * Constructs the global dispatcher
      *
-     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager A reference to the object manager
+     * @param ObjectManagerInterface $objectManager A reference to the object manager
+     * @param ContainerInterface $container
+     * @param SignalSlotDispatcher $signalSlotDispatcher
      */
-    public function __construct(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
-    {
+    public function __construct(
+        ObjectManagerInterface $objectManager,
+        ContainerInterface $container,
+        SignalSlotDispatcher $signalSlotDispatcher
+    ) {
         $this->objectManager = $objectManager;
+        $this->container = $container;
+        $this->signalSlotDispatcher = $signalSlotDispatcher;
     }
 
     /**
@@ -99,7 +107,11 @@ class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
     protected function resolveController(\TYPO3\CMS\Extbase\Mvc\RequestInterface $request)
     {
         $controllerObjectName = $request->getControllerObjectName();
-        $controller = $this->objectManager->get($controllerObjectName);
+        if ($this->container->has($controllerObjectName)) {
+            $controller = $this->container->get($controllerObjectName);
+        } else {
+            $controller = $this->objectManager->get($controllerObjectName);
+        }
         if (!$controller instanceof \TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface) {
             throw new \TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerException(
                 'Invalid controller "' . $request->getControllerObjectName() . '". The controller must implement the TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ControllerInterface.',
index 266cb6c..c90f43d 100644 (file)
@@ -17,9 +17,11 @@ namespace TYPO3\CMS\Extbase\Object\Container;
  */
 
 use Doctrine\Instantiator\InstantiatorInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
 use Psr\Log\LoggerInterface;
-use TYPO3\CMS\Core\Cache\CacheManager;
-use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Reflection\ClassSchema;
 use TYPO3\CMS\Extbase\Reflection\ReflectionService;
@@ -28,12 +30,19 @@ use TYPO3\CMS\Extbase\Reflection\ReflectionService;
  * Internal TYPO3 Dependency Injection container
  * @internal only to be used within Extbase, not part of TYPO3 Core API.
  */
-class Container implements \TYPO3\CMS\Core\SingletonInterface
+class Container implements SingletonInterface, LoggerAwareInterface
 {
+    use LoggerAwareTrait;
+
     const SCOPE_PROTOTYPE = 1;
     const SCOPE_SINGLETON = 2;
 
     /**
+     * @var ContainerInterface
+     */
+    private $psrContainer;
+
+    /**
      * registered alternative implementations of a class
      * e.g. used to know the class for an AbstractClass or a Dependency
      *
@@ -66,6 +75,14 @@ class Container implements \TYPO3\CMS\Core\SingletonInterface
     private $reflectionService;
 
     /**
+     * @param ContainerInterface $psrContainer
+     */
+    public function __construct(ContainerInterface $psrContainer)
+    {
+        $this->psrContainer = $psrContainer;
+    }
+
+    /**
      * Internal method to create the class instantiator, extracted to be mockable
      *
      * @return InstantiatorInterface
@@ -121,15 +138,15 @@ class Container implements \TYPO3\CMS\Core\SingletonInterface
     protected function getInstanceInternal(string $className, ...$givenConstructorArguments): object
     {
         $className = $this->getImplementationClassName($className);
-        if ($className === \TYPO3\CMS\Extbase\Object\Container\Container::class) {
-            return $this;
-        }
-        if ($className === \TYPO3\CMS\Core\Cache\CacheManager::class) {
-            return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
-        }
-        if ($className === \TYPO3\CMS\Core\Package\PackageManager::class) {
-            return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
+
+        if ($givenConstructorArguments === [] && $this->psrContainer->has($className)) {
+            $instance = $this->psrContainer->get($className);
+            if (!is_object($instance)) {
+                throw new \TYPO3\CMS\Extbase\Object\Exception('PSR-11 container returned non object for class name "' . $className . '".', 1562240407);
+            }
+            return $instance;
         }
+
         $className = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($className);
         if (isset($this->singletonInstances[$className])) {
             if (!empty($givenConstructorArguments)) {
@@ -236,6 +253,7 @@ class Container implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param string $className
      * @param string $alternativeClassName
+     * @todo deprecate in favor of core DI configuration (aliases/overrides)
      */
     public function registerImplementation(string $className, string $alternativeClassName): void
     {
@@ -336,7 +354,7 @@ class Container implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function getLogger(): LoggerInterface
     {
-        return GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
+        return $this->logger;
     }
 
     /**
@@ -349,6 +367,6 @@ class Container implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function getReflectionService(): ReflectionService
     {
-        return $this->reflectionService ?? ($this->reflectionService = GeneralUtility::makeInstance(ReflectionService::class, GeneralUtility::makeInstance(CacheManager::class)));
+        return $this->reflectionService ?? ($this->reflectionService = $this->psrContainer->get(ReflectionService::class));
     }
 }
index ccf191d..e8e5059 100644 (file)
@@ -16,7 +16,9 @@ namespace TYPO3\CMS\Extbase\Object;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Extbase\Object\Container\Container;
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\Container\Container as ExtbaseContainer;
 
 /**
  * Implementation of the default Extbase Object Manager
@@ -24,16 +26,25 @@ use TYPO3\CMS\Extbase\Object\Container\Container;
 class ObjectManager implements ObjectManagerInterface
 {
     /**
-     * @var \TYPO3\CMS\Extbase\Object\Container\Container
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var ExtbaseContainer
      */
     protected $objectContainer;
 
     /**
      * Constructs a new Object Manager
+     *
+     * @param ContainerInterface $container
+     * @param ExtbaseContainer $objectContainer
      */
-    public function __construct()
+    public function __construct(ContainerInterface $container, ExtbaseContainer $objectContainer)
     {
-        $this->objectContainer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\Container::class);
+        $this->container = $container;
+        $this->objectContainer = $objectContainer;
     }
 
     /**
@@ -70,7 +81,10 @@ class ObjectManager implements ObjectManagerInterface
      */
     public function __wakeup()
     {
-        $this->__construct();
+        $this->__construct(
+            GeneralUtility::getContainer(),
+            GeneralUtility::getContainer()->get(ExtbaseContainer::class)
+        );
     }
 
     /**
@@ -95,18 +109,28 @@ class ObjectManager implements ObjectManagerInterface
     public function get(string $objectName, ...$constructorArguments): object
     {
         if ($objectName === 'DateTime') {
-            $instance = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($objectName, ...$constructorArguments);
-        } else {
-            $instance = $this->objectContainer->getInstance($objectName, $constructorArguments);
+            return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($objectName, ...$constructorArguments);
         }
-        return $instance;
+
+        if ($this->container->has($objectName)) {
+            if ($constructorArguments !== []) {
+                $instance = $this->container->get($objectName);
+                if (!is_object($instance)) {
+                    throw new \TYPO3\CMS\Extbase\Object\Exception('PSR-11 container returned non object for class name "' . $className . '".', 1562357346);
+                }
+                return $instance;
+            }
+            trigger_error($objectName . ' is available in the PSR-11 container. That means you should not try to instanciate it using constructor arguments. Falling back to legacy extbase based injection.', E_USER_DEPRECATED);
+        }
+
+        return $this->objectContainer->getInstance($objectName, $constructorArguments);
     }
 
     /**
      * Returns the scope of the specified object.
      *
      * @param string $objectName The object name
-     * @return int One of the Container::SCOPE_ constants
+     * @return int One of the ExtbaseContainer::SCOPE_ constants
      * @throws \TYPO3\CMS\Extbase\Object\Container\Exception\UnknownObjectException
      */
     public function getScope(string $objectName): int
@@ -114,7 +138,7 @@ class ObjectManager implements ObjectManagerInterface
         if (!$this->isRegistered($objectName)) {
             throw new \TYPO3\CMS\Extbase\Object\Container\Exception\UnknownObjectException('Object "' . $objectName . '" is not registered.', 1265367590);
         }
-        return $this->objectContainer->isSingleton($objectName) ? Container::SCOPE_SINGLETON : Container::SCOPE_PROTOTYPE;
+        return $this->objectContainer->isSingleton($objectName) ? ExtbaseContainer::SCOPE_SINGLETON : ExtbaseContainer::SCOPE_PROTOTYPE;
     }
 
     /**
diff --git a/typo3/sysext/extbase/Classes/ServiceProvider.php b/typo3/sysext/extbase/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..0b73cf7
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Extbase;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Object\Container\Container::class => [ static::class, 'getObjectContainer' ],
+            Object\ObjectManager::class => [ static::class, 'getObjectManager' ],
+            SignalSlot\Dispatcher::class => [ static::class, 'getSignalSlotDispatcher' ],
+            Configuration\ConfigurationManager::class => [ static::class, 'getConfigurationManager' ],
+            Reflection\ReflectionService::class => [ static::class, 'getReflectionService' ],
+            Service\EnvironmentService::class => [ static::class, 'getEnvironmentService' ],
+            Service\ExtensionService::class => [ static::class, 'getExtensionService' ],
+            Security\Cryptography\HashService::class => [ static::class, 'getHashService' ],
+        ];
+    }
+
+    public static function getObjectContainer(ContainerInterface $container): Object\Container\Container
+    {
+        return self::new($container, Object\Container\Container::class, [$container]);
+    }
+
+    public static function getObjectManager(ContainerInterface $container): Object\ObjectManager
+    {
+        return self::new($container, Object\ObjectManager::class, [$container, $container->get(Object\Container\Container::class)]);
+    }
+
+    public static function getSignalSlotDispatcher(ContainerInterface $container): SignalSlot\Dispatcher
+    {
+        $logger = $container->get(LogManager::class)->getLogger(SignalSlot\Dispatcher::class);
+        return self::new($container, SignalSlot\Dispatcher::class, [$container->get(Object\ObjectManager::class), $logger]);
+    }
+
+    public static function getConfigurationManager(ContainerInterface $container): Configuration\ConfigurationManager
+    {
+        return self::new($container, Configuration\ConfigurationManager::class, [
+            $container->get(Object\ObjectManager::class),
+            $container->get(Service\EnvironmentService::class),
+        ]);
+    }
+
+    public static function getReflectionService(ContainerInterface $container): Reflection\ReflectionService
+    {
+        return self::new($container, Reflection\ReflectionService::class, [$container->get(CacheManager::class)]);
+    }
+
+    public static function getEnvironmentService(ContainerInterface $container): Service\EnvironmentService
+    {
+        return self::new($container, Service\EnvironmentService::class);
+    }
+
+    public static function getExtensionService(ContainerInterface $container): Service\ExtensionService
+    {
+        $extensionService = self::new($container, Service\ExtensionService::class);
+        $extensionService->injectObjectManager($container->get(Object\ObjectManager::class));
+        $extensionService->injectConfigurationManager($container->get(Configuration\ConfigurationManager::class));
+        return $extensionService;
+    }
+
+    public static function getHashService(ContainerInterface $container): Security\Cryptography\HashService
+    {
+        return self::new($container, Security\Cryptography\HashService::class);
+    }
+}
index 2212f5b..d72d401 100644 (file)
@@ -17,9 +17,7 @@ namespace TYPO3\CMS\Extbase\SignalSlot;
  */
 
 use Psr\Log\LoggerInterface;
-use TYPO3\CMS\Core\Log\LogManager;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
 
 /**
  * A dispatcher which dispatches signals by calling its registered slot methods
@@ -29,12 +27,7 @@ use TYPO3\CMS\Extbase\Object\ObjectManager;
 class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
 {
     /**
-     * @var bool
-     */
-    protected $isInitialized = false;
-
-    /**
-     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
+     * @var ObjectManagerInterface
      */
     protected $objectManager;
 
@@ -53,21 +46,13 @@ class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
     protected $logger;
 
     /**
-     * Initializes this object.
-     *
-     * This method needs to be used as an alternative to inject aspects.
-     * Since this dispatcher is used very early when the ObjectManager
-     * is not fully initialized (especially concerning caching framework),
-     * this is the only way.
+     * @param ObjectManagerInterface $objectManager
+     * @param LoggerInterface $logger
      */
-    public function initializeObject(): void
+    public function __construct(ObjectManagerInterface $objectManager, LoggerInterface $logger)
     {
-        if (!$this->isInitialized) {
-            $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
-            $logManager = GeneralUtility::makeInstance(LogManager::class);
-            $this->logger = $logManager->getLogger(self::class);
-            $this->isInitialized = true;
-        }
+        $this->objectManager = $objectManager;
+        $this->logger = $logger;
     }
 
     /**
@@ -120,7 +105,6 @@ class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
      */
     public function dispatch(string $signalClassName, string $signalName, array $signalArguments = [])
     {
-        $this->initializeObject();
         $this->logger->debug(
             'Triggered signal ' . $signalClassName . ' ' . $signalName,
             [
@@ -136,9 +120,6 @@ class Dispatcher implements \TYPO3\CMS\Core\SingletonInterface
             if (isset($slotInformation['object'])) {
                 $object = $slotInformation['object'];
             } else {
-                if (!isset($this->objectManager)) {
-                    throw new Exception\InvalidSlotException(sprintf('Cannot dispatch %s::%s to class %s. The object manager is not yet available in the Signal Slot Dispatcher and therefore it cannot dispatch classes.', $signalClassName, $signalName, $slotInformation['class'] ?? ''), 1298113624);
-                }
                 if (!$this->objectManager->isRegistered($slotInformation['class'])) {
                     throw new Exception\InvalidSlotException('The given class "' . $slotInformation['class'] . '" is not a registered object.', 1245673367);
                 }
diff --git a/typo3/sysext/extbase/Configuration/Services.php b/typo3/sysext/extbase/Configuration/Services.php
new file mode 100644 (file)
index 0000000..e35268c
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Extbase;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+
+return function (ContainerConfigurator $containerConfigurator, ContainerBuilder $container) {
+    $container->registerForAutoconfiguration(Mvc\RequestHandlerInterface::class)->addTag('extbase.request_handler');
+    $container->registerForAutoconfiguration(Mvc\Controller\ControllerInterface::class)->addTag('extbase.controller');
+    $container->registerForAutoconfiguration(Mvc\Controller\AbstractController::class)->addTag('extbase.prototype_controller');
+    $container->registerForAutoconfiguration(Mvc\View\ViewInterface::class)->addTag('extbase.view');
+
+    $container->addCompilerPass(new class implements CompilerPassInterface {
+        public function process(ContainerBuilder $container): void
+        {
+            foreach ($container->findTaggedServiceIds('extbase.request_handler') as $id => $tags) {
+                $container->findDefinition($id)->setPublic(true);
+            }
+            foreach ($container->findTaggedServiceIds('extbase.controller') as $id => $tags) {
+                $container->findDefinition($id)->setPublic(true);
+            }
+            foreach ($container->findTaggedServiceIds('extbase.prototype_controller') as $id => $tags) {
+                $container->findDefinition($id)->setShared(false);
+            }
+            foreach ($container->findTaggedServiceIds('extbase.view') as $id => $tags) {
+                $container->findDefinition($id)->setShared(false)->setPublic(true);
+            }
+
+            // Push alias definition defined in symfony into the extbase container
+            // 'aliasDefinitions' is a private property of the Symfony ContainerBuilder class
+            // but as 'alias' statements an not be tagged, that is the only way to retrieve
+            // these aliases to map them to the extbase container
+            $reflection = new \ReflectionClass(get_class($container));
+            $aliasDefinitions = $reflection->getProperty('aliasDefinitions');
+            $aliasDefinitions->setAccessible(true);
+
+            $extbaseContainer = $container->findDefinition(Object\Container\Container::class);
+            // Add registerImplementation() call for aliases
+            foreach ($aliasDefinitions->getValue($container) as $from => $alias) {
+                if (!class_exists($from) && !interface_exists($from)) {
+                    continue;
+                }
+                $to = (string)$alias;
+                // Ignore aliases that are used to inject early instances into the container (instantiated during TYPO3 Bootstrap)
+                // and aliases that refer to service names instead of class names
+                if (strpos($to, '_early.') === 0 || !class_exists($to)) {
+                    continue;
+                }
+
+                $extbaseContainer->addMethodCall('registerImplementation', [$from, $to]);
+            }
+        }
+    });
+};
diff --git a/typo3/sysext/extbase/Configuration/Services.yaml b/typo3/sysext/extbase/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..366498c
--- /dev/null
@@ -0,0 +1,36 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Extbase\:
+    resource: '../Classes/*'
+
+  TYPO3\CMS\Extbase\Core\BootstrapInterface:
+    alias: TYPO3\CMS\Extbase\Core\Bootstrap
+
+  TYPO3\CMS\Extbase\Core\Bootstrap:
+    public: true
+    shared: false
+
+  TYPO3\CMS\Extbase\Object\ObjectManagerInterface:
+    alias: TYPO3\CMS\Extbase\Object\ObjectManager
+
+  # formerly in EXT:extbase/ext_localconf.php
+  TYPO3\CMS\Extbase\Persistence\QueryInterface:
+    alias: TYPO3\CMS\Extbase\Persistence\Generic\Query
+  TYPO3\CMS\Extbase\Persistence\QueryResultInterface:
+    alias: TYPO3\CMS\Extbase\Persistence\Generic\QueryResult
+  TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface:
+    alias: TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
+  TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface:
+    alias: TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend
+  TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface:
+    alias: TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings
+
+  # Set incompatible classes to null, these require (runtime) parametrized
+  # prototype instantiation
+  TYPO3\CMS\Extbase\Persistence\Generic\Query: ~
+  TYPO3\CMS\Extbase\Persistence\Generic\QueryResult: ~
+  TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings: ~
index cc4c0c7..b8c7224 100644 (file)
@@ -51,11 +51,23 @@ class ContainerTest extends UnitTestCase
             ->setMethods(['notice'])
             ->disableOriginalConstructor()
             ->getMock();
+        $reflectionService = new \TYPO3\CMS\Extbase\Reflection\ReflectionService;
+
+        $notFoundException = new class extends \Exception implements \Psr\Container\NotFoundExceptionInterface {
+        };
+
+        $psrContainer = $this->getMockBuilder(\Psr\Container\ContainerInterface::class)
+            ->setMethods(['has', 'get'])
+            ->getMock();
+        $psrContainer->expects($this->any())->method('has')->will($this->returnValue(false));
+        $psrContainer->expects($this->any())->method('get')->will($this->throwException($notFoundException));
 
         $this->subject = $this->getMockBuilder(Container::class)
-            ->setMethods(['getLogger'])
+            ->setConstructorArgs([$psrContainer])
+            ->setMethods(['getLogger', 'getReflectionService'])
             ->getMock();
         $this->subject->expects($this->any())->method('getLogger')->will($this->returnValue($this->logger));
+        $this->subject->expects($this->any())->method('getReflectionService')->will($this->returnValue($reflectionService));
     }
 
     /**
@@ -957,8 +969,7 @@ class ContainerTest extends UnitTestCase
      */
     public function getInstanceInjectsPublicProperties()
     {
-        $container = new Container();
-        $object = $container->getInstance(PublicPropertyInjectClass::class);
+        $object = $this->subject->getInstance(PublicPropertyInjectClass::class);
         self::assertInstanceOf(ArgumentTestClassForPublicPropertyInjection::class, $object->foo);
     }
 
@@ -967,8 +978,7 @@ class ContainerTest extends UnitTestCase
      */
     public function getInstanceInjectsProtectedProperties()
     {
-        $container = new Container();
-        $object = $container->getInstance(ProtectedPropertyInjectClass::class);
+        $object = $this->subject->getInstance(ProtectedPropertyInjectClass::class);
         self::assertInstanceOf(ArgumentTestClassForPublicPropertyInjection::class, $object->getFoo());
     }
 }
index aafd577..223a4e8 100644 (file)
@@ -249,7 +249,14 @@ class DataMapperTest extends UnitTestCase
         $classSchema1 = new ClassSchema(Fixture\DummyParentEntity::class);
         $identifier = 1;
 
-        $session = new \TYPO3\CMS\Extbase\Persistence\Generic\Session(new Container());
+        $psrContainer = $this->getMockBuilder(\Psr\Container\ContainerInterface::class)
+            ->setMethods(['has', 'get'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $psrContainer->expects($this->any())->method('has')->will($this->returnValue(false));
+        $container = new Container($psrContainer);
+
+        $session = new \TYPO3\CMS\Extbase\Persistence\Generic\Session(new Container($psrContainer));
         $session->registerObject($child, $identifier);
 
         $mockReflectionService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class)
index ffea230..7597351 100644 (file)
@@ -339,7 +339,12 @@ class PersistenceManagerTest extends UnitTestCase
         $classNameWithNamespace = __NAMESPACE__ . '\\Domain\\Model\\' . $className;
         $repositorClassNameWithNamespace = __NAMESPACE__ . '\\Domain\\Repository\\' . $className . 'Repository';
 
-        $session = new Session(new Container());
+        $psrContainer = $this->getMockBuilder(\Psr\Container\ContainerInterface::class)
+            ->setMethods(['has', 'get'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $psrContainer->expects($this->any())->method('has')->will($this->returnValue(false));
+        $session = new Session(new Container($psrContainer));
         $changedEntities = new ObjectStorage();
         $entity1 = new $classNameWithNamespace();
         /** @var RepositoryInterface|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $repository */
index 1fa5881..7bd6cdf 100644 (file)
@@ -22,13 +22,20 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class SessionTest extends UnitTestCase
 {
+    protected function createContainer(): Container
+    {
+        $psrContainer = $this->getMockBuilder(\Psr\Container\ContainerInterface::class)->setMethods(['has', 'get'])->getMock();
+        $psrContainer->expects($this->any())->method('has')->will($this->returnValue(false));
+        return new Container($psrContainer);
+    }
+
     /**
      * @test
      */
     public function objectRegisteredWithRegisterReconstitutedEntityCanBeRetrievedWithGetReconstitutedEntities()
     {
         $someObject = new \ArrayObject([]);
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerReconstitutedEntity($someObject);
 
         $ReconstitutedEntities = $session->getReconstitutedEntities();
@@ -41,7 +48,7 @@ class SessionTest extends UnitTestCase
     public function unregisterReconstitutedEntityRemovesObjectFromSession()
     {
         $someObject = new \ArrayObject([]);
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject($someObject, 'fakeUuid');
         $session->registerReconstitutedEntity($someObject);
         $session->unregisterReconstitutedEntity($someObject);
@@ -57,7 +64,7 @@ class SessionTest extends UnitTestCase
     {
         $object1 = new \stdClass();
         $object2 = new \stdClass();
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject($object1, 12345);
 
         $this->assertTrue($session->hasObject($object1), 'Session claims it does not have registered object.');
@@ -69,7 +76,7 @@ class SessionTest extends UnitTestCase
      */
     public function hasIdentifierReturnsTrueForRegisteredObject()
     {
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject(new \stdClass(), 12345);
 
         $this->assertTrue($session->hasIdentifier('12345', 'stdClass'), 'Session claims it does not have registered object.');
@@ -82,7 +89,7 @@ class SessionTest extends UnitTestCase
     public function getIdentifierByObjectReturnsRegisteredUUIDForObject()
     {
         $object = new \stdClass();
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject($object, 12345);
 
         $this->assertEquals($session->getIdentifierByObject($object), 12345, 'Did not get UUID registered for object.');
@@ -94,7 +101,7 @@ class SessionTest extends UnitTestCase
     public function getObjectByIdentifierReturnsRegisteredObjectForUUID()
     {
         $object = new \stdClass();
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject($object, 12345);
 
         $this->assertSame($session->getObjectByIdentifier('12345', 'stdClass'), $object, 'Did not get object registered for UUID.');
@@ -107,7 +114,7 @@ class SessionTest extends UnitTestCase
     {
         $object1 = new \stdClass();
         $object2 = new \stdClass();
-        $session = new Session(new Container());
+        $session = new Session($this->createContainer());
         $session->registerObject($object1, 12345);
         $session->registerObject($object2, 67890);
 
@@ -129,7 +136,7 @@ class SessionTest extends UnitTestCase
      */
     public function newSessionIsEmpty()
     {
-        $persistenceSession = new Session(new Container());
+        $persistenceSession = new Session($this->createContainer());
         $reconstitutedObjects = $persistenceSession->getReconstitutedEntities();
         $this->assertEquals(0, count($reconstitutedObjects), 'The reconstituted objects storage was not empty.');
     }
@@ -139,7 +146,7 @@ class SessionTest extends UnitTestCase
      */
     public function objectCanBeRegisteredAsReconstituted()
     {
-        $persistenceSession = new Session(new Container());
+        $persistenceSession = new Session($this->createContainer());
         $entity = $this->createMock(AbstractEntity::class);
         $persistenceSession->registerReconstitutedEntity($entity);
         $reconstitutedObjects = $persistenceSession->getReconstitutedEntities();
@@ -151,7 +158,7 @@ class SessionTest extends UnitTestCase
      */
     public function objectCanBeUnregisteredAsReconstituted()
     {
-        $persistenceSession = new Session(new Container());
+        $persistenceSession = new Session($this->createContainer());
         $entity = $this->createMock(AbstractEntity::class);
         $persistenceSession->registerReconstitutedEntity($entity);
         $persistenceSession->unregisterReconstitutedEntity($entity);
index 1d1f622..0eac4ca 100644 (file)
@@ -15,7 +15,8 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\SignalSlot;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Log\Logger;
+use Prophecy\Prophecy\ObjectProphecy;
+use Psr\Log\LoggerInterface;
 use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
 use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException;
@@ -36,6 +37,11 @@ class DispatcherTest extends UnitTestCase
     protected $resetSingletonInstances = true;
 
     /**
+     * @var ObjectManagerInterface|ObjectProphecy
+     */
+    protected $objectManagerProphecy;
+
+    /**
      * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
      */
     protected $signalSlotDispatcher;
@@ -43,8 +49,12 @@ class DispatcherTest extends UnitTestCase
     protected function setUp(): void
     {
         parent::setUp();
-        $accessibleClassName = $this->getAccessibleMock(Dispatcher::class, ['dummy']);
-        $this->signalSlotDispatcher = new $accessibleClassName();
+        $this->objectManagerProphecy = $this->prophesize(ObjectManagerInterface::class);
+
+        $this->signalSlotDispatcher = new Dispatcher(
+            $this->objectManagerProphecy->reveal(),
+            $this->prophesize(LoggerInterface::class)->reveal()
+        );
     }
 
     /**
@@ -121,13 +131,8 @@ class DispatcherTest extends UnitTestCase
     {
         $slotClassName = OnlyClassNameSpecifiedFixture::class;
         $mockSlot = new OnlyClassNameSpecifiedFixture();
-        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
-        $mockObjectManager->expects($this->once())->method('isRegistered')->with($slotClassName)->will($this->returnValue(true));
-        $mockObjectManager->expects($this->once())->method('get')->with($slotClassName)->will($this->returnValue($mockSlot));
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('objectManager', $mockObjectManager);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
+        $this->objectManagerProphecy->isRegistered($slotClassName)->willReturn(true);
+        $this->objectManagerProphecy->get($slotClassName)->willReturn($mockSlot);
         $this->signalSlotDispatcher->connect('Foo', 'emitBar', $slotClassName, 'slot', false);
         $this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
         $this->assertSame($mockSlot->arguments, ['bar', 'quux']);
@@ -138,10 +143,6 @@ class DispatcherTest extends UnitTestCase
      */
     public function dispatchHandsOverArgumentsReturnedByAFormerSlot()
     {
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
-
         $firstMockSlot = $this->createMock(SlotFixture::class);
         $firstMockSlot->expects($this->once())
             ->method('slot')
@@ -167,10 +168,6 @@ class DispatcherTest extends UnitTestCase
      */
     public function dispatchHandsOverArgumentsReturnedByAFormerSlotWithoutInterferingWithSignalSlotInformation()
     {
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
-
         $firstMockSlot = $this->createMock(SlotFixture::class);
         $firstMockSlot->expects($this->once())
             ->method('slot')
@@ -196,10 +193,6 @@ class DispatcherTest extends UnitTestCase
      */
     public function dispatchHandsOverFormerArgumentsIfPreviousSlotDoesNotReturnAnything()
     {
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
-
         $firstMockSlot = $this->createMock(SlotFixture::class);
         $firstMockSlot->expects($this->once())
             ->method('slot')
@@ -232,9 +225,6 @@ class DispatcherTest extends UnitTestCase
     {
         $this->expectException(InvalidSlotReturnException::class);
         $this->expectExceptionCode(1376683067);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
 
         $mockSlot = $this->createMock(SlotFixture::class);
         $mockSlot->expects($this->once())
@@ -256,9 +246,6 @@ class DispatcherTest extends UnitTestCase
     {
         $this->expectException(InvalidSlotReturnException::class);
         $this->expectExceptionCode(1376683066);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
 
         $mockSlot = $this->createMock(SlotFixture::class);
         $mockSlot->expects($this->once())
@@ -280,12 +267,7 @@ class DispatcherTest extends UnitTestCase
     {
         $this->expectException(InvalidSlotException::class);
         $this->expectExceptionCode(1245673367);
-        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
-        $mockObjectManager->expects($this->once())->method('isRegistered')->with('NonExistingClassName')->will($this->returnValue(false));
-        $this->signalSlotDispatcher->_set('objectManager', $mockObjectManager);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
+        $this->objectManagerProphecy->isRegistered('NonExistingClassName')->willReturn(false);
         $this->signalSlotDispatcher->connect('Foo', 'emitBar', 'NonExistingClassName', 'slot', true);
         $this->signalSlotDispatcher->dispatch('Foo', 'emitBar', []);
     }
@@ -299,13 +281,8 @@ class DispatcherTest extends UnitTestCase
         $this->expectExceptionCode(1245673368);
         $slotClassName = SlotMethodDoesNotExistFixture::class;
         $mockSlot = new SlotMethodDoesNotExistFixture();
-        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
-        $mockObjectManager->expects($this->once())->method('isRegistered')->with($slotClassName)->will($this->returnValue(true));
-        $mockObjectManager->expects($this->once())->method('get')->with($slotClassName)->will($this->returnValue($mockSlot));
-        $this->signalSlotDispatcher->_set('objectManager', $mockObjectManager);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
+        $this->objectManagerProphecy->isRegistered($slotClassName)->willReturn(true);
+        $this->objectManagerProphecy->get($slotClassName)->willReturn($mockSlot);
         $this->signalSlotDispatcher->connect('Foo', 'emitBar', $slotClassName, 'unknownMethodName', true);
         $this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
         $this->assertSame($mockSlot->arguments, ['bar', 'quux']);
@@ -320,12 +297,7 @@ class DispatcherTest extends UnitTestCase
         $mockSlot = function () use (&$arguments) {
             $arguments = func_get_args();
         };
-        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
         $this->signalSlotDispatcher->connect('SignalClassName', 'methodName', $mockSlot, '', true);
-        $this->signalSlotDispatcher->_set('objectManager', $mockObjectManager);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
         $this->signalSlotDispatcher->dispatch('SignalClassName', 'methodName', ['bar', 'quux']);
         $this->assertSame(['bar', 'quux', 'SignalClassName::methodName'], $arguments);
     }
@@ -368,20 +340,4 @@ class DispatcherTest extends UnitTestCase
         ];
         $this->assertSame($arguments, $this->signalSlotDispatcher->dispatch('ClassA', 'emitSomeSignal', $arguments));
     }
-
-    /**
-     * @test
-     */
-    public function dispatchThrowsInvalidSlotExceptionIfObjectManagerOfSignalSlotDispatcherIsNotSet()
-    {
-        $this->expectException(InvalidSlotException::class);
-        $this->expectExceptionCode(1298113624);
-        $this->signalSlotDispatcher->_set('isInitialized', true);
-        $mockLogger = $this->createMock(Logger::class);
-        $this->signalSlotDispatcher->_set('logger', $mockLogger);
-        $this->signalSlotDispatcher->_set('objectManager', null);
-        $this->signalSlotDispatcher->_set('slots', ['ClassA' => ['emitSomeSignal' => [[]]]]);
-
-        $this->assertSame(null, $this->signalSlotDispatcher->dispatch('ClassA', 'emitSomeSignal'));
-    }
 }
index c8a7842..53c39f5 100644 (file)
@@ -14,6 +14,7 @@
        },
        "require": {
                "phpdocumentor/reflection-docblock": "^4.3",
+               "symfony/dependency-injection": "^4.1",
                "symfony/property-access": "^4.2",
                "symfony/property-info": "^4.2",
                "typo3/cms-core": "10.0.*@dev",
@@ -34,6 +35,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Extbase\\ServiceProvider",
                                "protected": true,
                                "partOfFactoryDefault": true,
                                "partOfMinimalUsableSystem": true
index 73d746a..eddb068 100644 (file)
@@ -1,18 +1,6 @@
 <?php
 defined('TYPO3_MODE') or die();
 
-// We set the default implementation for Storage Backend & Query Settings in Backend and Frontend.
-// The code below is NO PUBLIC API!
-/** @var \TYPO3\CMS\Extbase\Object\Container\Container $extbaseObjectContainer */
-$extbaseObjectContainer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\Container::class);
-// Singleton
-$extbaseObjectContainer->registerImplementation(\TYPO3\CMS\Extbase\Persistence\QueryInterface::class, \TYPO3\CMS\Extbase\Persistence\Generic\Query::class);
-$extbaseObjectContainer->registerImplementation(\TYPO3\CMS\Extbase\Persistence\QueryResultInterface::class, \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult::class);
-$extbaseObjectContainer->registerImplementation(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface::class, \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class);
-$extbaseObjectContainer->registerImplementation(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface::class, \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend::class);
-$extbaseObjectContainer->registerImplementation(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface::class, \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class);
-unset($extbaseObjectContainer);
-
 // Register type converters
 \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter(\TYPO3\CMS\Extbase\Property\TypeConverter\ArrayConverter::class);
 \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter(\TYPO3\CMS\Extbase\Property\TypeConverter\BooleanConverter::class);
diff --git a/typo3/sysext/extensionmanager/Configuration/Services.yaml b/typo3/sysext/extensionmanager/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..a913c94
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Extensionmanager\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/felogin/Configuration/Services.yaml b/typo3/sysext/felogin/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..e368385
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Felogin\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/filelist/Configuration/Services.yaml b/typo3/sysext/filelist/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..21922cf
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Filelist\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/fluid/Configuration/Services.php b/typo3/sysext/fluid/Configuration/Services.php
new file mode 100644 (file)
index 0000000..6c2aa0b
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Fluid;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
+
+return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
+    $containerBuilder->registerForAutoconfiguration(ViewHelperInterface::class)->addTag('fluid.viewhelper');
+
+    $containerBuilder->addCompilerPass(new class implements CompilerPassInterface {
+        public function process(ContainerBuilder $container)
+        {
+            foreach ($container->findTaggedServiceIds('fluid.viewhelper') as $id => $tags) {
+                $container->findDefinition($id)->setPublic(true)->setShared(false);
+            }
+        }
+    });
+};
diff --git a/typo3/sysext/fluid/Configuration/Services.yaml b/typo3/sysext/fluid/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..f8b1272
--- /dev/null
@@ -0,0 +1,14 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Fluid\:
+    resource: '../Classes/*'
+
+  # Templateview has $context = null, symfony auto-injects in that case,
+  # extbase did not, force passing `null`
+  TYPO3\CMS\Fluid\View\TemplateView:
+    arguments:
+      $context: null
index d8cce88..ab482bb 100644 (file)
@@ -13,6 +13,7 @@
                "sort-packages": true
        },
        "require": {
+               "symfony/dependency-injection": "^4.1",
                "typo3/cms-core": "10.0.*@dev",
                "typo3/cms-extbase": "10.0.*@dev",
                "typo3fluid/fluid": "^2.6.1"
diff --git a/typo3/sysext/fluid_styled_content/Configuration/Services.yaml b/typo3/sysext/fluid_styled_content/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..ae29e1a
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\FluidStyledContent\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/form/Configuration/Services.yaml b/typo3/sysext/form/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..73e18e6
--- /dev/null
@@ -0,0 +1,9 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Form\:
+    resource: '../Classes/*'
+    exclude: '../Classes/{Domain/Model}'
index c0439a1..81c2460 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Frontend\ContentObject;
 
 use Doctrine\DBAL\DBALException;
 use Doctrine\DBAL\Driver\Statement;
+use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use Symfony\Component\Mime\NamedAddress;
@@ -87,6 +88,11 @@ class ContentObjectRenderer implements LoggerAwareInterface
     use LoggerAwareTrait;
 
     /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
      * @var array
      */
     public $align = [
@@ -451,11 +457,13 @@ class ContentObjectRenderer implements LoggerAwareInterface
 
     /**
      * @param TypoScriptFrontendController $typoScriptFrontendController
+     * @param ContainerInterface $container
      */
-    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null)
+    public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null, ContainerInterface $container = null)
     {
         $this->typoScriptFrontendController = $typoScriptFrontendController;
         $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'];
+        $this->container = $container;
     }
 
     /**
@@ -468,7 +476,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
     public function __sleep()
     {
         $vars = get_object_vars($this);
-        unset($vars['typoScriptFrontendController'], $vars['logger']);
+        unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container']);
         if ($this->currentFile instanceof FileReference) {
             $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
         } elseif ($this->currentFile instanceof File) {
@@ -502,6 +510,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
             }
         }
         $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
+        $this->container = GeneralUtility::getContainer();
     }
 
     /**
@@ -5527,7 +5536,11 @@ class ContentObjectRenderer implements LoggerAwareInterface
         if (count($parts) === 2) {
             // Check whether PHP class is available
             if (class_exists($parts[0])) {
-                $classObj = GeneralUtility::makeInstance($parts[0]);
+                if ($this->container && $this->container->has($parts[0])) {
+                    $classObj = $this->container->get($parts[0]);
+                } else {
+                    $classObj = GeneralUtility::makeInstance($parts[0]);
+                }
                 if (is_object($classObj) && method_exists($classObj, $parts[1])) {
                     $classObj->cObj = $this;
                     $content = call_user_func_array([
index 88ca391..e949877 100644 (file)
@@ -3716,4 +3716,30 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
         return null;
     }
+
+    /**
+     * Return the global instance of this class.
+     *
+     * Intended to be used as prototype factory for this class, see Services.yaml.
+     * This is required as long as TypoScriptFrontendController needs request
+     * dependent constructor parameters. Once that has been refactored this
+     * factory will be removed.
+     *
+     * @return TypoScriptFrontendController
+     * @internal
+     */
+    public static function getGlobalInstance(): ?self
+    {
+        if ($GLOBALS['TSFE'] instanceof self) {
+            return $GLOBALS['TSFE'];
+        }
+
+        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
+            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
+            // That's not an error then
+            return null;
+        }
+
+        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
+    }
 }
index 363e792..0980b47 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Frontend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
@@ -34,25 +35,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class Application extends AbstractApplication
 {
     /**
-     * @var string
-     */
-    protected $requestHandler = RequestHandler::class;
-
-    /**
-     * @var string
-     */
-    protected $middlewareStack = 'frontend';
-
-    /**
      * @var ConfigurationManager
      */
     protected $configurationManager;
 
     /**
+     * @param RequestHandlerInterface $requestHandler
      * @param ConfigurationManager $configurationManager
      */
-    public function __construct(ConfigurationManager $configurationManager)
+    public function __construct(RequestHandlerInterface $requestHandler, ConfigurationManager $configurationManager)
     {
+        $this->requestHandler = $requestHandler;
         $this->configurationManager = $configurationManager;
     }
 
index bf8c568..88f05d8 100644 (file)
@@ -20,10 +20,9 @@ use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Exception;
-use TYPO3\CMS\Core\Http\Dispatcher;
+use TYPO3\CMS\Core\Http\DispatcherInterface;
 use TYPO3\CMS\Core\Http\NullResponse;
 use TYPO3\CMS\Core\Http\Response;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Lightweight alternative to regular frontend requests; used when $_GET[eID] is set.
@@ -35,6 +34,19 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class EidHandler implements MiddlewareInterface
 {
     /**
+     * @var DispatcherInterface
+     */
+    protected $dispatcher;
+
+    /**
+     * @param DispatcherInterface $dispatcher
+     */
+    public function __construct(DispatcherInterface $dispatcher)
+    {
+        $this->dispatcher = $dispatcher;
+    }
+
+    /**
      * Dispatches the request to the corresponding eID class or eID script
      *
      * @param ServerRequestInterface $request
@@ -58,8 +70,7 @@ class EidHandler implements MiddlewareInterface
             return (new Response())->withStatus(404, 'eID not registered');
         }
 
-        $dispatcher = GeneralUtility::makeInstance(Dispatcher::class);
         $request = $request->withAttribute('target', $target);
-        return $dispatcher->dispatch($request) ?? new NullResponse();
+        return $this->dispatcher->dispatch($request) ?? new NullResponse();
     }
 }
diff --git a/typo3/sysext/frontend/Classes/ServiceProvider.php b/typo3/sysext/frontend/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..aad761f
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
+use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Exception as CoreException;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Http\Application::class => [ static::class, 'getApplication' ],
+            Http\RequestHandler::class => [ static::class, 'getRequestHandler' ],
+            'frontend.middlewares' => [ static::class, 'getFrontendMiddlewares' ],
+        ];
+    }
+
+    public static function getApplication(ContainerInterface $container): Http\Application
+    {
+        $requestHandler = new MiddlewareDispatcher(
+            $container->get(Http\RequestHandler::class),
+            $container->get('frontend.middlewares'),
+            $container
+        );
+        return new Http\Application($requestHandler, $container->get(ConfigurationManager::class));
+    }
+
+    public static function getRequestHandler(ContainerInterface $container): Http\RequestHandler
+    {
+        return new Http\RequestHandler;
+    }
+
+    /**
+     * @param ContainerInterface $container
+     * @return array
+     * @throws InvalidDataException
+     * @throws CoreException
+     */
+    public static function getFrontendMiddlewares(ContainerInterface $container): array
+    {
+        return $container->get(MiddlewareStackResolver::class)->resolve('frontend');
+    }
+}
diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..0a7c638
--- /dev/null
@@ -0,0 +1,18 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Frontend\:
+    resource: '../Classes/*'
+
+  TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController:
+    factory: ['TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController', getGlobalInstance]
+    shared: false
+    autoconfigure: false
+    autowire: false
+
+  TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer:
+    public: true
+    shared: false
index c14f14c..f991a57 100644 (file)
@@ -30,6 +30,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Frontend\\ServiceProvider",
                                "protected": true,
                                "partOfFactoryDefault": true,
                                "partOfMinimalUsableSystem": true
diff --git a/typo3/sysext/impexp/Configuration/Services.yaml b/typo3/sysext/impexp/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..825fd78
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Impexp\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/indexed_search/Configuration/Services.yaml b/typo3/sysext/indexed_search/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..66394f0
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\IndexedSearch\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/info/Configuration/Services.yaml b/typo3/sysext/info/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..cb3863e
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Info\:
+    resource: '../Classes/*'
index 093a2d4..43b0050 100644 (file)
@@ -24,6 +24,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
 use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
 use TYPO3\CMS\Install\Updates\ChattyInterface;
 use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
@@ -55,10 +56,7 @@ class UpgradeWizardListCommand extends Command
      */
     protected function bootstrap(): void
     {
-        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        Bootstrap::unsetReservedGlobalVariables();
-        Bootstrap::loadBaseTca(false);
-        Bootstrap::loadExtTables(false);
+        GeneralUtility::makeInstance(LateBootService::class)->loadExtLocalconfDatabaseAndExtTables();
         Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
         Bootstrap::initializeBackendAuthentication();
     }
index 6978299..23ec72e 100644 (file)
@@ -25,6 +25,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
 use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
 use TYPO3\CMS\Install\Updates\ChattyInterface;
 use TYPO3\CMS\Install\Updates\ConfirmableInterface;
@@ -60,10 +61,7 @@ class UpgradeWizardRunCommand extends Command
      */
     protected function bootstrap(): void
     {
-        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        Bootstrap::unsetReservedGlobalVariables();
-        Bootstrap::loadBaseTca(false);
-        Bootstrap::loadExtTables(false);
+        GeneralUtility::makeInstance(LateBootService::class)->loadExtLocalconfDatabaseAndExtTables();
         Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
         Bootstrap::initializeBackendAuthentication();
         $this->upgradeWizardsService = new UpgradeWizardsService();
index 33b9e9a..85c4fca 100644 (file)
@@ -15,10 +15,12 @@ namespace TYPO3\CMS\Install\Controller;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Install\Service\LateBootService;
 
 /**
  * Controller abstract for shared parts of the install tool
@@ -56,12 +58,11 @@ class AbstractController
      *
      * Those actions can potentially fatal if some old extension is loaded that triggers
      * a fatal in ext_localconf or ext_tables code! Use only if really needed.
+     *
+     * @return ContainerInterface
      */
-    protected function loadExtLocalconfDatabaseAndExtTables()
+    protected function loadExtLocalconfDatabaseAndExtTables(): ContainerInterface
     {
-        \TYPO3\CMS\Core\Core\Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        \TYPO3\CMS\Core\Core\Bootstrap::unsetReservedGlobalVariables();
-        \TYPO3\CMS\Core\Core\Bootstrap::loadBaseTca(false);
-        \TYPO3\CMS\Core\Core\Bootstrap::loadExtTables(false);
+        return GeneralUtility::makeInstance(LateBootService::class)->loadExtLocalconfDatabaseAndExtTables();
     }
 }
index 5f0ddfd..33575a3 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Install\Controller;
 
 use Doctrine\DBAL\DBALException;
 use Doctrine\DBAL\DriverManager;
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
@@ -51,6 +52,7 @@ use TYPO3\CMS\Install\Configuration\FeatureManager;
 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
 use TYPO3\CMS\Install\Service\EnableFileService;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
 use TYPO3\CMS\Install\SystemEnvironment\Check;
 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
@@ -1165,9 +1167,9 @@ For each website you need a TypoScript template on the main page of your website
         // Will load ext_localconf and ext_tables. This is pretty safe here since we are
         // in first install (database empty), so it is very likely that no extension is loaded
         // that could trigger a fatal at this point.
-        $this->loadExtLocalconfDatabaseAndExtTables();
+        $container = $this->loadExtLocalconfDatabaseAndExtTables();
 
-        $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
+        $sqlReader = $container->get(SqlReader::class);
         $sqlCode = $sqlReader->getTablesDefinitionString(true);
         $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
         $createTableStatements = $sqlReader->getCreateTableStatementArray($sqlCode);
@@ -1199,13 +1201,12 @@ For each website you need a TypoScript template on the main page of your website
      *
      * Those actions can potentially fatal if some old extension is loaded that triggers
      * a fatal in ext_localconf or ext_tables code! Use only if really needed.
+     *
+     * @return ContainerInterface
      */
-    protected function loadExtLocalconfDatabaseAndExtTables()
+    protected function loadExtLocalconfDatabaseAndExtTables(): ContainerInterface
     {
-        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        Bootstrap::unsetReservedGlobalVariables();
-        Bootstrap::loadBaseTca(false);
-        Bootstrap::loadExtTables(false);
+        return GeneralUtility::makeInstance(LateBootService::class)->loadExtLocalconfDatabaseAndExtTables();
     }
 
     /**
index a1b4d72..25032a6 100644 (file)
@@ -202,11 +202,11 @@ class MaintenanceController extends AbstractController
      */
     public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
     {
-        $this->loadExtLocalconfDatabaseAndExtTables();
+        $container = $this->loadExtLocalconfDatabaseAndExtTables();
         $messageQueue = new FlashMessageQueue('install');
         $suggestions = [];
         try {
-            $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
+            $sqlReader = $container->get(SqlReader::class);
             $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
             $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
             $addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements);
@@ -358,7 +358,7 @@ class MaintenanceController extends AbstractController
      */
     public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
     {
-        $this->loadExtLocalconfDatabaseAndExtTables();
+        $container = $this->loadExtLocalconfDatabaseAndExtTables();
         $messageQueue = new FlashMessageQueue('install');
         $selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
         if (empty($selectedHashes)) {
@@ -368,7 +368,7 @@ class MaintenanceController extends AbstractController
                 FlashMessage::WARNING
             ));
         } else {
-            $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
+            $sqlReader = $container->get(SqlReader::class);
             $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
             $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
             $statementHashesToPerform = array_flip($selectedHashes);
index e543349..8c26395 100644 (file)
@@ -59,6 +59,7 @@ use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyPublicMatcher;
 use TYPO3\CMS\Install\ExtensionScanner\Php\MatcherFactory;
 use TYPO3\CMS\Install\Service\CoreUpdateService;
 use TYPO3\CMS\Install\Service\CoreVersionService;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\LoadTcaService;
 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
 use TYPO3\CMS\Install\UpgradeAnalysis\DocumentationFile;
@@ -85,11 +86,18 @@ class UpgradeController extends AbstractController
     protected $packageManager;
 
     /**
-     * @param PackageManager|null $packageManager
+     * @var LateBootService
      */
-    public function __construct(PackageManager $packageManager = null)
+    private $lateBootService;
+
+    /**
+     * @param PackageManager $packageManager
+     * @param LateBootService $lateBootService
+     */
+    public function __construct(PackageManager $packageManager, LateBootService $lateBootService)
     {
-        $this->packageManager = $packageManager ?? GeneralUtility::makeInstance(PackageManager::class);
+        $this->packageManager = $packageManager;
+        $this->lateBootService = $lateBootService;
     }
 
     /**
@@ -421,6 +429,9 @@ class UpgradeController extends AbstractController
      */
     public function extensionCompatTesterLoadExtLocalconfAction(ServerRequestInterface $request): ResponseInterface
     {
+        $container = $this->lateBootService->getContainer();
+        $backup = $this->lateBootService->makeCurrent($container);
+
         $extension = $request->getParsedBody()['install']['extension'];
         foreach ($this->packageManager->getActivePackages() as $package) {
             $this->extensionCompatTesterLoadExtLocalconfForExtension($package);
@@ -428,6 +439,9 @@ class UpgradeController extends AbstractController
                 break;
             }
         }
+
+        $this->lateBootService->makeCurrent(null, $backup);
+
         return new JsonResponse([
             'success' => true,
         ]);
@@ -441,6 +455,9 @@ class UpgradeController extends AbstractController
      */
     public function extensionCompatTesterLoadExtTablesAction(ServerRequestInterface $request): ResponseInterface
     {
+        $container = $this->lateBootService->getContainer();
+        $backup = $this->lateBootService->makeCurrent($container);
+
         $extension = $request->getParsedBody()['install']['extension'];
         $activePackages = $this->packageManager->getActivePackages();
         foreach ($activePackages as $package) {
@@ -453,6 +470,9 @@ class UpgradeController extends AbstractController
                 break;
             }
         }
+
+        $this->lateBootService->makeCurrent(null, $backup);
+
         return new JsonResponse([
             'success' => true,
         ]);
index 607b34a..d632390 100644 (file)
@@ -17,17 +17,13 @@ namespace TYPO3\CMS\Install\Http;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Context\VisibilityAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
 use TYPO3\CMS\Core\Http\AbstractApplication;
-use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Install\Middleware\Installer;
-use TYPO3\CMS\Install\Middleware\Maintenance;
 
 /**
  * Entry point for the TYPO3 Install Tool
@@ -36,36 +32,11 @@ use TYPO3\CMS\Install\Middleware\Maintenance;
 class Application extends AbstractApplication
 {
     /**
-     * @var string
-     */
-    protected $requestHandler = NotFoundRequestHandler::class;
-
-    /**
-     * @var ConfigurationManager
-     */
-    protected $configurationManager;
-
-    /**
-     * @param ConfigurationManager $configurationManager
-     */
-    public function __construct(ConfigurationManager $configurationManager)
-    {
-        $this->configurationManager = $configurationManager;
-    }
-
-    /**
      * @param RequestHandlerInterface $requestHandler
-     * @return MiddlewareDispatcher
      */
-    protected function createMiddlewareDispatcher(RequestHandlerInterface $requestHandler): MiddlewareDispatcher
+    public function __construct(RequestHandlerInterface $requestHandler)
     {
-        $dispatcher = new MiddlewareDispatcher($requestHandler);
-
-        // Stack of middlewares, executed LIFO
-        $dispatcher->lazy(Installer::class);
-        $dispatcher->add(GeneralUtility::makeInstance(Maintenance::class, $this->configurationManager));
-
-        return $dispatcher;
+        $this->requestHandler = $requestHandler;
     }
 
     /**
index cf19621..451cf82 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Install\Middleware;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
@@ -56,6 +57,11 @@ class Maintenance implements MiddlewareInterface
     protected $configurationManager;
 
     /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
      * @var array List of valid controllers
      */
     protected $controllers = [
@@ -71,9 +77,10 @@ class Maintenance implements MiddlewareInterface
     /**
      * @param ConfigurationManager $configurationManager
      */
-    public function __construct(ConfigurationManager $configurationManager)
+    public function __construct(ConfigurationManager $configurationManager, ContainerInterface $container)
     {
         $this->configurationManager = $configurationManager;
+        $this->container = $container;
     }
 
     /**
@@ -188,8 +195,9 @@ class Maintenance implements MiddlewareInterface
                 );
             }
             $this->recreatePackageStatesFileIfMissing();
+            $className = $this->controllers[$controllerName];
             /** @var AbstractController $controller */
-            $controller = new $this->controllers[$controllerName];
+            $controller = $this->container->has($className) ? $this->container->get($className) : new $className;
             if (!method_exists($controller, $action)) {
                 throw new \RuntimeException(
                     'Unknown action method ' . $action . ' in controller ' . $controllerName,
index e7996ba..175c11c 100644 (file)
@@ -14,7 +14,6 @@ namespace TYPO3\CMS\Install\Service;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -26,6 +25,16 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class ClearCacheService
 {
     /**
+     * @var LateBootService
+     */
+    private $lateBootService;
+
+    public function __construct(LateBootService $lateBootService)
+    {
+        $this->lateBootService = $lateBootService;
+    }
+
+    /**
      * This clear cache implementation follows a pretty brutal approach.
      * Goal is to reliably get rid of cache entries, even if some broken
      * extension is loaded that would kill the backend 'clear cache' action.
@@ -64,14 +73,10 @@ class ClearCacheService
         }
 
         // From this point on, the code may fatal, if some broken extension is loaded.
-
-        // Use bootstrap to load all ext_localconf and ext_tables
-        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
-        Bootstrap::unsetReservedGlobalVariables();
-        Bootstrap::loadBaseTca(false);
-        Bootstrap::loadExtTables(false);
+        $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
 
         // The cache manager is already instantiated in the install tool
+        // (both in the failsafe and the late boot container), but
         // with some hacked settings to disable caching of extbase and fluid.
         // We want a "fresh" object here to operate on a different cache setup.
         // cacheManager implements SingletonInterface, so the only way to get a "fresh"
diff --git a/typo3/sysext/install/Classes/Service/LateBootService.php b/typo3/sysext/install/Classes/Service/LateBootService.php
new file mode 100644 (file)
index 0000000..1e79d41
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\Imaging\IconRegistry;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal This is NOT an API class, it is for internal use in the install tool only.
+ */
+class LateBootService
+{
+    /**
+     * @var ContainerBuilder
+     */
+    private $containerBuilder;
+
+    /**
+     * @var ContainerInterface
+     */
+    private $failsafeContainer;
+
+    /**
+     * @var ContainerInterface
+     */
+    private $container = null;
+
+    /**
+     * @param ContainerBuilder $containerBuilder
+     * @param ConteinerInterface $failsafeContainer
+     */
+    public function __construct(ContainerBuilder $containerBuilder, ContainerInterface $failsafeContainer)
+    {
+        $this->containerBuilder = $containerBuilder;
+        $this->failsafeContainer = $failsafeContainer;
+    }
+
+    /**
+     * @return ContainerInterface
+     */
+    public function getContainer(): ContainerInterface
+    {
+        return $this->container ?? $this->prepareContainer();
+    }
+
+    /**
+     * @return ContainerInterface
+     */
+    private function prepareContainer(): ContainerInterface
+    {
+        $packageManager = $this->failsafeContainer->get(PackageManager::class);
+
+        // Use caching for the full boot – uncached symfony autowiring for every install-tool lateboot request would be too slow.
+        $disableCaching = false;
+        $coreCache = Bootstrap::createCache('core', $disableCaching);
+
+        $failsafe = false;
+
+        // Build a non-failsafe container which is required for loading ext_localconf
+        return $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $coreCache, $failsafe);
+    }
+
+    /**
+     * Switch global context to a new context, or revert
+     * to the original booting container if no container
+     * is specified
+     *
+     * @param ContainerInterface $container
+     * @param array $backup
+     * @return array
+     */
+    public function makeCurrent(ContainerInterface $container = null, array $oldBackup = []): array
+    {
+        $container = $container ?? $this->failsafeContainer;
+
+        $backup = [
+            'singletonInstances', GeneralUtility::getSingletonInstances(),
+        ];
+
+        GeneralUtility::purgeInstances();
+
+        // Set global state to the non-failsafe container and it's instances
+        GeneralUtility::setContainer($container);
+        ExtensionManagementUtility::setPackageManager($container->get(PackageManager::class));
+
+        $backupSingletonInstances = $oldBackup['singletonInstances'] ?? [];
+        foreach ($backupSingletonInstances as $className => $instance) {
+            GeneralUtility::setSingletonInstance($className, $instance);
+        }
+
+        return $backup;
+    }
+
+    /**
+     * Bootstrap a non-failsafe container and load ext_localconf
+     *
+     * @return ContainerInterface
+     */
+    public function loadExtLocalconfDatabaseAndExtTables(): ContainerInterface
+    {
+        $container = $this->getContainer();
+
+        $backup = $this->makeCurrent($container);
+
+        $container->get('boot.state')->done = false;
+        $assetsCache = $container->get('cache.assets');
+        IconRegistry::setCache($assetsCache);
+        PageRenderer::setCache($assetsCache);
+        Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
+        Bootstrap::unsetReservedGlobalVariables();
+        Bootstrap::loadBaseTca(false);
+        Bootstrap::loadExtTables(false);
+        $container->get('boot.state')->done = true;
+
+        $this->makeCurrent(null, $backup);
+
+        return $container;
+    }
+}
index 8c07844..0a61878 100644 (file)
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Install\Service;
 
 use TYPO3\CMS\Core\Category\CategoryRegistry;
 use TYPO3\CMS\Core\Package\PackageManager;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Service for loading the TCA
@@ -25,6 +24,16 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class LoadTcaService
 {
     /**
+     * @var LateBootService
+     */
+    private $lateBootService;
+
+    public function __construct(LateBootService $lateBootService)
+    {
+        $this->lateBootService = $lateBootService;
+    }
+
+    /**
      * Load TCA
      * Mostly a copy of ExtensionManagementUtility to include TCA without migrations.
      * To be used in install tool only.
@@ -35,9 +44,12 @@ class LoadTcaService
      */
     public function loadExtensionTablesWithoutMigration()
     {
+        $container = $this->lateBootService->getContainer();
+        $backup = $this->lateBootService->makeCurrent($container);
+
         $GLOBALS['TCA'] = [];
 
-        $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
+        $activePackages = $container->get(PackageManager::class)->getActivePackages();
 
         // First load "full table" files from Configuration/TCA
         foreach ($activePackages as $package) {
@@ -80,6 +92,8 @@ class LoadTcaService
                 }
             }
         }
+
+        $this->lateBootService->makeCurrent(null, $backup);
     }
 
     /**
@@ -89,7 +103,10 @@ class LoadTcaService
      */
     public function loadSingleExtTablesFile(string $extensionKey)
     {
-        $packageManager = GeneralUtility::makeInstance(PackageManager::class);
+        $container = $this->lateBootService->getContainer();
+        $backup = $this->lateBootService->makeCurrent($container);
+
+        $packageManager = $container->get(PackageManager::class);
         try {
             $package = $packageManager->getPackage($extensionKey);
         } catch (\TYPO3\CMS\Core\Package\Exception\UnknownPackageException $e) {
@@ -104,5 +121,7 @@ class LoadTcaService
         if (@file_exists($extTablesPath)) {
             require $extTablesPath;
         }
+
+        $this->lateBootService->makeCurrent(null, $backup);
     }
 }
diff --git a/typo3/sysext/install/Classes/ServiceProvider.php b/typo3/sysext/install/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..1ad772e
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Http\Application::class => [ static::class, 'getApplication' ],
+            Http\NotFoundRequestHandler::class => [ static::class, 'getNotFoundRequestHandler' ],
+            Service\LateBootService::class => [ static::class, 'getLateBootService' ],
+            Service\ClearCacheService::class => [ static::class, 'getClearCacheService' ],
+            Service\LoadTcaService::class => [ static::class, 'getLoadTcaService' ],
+            Middleware\Maintenance::class => [ static::class, 'getMaintenanceMiddleware' ],
+            Controller\UpgradeController::class => [ static::class, 'getUpgradeController' ],
+        ];
+    }
+
+    public static function getApplication(ContainerInterface $container): Http\Application
+    {
+        $requestHandler = $container->get(Http\NotFoundRequestHandler::class);
+        $dispatcher = new MiddlewareDispatcher($requestHandler, [], $container);
+
+        // Stack of middlewares, executed LIFO
+        $dispatcher->lazy(Middleware\Installer::class);
+        $dispatcher->add($container->get(Middleware\Maintenance::class));
+
+        return new Http\Application($dispatcher);
+    }
+
+    public static function getNotFoundRequestHandler(ContainerInterface $container): Http\NotFoundRequestHandler
+    {
+        return new Http\NotFoundRequestHandler;
+    }
+
+    public static function getLateBootService(ContainerInterface $container): Service\LateBootService
+    {
+        return new Service\LateBootService(
+            $container->get(ContainerBuilder::class),
+            $container
+        );
+    }
+
+    public static function getClearCacheService(ContainerInterface $container): Service\ClearCacheService
+    {
+        return new Service\ClearCacheService($container->get(Service\LateBootService::class));
+    }
+
+    public static function getLoadTcaService(ContainerInterface $container): Service\LoadTcaService
+    {
+        return new Service\LoadTcaService($container->get(Service\LateBootService::class));
+    }
+
+    public static function getMaintenanceMiddleware(ContainerInterface $container): Middleware\Maintenance
+    {
+        return new Middleware\Maintenance(
+            $container->get(ConfigurationManager::class),
+            $container
+        );
+    }
+
+    public static function getUpgradeController(ContainerInterface $container): Controller\UpgradeController
+    {
+        return new Controller\UpgradeController(
+            $container->get(PackageManager::class),
+            $container->get(Service\LateBootService::class)
+        );
+    }
+}
index 1ee6226..ddb75d7 100644 (file)
@@ -20,6 +20,7 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Install\Controller\UpgradeController;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -74,9 +75,13 @@ class UpgradeControllerTest extends UnitTestCase
         $packageManagerMock = $this->getMockBuilder(PackageManager::class)
             ->disableOriginalConstructor()
             ->getMock();
+        $lateBootServiceMock = $this->getMockBuilder(LateBootService::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
         /** @var UpgradeController|\PHPUnit\Framework\MockObject\MockObject $subject */
         $subject = $this->getMockBuilder(UpgradeController::class)
-            ->setConstructorArgs([$packageManagerMock])
+            ->setConstructorArgs([$packageManagerMock, $lateBootServiceMock])
             ->setMethods(['getDocumentationFiles', 'initializeStandaloneView'])
             ->getMock();
 
index 9c16fc8..0f13d01 100644 (file)
@@ -31,6 +31,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Install\\ServiceProvider",
                                "protected": true,
                                "partOfFactoryDefault": true,
                                "partOfMinimalUsableSystem": true
diff --git a/typo3/sysext/linkvalidator/Configuration/Services.yaml b/typo3/sysext/linkvalidator/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..f5615ce
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Linkvalidator\:
+    resource: '../Classes/*'
index ffbb3a5..a9d624e 100644 (file)
@@ -15,18 +15,15 @@ namespace TYPO3\CMS\Lowlevel\Controller;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Container\ContainerInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Template\ModuleTemplate;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Http\HtmlResponse;
-use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
 use TYPO3\CMS\Core\Localization\LanguageService;
-use TYPO3\CMS\Core\Package\PackageManager;
-use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
@@ -39,6 +36,16 @@ use TYPO3\CMS\Lowlevel\Utility\ArrayBrowser;
 class ConfigurationController
 {
     /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->container = $container;
+    }
+
+    /**
      * Available trees to render.
      *  * label is an LLL identifier
      *  * type is used to identify the data source type
@@ -220,17 +227,12 @@ class ConfigurationController
         } elseif ($selectedTreeDetails['type'] === 'httpMiddlewareStacks') {
             // Keep the order of the keys
             $sortKeysByName = false;
-            $stackResolver = GeneralUtility::makeInstance(
-                MiddlewareStackResolver::class,
-                GeneralUtility::makeInstance(PackageManager::class),
-                GeneralUtility::makeInstance(DependencyOrderingService::class),
-                GeneralUtility::makeInstance(CacheManager::class)->getCache('core')
-            );
             $renderArray = [];
             foreach (['frontend', 'backend'] as $stackName) {
                 // reversing the array allows the admin to read the stack from top to bottom
-                $renderArray[$stackName] = array_reverse($stackResolver->resolve($stackName));
+                $renderArray[$stackName] = array_reverse($this->container->get($stackName . '.middlewares'));
             }
+            $renderArray['raw'] = $this->container->get('middlewares');
         } elseif ($selectedTreeDetails['type'] === 'siteConfiguration') {
             $renderArray = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
         } else {
diff --git a/typo3/sysext/lowlevel/Classes/ServiceProvider.php b/typo3/sysext/lowlevel/Classes/ServiceProvider.php
new file mode 100644 (file)
index 0000000..ee78162
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Lowlevel;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Package\AbstractServiceProvider;
+
+/**
+ * @internal
+ */
+class ServiceProvider extends AbstractServiceProvider
+{
+    protected static function getPackagePath(): string
+    {
+        return __DIR__ . '/../';
+    }
+
+    public function getFactories(): array
+    {
+        return [
+            Controller\ConfigurationController::class => [ static::class, 'getConfigurationController' ],
+        ];
+    }
+
+    public static function getConfigurationController(ContainerInterface $container): Controller\ConfigurationController
+    {
+        return self::new($container, Controller\ConfigurationController::class, [$container]);
+    }
+}
diff --git a/typo3/sysext/lowlevel/Configuration/Services.yaml b/typo3/sysext/lowlevel/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..6ded070
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Lowlevel\:
+    resource: '../Classes/*'
index 388e4da..6d3b3aa 100644 (file)
@@ -27,6 +27,7 @@
                },
                "typo3/cms": {
                        "Package": {
+                               "serviceProvider": "TYPO3\\CMS\\Lowlevel\\ServiceProvider",
                                "partOfFactoryDefault": true
                        },
                        "extension-key": "lowlevel"
diff --git a/typo3/sysext/opendocs/Configuration/Services.yaml b/typo3/sysext/opendocs/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..5e278e8
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Opendocs\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/recordlist/Configuration/Services.yaml b/typo3/sysext/recordlist/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..56cc690
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Recordlist\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/recycler/Configuration/Services.yaml b/typo3/sysext/recycler/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..b392e22
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Recycler\:
+    resource: '../Classes/*'
index d59a08a..a4452c7 100644 (file)
@@ -39,6 +39,16 @@ class RedirectHandler implements MiddlewareInterface, LoggerAwareInterface
     use LoggerAwareTrait;
 
     /**
+     * @var RedirectService
+     */
+    protected $redirectService;
+
+    public function __construct(RedirectService $redirectService)
+    {
+        $this->redirectService = $redirectService;
+    }
+
+    /**
      * First hook within the Frontend Request handling
      *
      * @param ServerRequestInterface $request
@@ -47,9 +57,8 @@ class RedirectHandler implements MiddlewareInterface, LoggerAwareInterface
      */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
-        $redirectService = GeneralUtility::makeInstance(RedirectService::class);
         $port = $request->getUri()->getPort();
-        $matchedRedirect = $redirectService->matchRedirect(
+        $matchedRedirect = $this->redirectService->matchRedirect(
             $request->getUri()->getHost() . ($port ? ':' . $port : ''),
             $request->getUri()->getPath(),
             $request->getUri()->getQuery() ?? ''
@@ -57,7 +66,7 @@ class RedirectHandler implements MiddlewareInterface, LoggerAwareInterface
 
         // If the matched redirect is found, resolve it, and check further
         if (is_array($matchedRedirect)) {
-            $url = $redirectService->getTargetUrl($matchedRedirect, $request->getQueryParams(), $request->getAttribute('site', null));
+            $url = $this->redirectService->getTargetUrl($matchedRedirect, $request->getQueryParams(), $request->getAttribute('site', null));
             if ($url instanceof UriInterface) {
                 $this->logger->debug('Redirecting', ['record' => $matchedRedirect, 'uri' => $url]);
                 $response = $this->buildRedirectResponse($url, $matchedRedirect);
index 47abfde..12e4f3d 100644 (file)
@@ -41,6 +41,22 @@ class RedirectService implements LoggerAwareInterface
     use LoggerAwareTrait;
 
     /**
+     * @var RedirectCacheService
+     */
+    protected $redirectCacheService;
+
+    /**
+     * @var LinkService
+     */
+    protected $linkService;
+
+    public function __construct(RedirectCacheService $redirectCacheService, LinkService $linkService)
+    {
+        $this->redirectCacheService = $redirectCacheService;
+        $this->linkService = $linkService;
+    }
+
+    /**
      * Checks against all available redirects "flat" or "regexp", and against starttime/endtime
      *
      * @param string $domain
@@ -118,7 +134,7 @@ class RedirectService implements LoggerAwareInterface
      */
     protected function fetchRedirects(): array
     {
-        return GeneralUtility::makeInstance(RedirectCacheService::class)->getRedirects();
+        return $this->redirectCacheService->getRedirects();
     }
 
     /**
@@ -130,10 +146,8 @@ class RedirectService implements LoggerAwareInterface
      */
     protected function resolveLinkDetailsFromLinkTarget(string $redirectTarget): array
     {
-        // build the target URL, take force SSL into account etc.
-        $linkService = GeneralUtility::makeInstance(LinkService::class);
         try {
-            $linkDetails = $linkService->resolve($redirectTarget);
+            $linkDetails = $this->linkService->resolve($redirectTarget);
             switch ($linkDetails['type']) {
                 case LinkService::TYPE_URL:
                     // all set up, nothing to do
diff --git a/typo3/sysext/redirects/Configuration/Services.yaml b/typo3/sysext/redirects/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..f0fde48
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Redirects\:
+    resource: '../Classes/*'
index 3baee98..3b496f9 100644 (file)
@@ -24,7 +24,6 @@ use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
 use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Site\Entity\Site;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Redirects\Service\RedirectCacheService;
 use TYPO3\CMS\Redirects\Service\RedirectService;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -42,6 +41,11 @@ class RedirectServiceTest extends UnitTestCase
     protected $redirectCacheServiceProphecy;
 
     /**
+     * @var LinkService|ObjectProphecy
+     */
+    protected $linkServiceProphecy;
+
+    /**
      * @var RedirectService
      */
     protected $redirectService;
@@ -51,7 +55,9 @@ class RedirectServiceTest extends UnitTestCase
         parent::setUp();
         $loggerProphecy = $this->prophesize(LoggerInterface::class);
         $this->redirectCacheServiceProphecy = $this->prophesize(RedirectCacheService::class);
-        $this->redirectService = new RedirectService();
+        $this->linkServiceProphecy = $this->prophesize(LinkService::class);
+
+        $this->redirectService = new RedirectService($this->redirectCacheServiceProphecy->reveal(), $this->linkServiceProphecy->reveal());
         $this->redirectService->setLogger($loggerProphecy->reveal());
 
         $GLOBALS['SIM_ACCESS_TIME'] = 42;
@@ -63,7 +69,6 @@ class RedirectServiceTest extends UnitTestCase
     public function matchRedirectReturnsNullIfNoRedirectsExist()
     {
         $this->redirectCacheServiceProphecy->getRedirects()->willReturn([]);
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'foo');
 
@@ -95,7 +100,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'foo');
 
@@ -128,7 +132,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'index.php', 'id=123');
 
@@ -161,7 +164,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'index.php', 'id=123');
 
@@ -194,7 +196,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'index.php', 'id=123&a=b');
 
@@ -227,7 +228,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'index.php', 'id=123&a=a');
 
@@ -276,7 +276,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'special/page', 'key=998877');
 
@@ -324,7 +323,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'foo');
 
@@ -356,7 +354,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'foo');
 
@@ -398,7 +395,6 @@ class RedirectServiceTest extends UnitTestCase
                 ],
             ]
         );
-        GeneralUtility::addInstance(RedirectCacheService::class, $this->redirectCacheServiceProphecy->reveal());
 
         $result = $this->redirectService->matchRedirect('example.com', 'foo');
 
@@ -410,9 +406,7 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlReturnsNullIfUrlCouldNotBeResolved()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
-        $linkServiceProphecy->resolve(Argument::any())->willThrow(new InvalidPathException('', 1516531195));
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve(Argument::any())->willThrow(new InvalidPathException('', 1516531195));
 
         $result = $this->redirectService->getTargetUrl(['target' => 'invalid'], [], new Site('dummy', 13, []));
 
@@ -424,7 +418,6 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlReturnsUrlForTypeUrl()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
         $redirectTargetMatch = [
             'target' => 'https://example.com',
             'force_https' => '0',
@@ -434,8 +427,7 @@ class RedirectServiceTest extends UnitTestCase
             'type' => LinkService::TYPE_URL,
             'url' => 'https://example.com/'
         ];
-        $linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
 
         $result = $this->redirectService->getTargetUrl($redirectTargetMatch, [], new Site('dummy', 13, []));
 
@@ -448,7 +440,6 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlReturnsUrlForTypeFile()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
         $fileProphecy = $this->prophesize(File::class);
         $fileProphecy->getPublicUrl()->willReturn('https://example.com/file.txt');
         $redirectTargetMatch = [
@@ -460,8 +451,7 @@ class RedirectServiceTest extends UnitTestCase
             'type' => LinkService::TYPE_FILE,
             'file' => $fileProphecy->reveal()
         ];
-        $linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
 
         $result = $this->redirectService->getTargetUrl($redirectTargetMatch, [], new Site('dummy', 13, []));
 
@@ -474,7 +464,6 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlReturnsUrlForTypeFolder()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
         $folderProphecy = $this->prophesize(Folder::class);
         $folderProphecy->getPublicUrl()->willReturn('https://example.com/folder/');
         $redirectTargetMatch = [
@@ -487,8 +476,7 @@ class RedirectServiceTest extends UnitTestCase
             'type' => LinkService::TYPE_FOLDER,
             'folder' => $folder
         ];
-        $linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
 
         $result = $this->redirectService->getTargetUrl($redirectTargetMatch, [], new Site('dummy', 13, []));
 
@@ -501,7 +489,6 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlRespectsForceHttps()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
         $redirectTargetMatch = [
             'target' => 'https://example.com',
             'keep_query_parameters' => '0',
@@ -511,8 +498,7 @@ class RedirectServiceTest extends UnitTestCase
             'type' => LinkService::TYPE_URL,
             'url' => 'http://example.com'
         ];
-        $linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
 
         $result = $this->redirectService->getTargetUrl($redirectTargetMatch, [], new Site('dummy', 13, []));
 
@@ -525,7 +511,6 @@ class RedirectServiceTest extends UnitTestCase
      */
     public function getTargetUrlAddsExistingQueryParams()
     {
-        $linkServiceProphecy = $this->prophesize(LinkService::class);
         $redirectTargetMatch = [
             'target' => 'https://example.com',
             'force_https' => '0',
@@ -535,8 +520,7 @@ class RedirectServiceTest extends UnitTestCase
             'type' => LinkService::TYPE_URL,
             'url' => 'https://example.com/?foo=1&bar=2'
         ];
-        $linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
-        GeneralUtility::setSingletonInstance(LinkService::class, $linkServiceProphecy->reveal());
+        $this->linkServiceProphecy->resolve($redirectTargetMatch['target'])->willReturn($linkDetails);
 
         $result = $this->redirectService->getTargetUrl($redirectTargetMatch, ['bar' => 3, 'baz' => 4], new Site('dummy', 13, []));
 
diff --git a/typo3/sysext/reports/Configuration/Services.yaml b/typo3/sysext/reports/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..c98772c
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Reports\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/rte_ckeditor/Configuration/Services.yaml b/typo3/sysext/rte_ckeditor/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..bdfce1a
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\RteCKEditor\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/scheduler/Configuration/Services.yaml b/typo3/sysext/scheduler/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..e854733
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Scheduler\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/seo/Configuration/Services.yaml b/typo3/sysext/seo/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..484cb85
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Seo\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/setup/Configuration/Services.yaml b/typo3/sysext/setup/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..1b5ed07
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Setup\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/sys_note/Configuration/Services.yaml b/typo3/sysext/sys_note/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..ebca06a
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\SysNote\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/t3editor/Configuration/Services.yaml b/typo3/sysext/t3editor/Configuration/Services.yaml
new file mode 100644 (file)
index 0000000..f9134d1
--- /dev/null
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+&nb