Object Framework
The lifecycle of objects are managed centrally by the object framework. It offers convenient support for Dependency Injection and provides some additional features such as a caching mechanism for objects. Because all packages are built on this foundation it is important to understand the general concept of objects in Flow. Note, the object management features of Flow are by default only enabled for classes in packages belonging to one of the neos-*` package types. All other classes are not considered by default. If you need that (see Enabling Other Package Classes For Object Management).
Tip
A very good start to understand the idea of Inversion of Control and Dependency Injection is reading Martin Fowler’s article on the topic.
Creating Objects
In simple, self-contained applications, creating objects is as simple as using the new
operator. However, as the program gets more complex, a developer is confronted with
solving dependencies to other objects, make classes configurable (maybe through a factory
method) and finally assure a certain scope for the object (such as Singleton
or
Prototype
). Howard Lewis Ship explained this circumstances nicely in his blog
(quite some time ago):
Once you start thinking in terms of large numbers of objects, and a whole lot of just in time object creation and configuration, the question of how to create a new object doesn’t change (that’s what
new
is for) … but the questions when and who become difficult to tackle. Especially when the when is very dynamic, due to just-in-time instantiation, and the who is unknown, because there are so many places a particular object may be used.
The Object Manager is responsible for object building and dependency resolution (we’ll discover shortly why dependency injection makes such a difference to your application design). In order to fulfill its task, it is important that all objects are instantiated only through the object framework.
Important
As a general rule of thumb for those not developing the Flow core itself but your very own packages:
Use Dependency Injection whenever possible for retrieving singletons.
Object Scopes
Objects live in a specific scope. The most commonly used are prototype and singleton:
Scope |
Description |
---|---|
singleton |
The object instance is unique during one request - each
injection by the Object Manager or explicit call of
|
prototype (default) |
The object instance is not unique - each injection or call of
the Object Factory’s |
session |
The object instance is unique during the whole user session -
each injection or |
Background: Objects in PHP
In PHP, objects of the scope prototype
are created with the new
operator:
$myFreshObject = new \MyCompany\MyPackage\MyClassName();
In contrast to Prototype, the Singleton design pattern ensures that only one instance of a
class exists at a time. In PHP the Singleton pattern is often implemented by providing a
static function (usually called getInstance
), which returns a unique instance of the
class:
/**
* Implementation of the Singleton pattern
*/
class ASingletonClass {
protected static $instance;
static public function getInstance() {
if (!is_object(self::$instance)) {
self::$instance = $this;
}
return self::$instance;
}
}
Although this way of implementing the singleton will possibly not conflict with the Object Manager, it is counterproductive to the integrity of the system and might raise problems with unit testing (sometimes Singleton is referred to as an Anti Pattern). The above examples are not recommended for the use within Flow applications.
The scope of an object is determined from its configuration (see also Configuring objects).
The recommended way to specify the scope is the @scope
annotation:
namespace MyCompany\MyPackage;
use Neos\Flow\Annotations as Flow;
/**
* A sample class
*
* @Flow\Scope("singleton")
*/
class SomeClass {
}
Prototype is the default scope and is therefore assumed if no @scope
annotation or
other configuration was found.
Creating Prototypes
To create prototype objects, just use the new
operator as you are used to:
$myFreshObject = new \MyCompany\MyPackage\MyClassName();
When you do this, some magic is going on behind the scenes which still makes sure the object you get back is managed by the object framework. Thus, all dependencies are properly injected into the object, lifecycle callbacks are fired, and you can use Aspect-Oriented Programming, etc.
Behind the scenes of the Object Framework
In order to provide the functionality that you can just use new
to create new
prototype objects, a lot of advanced things happen behind the scenes.
Flow internally copies all classes to another file, and appends _Original
to their
class name. Then, it creates a new class under the original name where all the magic is
happening.
However, you as a user do not have to deal with that. The only thing you need to remember
is using new
for creating new Prototype objects. And you might know this from PHP ;-)
Retrieving Singletons
The Object Manager maintains a registry of all instantiated singletons and ensures that only one instance of each class exists. The preferred way to retrieve a singleton object is dependency injection.
Example: Retrieving the Object Manager through dependency injection
namespace MyCompany\MyPackage;
/**
* A sample class
*/
class SampleClass {
/**
* @var \Neos\Flow\ObjectManagement\ObjectManagerInterface
*/
protected $objectManager;
/**
* Constructor.
* The Object Manager will automatically be passed (injected) by the object
* framework on instantiating this class.
*
* @param \Neos\Flow\ObjectManagement\ObjectManagerInterface $objectManager
*/
public function __construct(\Neos\Flow\ObjectManagement\ObjectManagerInterface $objectManager) {
$this->objectManager = $objectManager;
}
}
Once the SampleClass
is being instantiated, the object framework will automagically
pass a reference to the Object Manager (which is an object of scope singleton) as an
argument to the constructor. This kind of dependency injection is called
Constructor Injection and will be explained - together with other kinds of injection -
in one of the later sections.
Although dependency injection is what you should strive for, it might happen that you need
to retrieve object instances directly. The ObjectManager
provides methods for
retrieving object instances for these rare situations. First, you need an instance of the
ObjectManager
itself, again by taking advantage of constructor injection:
public function __construct(\Neos\Flow\ObjectManagement\ObjectManagerInterface $objectManager) {
$this->objectManager = $objectManager;
}
Note
In the text, we commonly refer to the ObjectManager
. However, in your code, you should
always use the ObjectManagerInterface
if you need an instance of the Object Manager injected.
To explicitly retrieve an object instance use the get()
method:
$myObjectInstance = $objectManager->get('MyCompany\MyPackage\MyClassName');
It is not possible to pass arguments to the constructor of the object, as the object might
be already instantiated when you call get()
. If the object needs constructor arguments,
these must be configured in Objects.yaml.
Lifecycle methods
The lifecycle of an object goes through different stages. It boils down to the following order:
Solve dependencies for constructor injection
Create an instance of the object class, injecting the constructor dependencies
Solve and inject dependencies for setter injection
Live a happy object-life and solve exciting tasks
Dispose the object instance
Your object might want to take some action after certain of the above steps. Whenever one of the following methods exists in the object class, it will be invoked after the related lifecycle step:
No action after this step
During instantiation the function
__construct()
is called (by PHP itself), dependencies are passed to the constructor argumentsAfter all dependencies have been injected (through constructor- or setter injection) the object’s
initializeObject()
method is called. The name of this method is configurable inside Objects.yaml.initializeObject()
is also called if no dependencies were injected.During the life of an object no special lifecycle methods are called
Before destruction of the object, the function
shutdownObject()
is called. The name of this method is also configurable.On disposal, the function
__destruct()
is called (by PHP itself)
We strongly recommend that you use the shutdownObject
method instead of PHP’s
__destruct
method for shutting down your object. If you used __destruct
it might
happen that important parts of the framework are already unavailable. Here’s a simple
example with all kinds of lifecycle methods:
Example: Sample class with lifecycle methods
class Foo {
protected $bar;
protected $identifier = 'Untitled';
public function __construct() {
echo ('Constructing object ...');
}
public function injectBar(\MyCompany\MyPackage\BarInterface $bar) {
$this->bar = $bar;
}
public function setIdentifier($identifier) {
$this->identifier = $identifier;
}
public function initializeObject() {
echo ('Initializing object ...');
}
public function shutdownObject() {
echo ('Shutting down object ...')
}
public function __destruct() {
echo ('Destructing object ...');
}
}
Output:
Constructing object ...
Initializing object ...
Shutting down object ...
Destructing object ...
Object Registration and API
Object Framework API
The object framework provides a lean API for registering, configuring and retrieving instances of objects. Some of the methods provided are exclusively used within Flow package or in test cases and should possibly not be used elsewhere. By offering Dependency Injection, the object framework helps you to avoid creating rigid interdependencies between objects and allows for writing code which is hardly or even not at all aware of the framework it is working in. Calls to the Object Manager should therefore be the exception.
For a list of available methods please refer to the API documentation of the interface
Neos\Flow\ObjectManagement\ObjectManagerInterface
.
Object Names vs. Class Names
We first need to introduce some namings: A class name is the name of a PHP class, while an object name is an identifier which is used inside the object framework to identify a certain object.
By default, the object name is identical to the PHP class which contains the
object’s code. A class called MyCompany\MyPackage\MyImplementation
will be
automatically available as an object with the exact same name. Every part of the system
which asks for an object with a certain name will therefore - by default - get an instance
of the class of that name.
It is possible to replace the original implementation of an object by another one. In that case the class name of the new implementation will naturally differ from the object name which stays the same at all times. In these cases it is important to be aware of the fine difference between an object name and a class name.
All PHP interfaces for which only one implementation class exist are also automatically registered as object names, with the implementation class being returned when asked for an instance of the interface.
Thus, you can also ask for interface implementations:
$objectTypeInstance = $objectManager->get('MyCompany\MyPackage\MyInterface');
Note
If zero or more than one class implements the interface, the Object Manager will throw an exception.
The advantage of programming against interfaces is the increased flexibility: By referring to interfaces rather than classes it is possible to write code depending on other classes without the need to be specific about the implementation. Which implementation will actually be used can be set at a later point in time by simple means of configuration.
With Flow version 6.2 it’s also possible to use “virtual object names” that don’t represent an interface or class name (see Virtual Objects).
Object Dependencies
The intention to base an application on a combination of packages and objects is to force a clean separation of domains which are realized by dedicated objects. The less each object knows about the internals of another object, the easier it is to modify or replace one of them, which in turn makes the whole system flexible. In a perfect world, each of the objects could be reused in a variety of contexts, for example independently from certain packages and maybe even outside the Flow framework.
Dependency Injection
An important prerequisite for reusable code is already met by encouraging encapsulation through object orientation. However, the objects are still aware of their environment as they need to actively collaborate with other objects and the framework itself: An authentication object will need a logger for logging intrusion attempts and the code of a shop system hopefully consists of more than just one class. Whenever an object refers to another directly, it adds more complexity and removes flexibility by opening new interdependencies. It is very difficult or even impossible to reuse such hardwired classes and testing them becomes a nightmare.
By introducing Dependency Injection, these interdependencies are minimized by inverting the control over resolving the dependencies: Instead of asking for the instance of an object actively, the depending object just gets one injected by the Object Manager. This methodology is also referred to as the “Hollywood Principle”: Don’t call us, we’ll call you. It helps in the development of code with loose coupling and high cohesion — or in short: It makes you a better programmer.
In the context of the previous example it means that the authentication object announces
that it needs a logger which implements a certain PHP interface (for example the
Psr\Log\LoggerInterface
).
The object itself has no control over what kind of logger (file-logger,
sms-logger, …) it finally gets and it doesn’t have to care about it anyway as long as it
matches the expected API. As soon as the authentication object is instantiated, the object
manager will resolve these dependencies, prepare an instance of a logger and
inject it to the authentication object.
Reading Tip
An article by Jonathan Amsterdam discusses the difference between creating an object
and requesting one (i.e. using new
versus using dependency injection). It
demonstrates why new
should be considered as a low-level tool and outlines issues
with polymorphism. He doesn’t mention dependency injection though …
Dependencies on other objects can be declared in the object’s configuration (see Configuring objects) or they can be solved automatically (so called autowiring). Generally there are two modes of dependency injection supported by Flow: Constructor Injection and Setter Injection.
Note
Please note that Flow removes all injected properties before serializing an object. Then after unserializing injections happen again. That means that injected properties are fresh instances and do not keep any state from before the serialization. That hold true also for Prototypes. If you want to keep a Prototype instance with its state throughout a serialize/unserialize cycle you should not inject the Prototype but rather create it in constructor of the object.
Constructor Injection
With constructor injection, the dependencies are passed as constructor arguments to the
depending object while it is instantiated. Here is an example of an object Foo
which
depends on an object Bar
:
Example: A simple example for Constructor Injection
namespace MyCompany\MyPackage;
class Foo {
protected $bar;
public function __construct(\MyCompany\MyPackage\BarInterface $bar) {
$this->bar = $bar;
}
public function doSomething() {
$this->bar->doSomethingElse();
}
}
So far there’s nothing special about this class, the type hint just makes sure that an instance of
a class implementing the \MyCompany\MyPackage\BarInterface
is passed to the constructor.
However, this is already a quite flexible approach because the type of $bar
can be
determined from outside by just passing one or the another implementation to the
constructor.
Now the Flow Object Manager does some magic: By a mechanism called Autowiring all
dependencies which were declared in a constructor will be injected automagically if the
constructor argument provides a type definition (i.e.
\MyCompany\MyPackage\BarInterface
in the above example). Autowiring is activated by
default (but can be switched off), therefore all you have to do is to write your
constructor method.
The object framework can also be configured manually to inject a certain object or object type. You’ll have to do that either if you want to switch off autowiring or want to specify a configuration which differs from would be done automatically.
Example: Objects.yaml file for Constructor Injection
MyCompany\MyPackage\Foo:
arguments:
1:
object: 'MyCompany\MyPackage\Bar'
The three lines above define that an object instance of \MyCompany\MyPackage\Bar
must
be passed to the first argument of the constructor when an instance of the object
MyCompany\MyPackage\Foo
is created.
Setter Injection
With setter injection, the dependencies are passed by calling setter methods of the
depending object right after it has been instantiated. Here is an example of the Foo
class which depends on a Bar
object - this time with setter injection:
Example: A simple example for Setter Injection
namespace MyCompany\MyPackage;
class Foo {
protected $bar;
public function setBar(\MyCompany\MyPackage\BarInterface $bar) {
$this->bar = $bar;
}
public function doSomething() {
$this->bar->doSomethingElse();
}
}
Analog to the constructor injection example, a BarInterface
compatible object is
injected into the Foo
object. In this case, however, the injection only takes
place after the class has been instantiated and a possible constructor method has been
called. The necessary configuration for the above example looks like this:
Example: Objects.yaml file for Setter Injection
MyCompany\MyPackage\Foo:
properties:
bar:
object: 'MyCompany\MyPackage\BarInterface'
Unlike constructor injection, setter injection like in the above example does not offer the autowiring feature. All dependencies have to be declared explicitly in the object configuration.
To save you from writing large configuration files, Flow supports a second
type of setter methods: By convention all methods whose name start with inject
are
considered as setters for setter injection. For those methods no further configuration is
necessary, dependencies will be autowired (if autowiring is not disabled):
Example: The preferred way of Setter Injection, using an inject method
namespace MyCompany\MyPackage;
class Foo {
protected $bar;
public function injectBar(\MyCompany\MyPackage\BarInterface $bar) {
$this->bar = $bar;
}
public function doSomething() {
$this->bar->doSomethingElse();
}
}
Note the new method name injectBar
- for the above example no further configuration is
required. Using inject*
methods is the preferred way for setter
injection in Flow.
Note
If both, a set*
and an inject*
method exist for the same property, the
inject*
method has precedence.
Constructor- or Setter Injection?
The natural question which arises at this point is Should I use constructor- or setter injection? There is no answer across-the-board — it mainly depends on the situation and your preferences. The authors of the Java-based Spring Framework for example prefer Setter Injection for its flexibility. The more puristic developers of PicoContainer strongly plead for using Constructor Injection for its cleaner approach. Reasons speaking in favor of constructor injections are:
Constructor Injection makes a stronger dependency contract
It enforces a determinate state of the depending object: using setter Injection, the injected object is only available after the constructor has been called
However, there might be situations in which constructor injection is not possible or even cumbersome:
If an object has many dependencies and maybe even many optional dependencies, setter injection is a better solution.
Subclasses are not always in control over the arguments passed to the constructor or might even be incapable of overriding the original constructor. Then setter injection is your only chance to get dependencies injected.
Setter injection can be helpful to avoid circular dependencies between objects.
Setters provide more flexibility to unit tests than a fixed set of constructor arguments
Property Injection
Setter injection is the academic, clean way to set dependencies from outside. However, writing these setters can become quite tiresome if all they do is setting the property. For these cases Flow provides support for Property Injection:
Example: Example for Property Injection
namespace MyCompany\MyPackage;
use Neos\Flow\Annotations as Flow;
class Foo {
/**
* An instance of a BarInterface compatible object.
*
* @var \MyCompany\MyPackage\BarInterface
* @Flow\Inject
*/
protected $bar;
public function doSomething() {
$this->bar->doSomethingElse();
}
}
You could say that property injection is the same like setter injection — just without the
setter. The Inject
annotation tells the object framework that the property is
supposed to be injected and the @var
annotation specifies the type. Note that property
injection even works (and should only be used) with protected properties. The Objects.yaml
configuration for property injection is identical to the setter injection configuration.
Note
If a setter method exists for the same property, it has precedence.
Setting properties directly, without a setter method, surely is convenient - but is it clean enough? In general it is a bad idea to allow direct access to mutable properties because you never know if at some point you need to take some action while a property is set. And if thousands of users (or only five) use your API, it’s hard to change your design decision in favor of a setter method.
However, we don’t consider injection methods as part of the public API. As you’ve seen, Flow takes care of all the object dependencies and the only other code working with injection methods directly are unit tests. Therefore we consider it safe to say that you can still switch back from property injection to setter injection without problems if it turns out that you really need it.
Lazy Dependency Injection
Using Property Injection is, in its current implementation, the most performant way
to inject a dependency. As an important additional benefit you also get Lazy
Dependency Injection: instead of loading the class of the dependency, instantiating
and intializing it, a proxy
is injected instead. This object waits until it
will be accessed the first time. Once you start using the dependency, the proxy
will build or retrieve the real dependency, call the requested method and return
the result. On all following method calls, the real object will be used.
By default all dependencies injected through Property Injection are lazy. Usually this process is fully transparent to the user, unless you start passing around dependencies to other objects:
Example: Passing a dependency around
namespace MyCompany\MyPackage;
use Neos\Flow\Annotations as Flow;
class Foo {
/**
* A dependency, injected lazily:
*
* @var \MyCompany\MyPackage\BarInterface
* @Flow\Inject
*/
protected $bar;
...
public function doSomething() {
$this->baz->doSomethingElse($this->bar);
}
}
class Baz {
public function doSomethingElse(Bar $bar) {
...
}
}
The above example will break: at the time you pass $this->bar
to the
doSomethingElse()
method, it is not yet a Bar
object but a
DependencyProxy
object. Because doSomethingElse()
has a type hint requiring
a Bar
object, PHP will issue a fatal error.
There are two ways to solve this:
activating the dependency manually
turning off lazy dependency injection for this property
Example: Manually activating a dependency
namespace MyCompany\MyPackage;
use Neos\Flow\Annotations as Flow;
class Foo {
/**
* A dependency, injected lazily:
*
* @var \MyCompany\MyPackage\BarInterface
* @Flow\Inject
*/
protected $bar;
...
public function doSomething() {
if ($this->bar instanceof \Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy) {
$this->bar->_activateDependency();
}
$this->baz->doSomethingElse($this->bar);
}
}
In the example above, $this->bar
is activated before it is passed to the next
method. It’s important to check if the object still is a proxy because otherwise
calling _activateDependency()
will fail.
Example: Turning off lazy dependency injection
namespace MyCompany\MyPackage;
use Neos\Flow\Annotations as Flow;
class Foo {
/**
* A dependency, injected eagerly
*
* @var \MyCompany\MyPackage\BarInterface
* @Flow\Inject(lazy = false)
*/
protected $bar;
...
public function doSomething() {
$this->baz->doSomethingElse($this->bar);
}
}
In the second solution, lazy dependency injection is turned off. This way you can
be sure that $this->bar
always contains the object you expected, but you don’t
benefit from the speed optimizations.
Settings Injection
No, this headline is not misspelled. Flow offers some convenient feature which allows for
automagically injecting the settings of the current package without the need to configure
the injection. If a class contains a method called injectSettings
and autowiring is
not disabled for that object, the Object Builder will retrieve the settings of the package
the object belongs to and pass it to the injectSettings
method.
Example: the magic injectSettings method
namespace MyCompany\MyPackage;
class Foo {
protected $settings = array();
public function injectSettings(array $settings) {
$this->settings = $settings;
}
public function doSomething() {
var_dump($this->settings);
}
}
The doSomething
method will output the settings of the MyPackage
package.
In case you only need a specific setting, there’s an even more convenient way to inject a single setting value into a class property:
namespace Acme\Demo;
use Neos\Flow\Annotations as Flow;
class SomeClass {
/**
* @var string
* @Flow\InjectConfiguration("administrator.name")
*/
protected $name;
/**
* @var string
* @Flow\InjectConfiguration(path="email", package="SomeOther.Package")
*/
protected $emailAddress;
}
The InjectConfiguration
annotation also supports for injecting all settings of a package. And it can also be used
to inject any other registered configuration type:
namespace Acme\Demo;
class SomeClass {
/**
* @var array
* @Flow\InjectConfiguration(package="SomeOther.Package")
*/
protected $allSettingsOfSomeOtherPackage;
/**
* @var array
* @Flow\InjectConfiguration(type="Views")
*/
protected $viewsConfiguration;
}
Required Dependencies
All dependencies defined in a constructor are, by its nature, required. If a dependency can’t be solved by autowiring or by configuration, Flow’s object builder will throw an exception.
Also autowired setter-injected dependencies are, by default, required. If the object builder can’t autowire an object for an injection method, it will throw an exception.
Dependency Resolution
The dependencies between objects are only resolved during the instantiation process. Whenever a new instance of an object class needs to be created, the object configuration is checked for possible dependencies. If there is any, the required objects are built and only if all dependencies could be resolved, the object class is finally instantiated and the dependency injection takes place.
During the resolution of dependencies it might happen that circular dependencies occur. If
an object A
requires an object B
to be injected to its constructor and then again object B
requires an object A
likewise passed as a constructor argument, none of the two classes can
be instantiated due to the mutual dependency. Although it is technically possible (albeit
quite complex) to solve this type of reference, Flow’s policy is not to allow circular
constructor dependencies at all. As a workaround you can use setter injection instead
for either one or both of the objects causing the trouble.
Configuring objects
The behavior of objects significantly depends on their configuration. During the initialization process all classes found in the various Classes/ directories are registered as objects and an initial configuration is prepared. In a second step, other configuration sources are queried for additional configuration options. Definitions found at these sources are added to the base configuration in the following order:
If they exist, the <PackageName>/Configuration/Objects.yaml will be included.
Additional configuration defined in the global Configuration/Objects.yaml directory is applied.
Additional configuration defined in the global Configuration/<ApplicationScope>/Objects.yaml directory is applied.
Currently there are three important situations in which you want to configure objects:
Override one object implementation with another
Set the active implementation for an object type
Explicitly define and configure dependencies to other objects
Configuring Objects Through Objects.yaml
If a file named Objects.yaml exists in the Configuration directory of a package, it will be included during the configuration process. The YAML file should stick to Flow’s general rules for YAML-based configuration.
Example: Sample Objects.yaml file
# #
# Object Configuration for the MyPackage package #
# #
# @package MyPackage
MyCompany\MyPackage\Foo:
arguments:
1:
object: 'MyCompany\MyPackage\Baz'
2:
value: "some string"
3:
value: false
properties:
bar:
object: 'MyCompany\MyPackage\BarInterface'
enableCache:
setting: MyPackage.Cache.enable
Configuring Objects Through Annotations
A very convenient way to configure certain aspects of objects are annotations. You write
down the configuration directly where it takes effect: in the class file. However, this
way of configuring objects is not really flexible, as it is hard coded. That’s why only
those options can be set through annotations which are part of the class design and won’t
change afterwards. Currently scope
, inject
and autowiring
are the only
supported annotations.
It’s up to you defining the scope in the class directly or doing it in a Objects.yaml configuration file – both have the same effect. We recommend using annotations in this case, as the scope usually is a design decision which is very unlikely to be changed.
Example: Sample scope annotation
/**
* This is my great class.
*
* @Flow\Scope("singleton")
*/
class SomeClass {
}
Example: Sample autowiring annotation for a class
/**
* This turns off autowiring for the whole class:
*
* @Flow\Autowiring(false)
*/
class SomeClass {
}
Example: Sample autowiring annotation for a method
/**
* This turns off autowiring for a single method:
*
* @param \Neos\Foo\Bar $bar
* @Flow\Autowiring(false)
*/
public function injectMySpecialDependency(\Neos\Foo\Bar $bar) {
}
Overriding Object Implementations
One advantage of componentry is the ability to replace objects by others without any bad impact on those parts depending on them.
A prerequisite for replaceable objects is that their classes implement a common interface which defines the public API of the original object. Other objects which implement the same interface can then act as a true replacement for the original object without the need to change code anywhere in the system. If this requirement is met, the only necessary step to replace the original implementation with a substitute is to alter the object configuration and set the class name to the new implementation.
To illustrate this circumstance, consider the following classes.
Example: The Greeter object type
namespace MyCompany\MyPackage;
interface GreeterInterface {
public function sayHelloTo($name);
}
class Greeter implements GreeterInterface {
public function sayHelloTo($name) {
echo 'Hello ' . $name;
}
}
During initialization the above Greeter
class will automatically be
registered as the default implementation of
MyCompany\MyPackage\GreeterInterface
and is available to other objects. In
the class code of another object you might find the following lines.
Example: Using the Greeter object type
// Use setter injection for fetching an instance
// of \MyCompany\MyPackage\GreeterInterface:
public function injectGreeter(\MyCompany\MyPackage\GreeterInterface $greeter) {
$this->greeter = $greeter;
}
public function someAction() {
$this->greeter->sayHelloTo('Heike');
}
If we want to use the much better object
\Neos\OtherPackage\GreeterWithCompliments
, the solution is to let the new
implementation implement the same interface.
Example: The improved Greeter object type
namespace Neos\OtherPackage;
class GreeterWithCompliments implements \MyCompany\MyPackage\GreeterInterface {
public function sayHelloTo($name) {
echo('Hello ' . $name . '! You look so great!');
}
}
Then we have to set which implementation of the MyCompany\MyPackage\GreeterInterface
should be active and are done:
Example: Objects.yaml file for object type definition
MyCompany\MyPackage\GreeterInterface:
className: 'Neos\OtherPackage\GreeterWithCompliments'
The the same code as above will get the improved GreeterWithCompliments
instead of the simple Greeter
now.
Configuring Injection
The object framework allows for injection of straight values, objects (i.e. dependencies) or settings either by passing them as constructor arguments during instantiation of the object class or by calling a setter method which sets the wished property accordingly. The necessary configuration for injecting objects is usually generated automatically by the autowiring capabilities of the Object Builder. Injection of straight values or settings, however, requires some explicit configuration.
Injection Values
Regardless of what injection type is used (constructor or setter injection), there are three kinds of value which can be injected:
value: static value of a simple type. Can be string, integer, boolean or array and is passed on as is.
object: object name which represents a dependency. Dependencies of the injected object are resolved and an instance of the object is passed along.
setting: setting defined in one of the Settings.yaml files. A path separated by dots specifies which setting to inject.
Constructor Injection
Arguments for constructor injection are defined through the arguments option. Each argument is identified by its position, counting starts with 1.
Example: Sample class for Constructor Injection
namespace MyCompany\MyPackage;
class Foo {
protected $bar;
protected $identifier;
protected $enableCache;
public function __construct(\MyCompany\MyPackage\BarInterface $bar, $identifier,
$enableCache) {
$this->bar = $bar;
$this->identifier = $identifier;
$this->enableCache = $enableCache;
}
public function doSomething() {
$this->bar->doSomethingElse();
}
}
Example: Sample configuration for Constructor Injection
MyCompany\MyPackage\Foo:
arguments:
1:
object: 'MyCompany\MyPackage\Bar'
2:
value: "some string"
3:
setting: "MyPackage.Cache.enable"
Note
It is usually not necessary to configure injection of objects explicitly. It is much
more convenient to just declare the type of the constructor arguments (like
MyCompany\MyPackage\BarInterface
in the above example) and let the autowiring
feature configure and resolve the dependencies for you.
Setter Injection
The following class and the related Objects.yaml file demonstrate the syntax for the definition of setter injection:
Example: Sample class for Setter Injection
namespace MyCompany\MyPackage;
class Foo {
protected $bar;
protected $identifier = 'Untitled';
protected $enableCache = false;
public function injectBar(\MyCompany\MyPackage\BarInterface $bar) {
$this->bar = $bar;
}
public function setIdentifier($identifier) {
$this->identifier = $identifier;
}
public function setEnableCache($enableCache) {
$this->enableCache = $enableCache;
}
public function doSomething() {
$this->bar->doSomethingElse();
}
}
Example: Sample configuration for Setter Injection
MyCompany\MyPackage\Foo:
properties:
bar:
object: 'MyCompany\MyPackage\Bar'
identifier:
value: 'some string'
enableCache:
setting: 'MyPackage.Cache.enable'
As you can see, it is important that a setter method with the same name as the property,
preceded by inject
or set
exists. It doesn’t matter though, if you choose inject
or
set
, except that inject
has the advantage of being autowireable. As a rule of thumb we
recommend using inject
for required dependencies and values and set
for optional
properties.
Injection of Objects Specified in Settings
In some cases it might be convenient to specify the name of the object to be injected in the settings rather than in the objects configuration. This can be achieved by specifying the settings path instead of the object name:
Example: Injecting an object specified in the settings
MyCompany\MyPackage\Foo:
properties:
bar:
object: 'MyCompany.MyPackage.fooStuff.barImplementation'
Example: Settings.yaml of MyPackage
MyCompany:
MyPackage:
fooStuff:
barImplementation: 'MyCompany\MyPackage\Bars\ASpecialBar'
Nested Object Configuration
While autowiring and automatic dependency injection offers a great deal of convenience, it is sometimes necessary to have a fine grained control over which objects are injected with which third objects injected.
Consider a Flow cache object, a VariableCache
for example: the cache itself depends
on a cache backend which on its part requires a few settings passed to its constructor -
this readily prepared cache should now be injected into another object. Sounds complex?
With the objects configuration it is however possible to configure even that nested object
structure:
Example: Nesting object configuration
MyCompany\MyPackage\Controller\StandardController:
properties:
cache:
object:
name: 'Neos\Cache\VariableCache'
arguments:
1:
value: MyCache
2:
object:
name: 'Neos\Cache\Backend\File'
properties:
cacheDirectory:
value: /tmp/
Disabling Autowiring
Injecting dependencies is a common task. Because Flow can detect the type of dependencies
a constructor needs, it automatically configures the object to ensure that the necessary
objects are injected. This automation is called autowiring and is enabled by default for
every object. As long as autowiring is in effect, the Object Builder will try to autowire
all constructor arguments and all methods named after the pattern inject*
.
If, for some reason, autowiring is not wanted, it can be disabled by setting an option in the object configuration:
Example: Turning off autowiring support in Objects.yaml
MyCompany\MyPackage\MyObject:
autowiring: false
Autowiring can also be switched off through the @Flow\Autowiring(false)
annotation - either
in the documentation block of a whole class or of a single method. For the latter the
annotation only has an effect when used in comment blocks of a constructor or of a method
whose name starts with inject
.
Custom Factories
Complex objects might require a custom factory which takes care of all important settings
and dependencies. As we have seen previously, a logger consists of a frontend, a backend
and configuration options for that backend. Instead of creating and configuring these
objects on your own, you should use the Neos\Flow\Log\PsrLoggerFactory
which provides a
convenient get
method taking care of all the rest:
$myCache = $loggerFactory->get('systemLogger');
It is possible to specify for each object if it should be created by a custom factory rather than the Object Builder. Consider the following configuration:
Example: Sample configuration for a Custom Factory
Neos\Flow\Log\PsrSystemLoggerInterface:
scope: singleton
factoryObjectName: Neos\Flow\Log\PsrLoggerFactory
factoryMethodName: get
From now on the LoggerFactory’s get
method will be called each time an object of
type PsrSystemLoggerInterface
needs to be instantiated. If arguments were passed to the
ObjectManagerInterface::get()
method or defined in the configuration, they will be
passed through to the custom factory method:
Example: YAML configuration for a Custom Factory with default arguments
Neos\Flow\Log\PsrSystemLoggerInterface:
scope: singleton
factoryObjectName: Neos\Flow\Log\PsrLoggerFactory
factoryMethodName: get
arguments:
1:
value: 'systemLogger'
Example: YAML configuration for a static custom factory method
Acme\Foo\Object:
scope: prototype
factoryMethodName: Acme\Foo\ObjectFactory::fromValue
arguments:
1:
settings: 'Acme.Foo.Object.ConfigurableValue'
Note that if you only specify the factoryMethodName, it needs to be the fully qualified name.
Example: PHP code using the custom factory
$myCache = $objectManager->get(\Neos\Flow\Log\PsrSystemLoggerInterface::class);
$objectManager
is a reference to the Neos\Flow\ObjectManagement\ObjectManager
.
The required arguments are automatically built from the values defined in the
object configuration.
Name of Lifecycle Methods
The default name of a lifecycle methods is initializeObject
and shutdownObject
.
If these methods exist, the initialization method will be called after the object has been
instantiated or recreated and all dependencies are injected and the shutdown method is
called before the Object Manager quits its service.
As the initialization method is being called after creating an object and after
recreating/reconstituting an object, there are cases where different code should be
executed. That is why the initialization method gets a parameter, which is one of the
\Neos\Flow\ObjectManagement\ObjectManagerInterface::INITIALIZATIONCAUSE_*
constants:
\Neos\Flow\ObjectManagement\ObjectManagerInterface::INITIALIZATIONCAUSE_CREATED
If the object is newly created (i.e. the constructor has been called)
\Neos\Flow\ObjectManagement\ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED
If the object has been recreated/reconstituted (i.e. the constructor has not been called)
The name of both methods is configurable per object for situations you don’t have control over the name of your initialization method (maybe, because you are integrating legacy code):
Example: Objects.yaml configuration of the initialization and shutdown method
MyCompany\MyPackage\MyObject:
lifecycleInitializationMethod: myInitializeMethodName
lifecycleShutdownMethod: myShutdownMethodName
Static Method Result Compilation
Some part of a Flow application may rely on data which is static during runtime, but which cannot or should not be hardcoded.
One example is the validation rules generated by the MVC framework for arguments of a controller action: the base information (PHP methods for the actions, type hints and arguments of these methods) is static. However, the validation rules should be determined automatically by the framework instead of being configured or hardcoded elsewhere. On the other hand, generating validation rules during runtime unnecessarily slows down the application. The solution is static method result compilation.
A method which generates data based on information already known at compile time can usually be made static. Consider the following example:
/**
* Returns a map of action method names and their parameters.
*
* @return array Array of method parameters by action name
*/
public function getActionMethodParameters() {
$methodParameters = $this->reflectionService->getMethodParameters(get_class($this), $this->actionMethodName);
foreach ($methodParameters as $parameterName => $parameterInfo) {
...
}
return $methodParameters;
}
In the example above, getActionMethodParameters()
returns data needed during
runtime which could easily be pre-compiled.
By annotating the method with @Flow\CompileStatic
and transforming it into a
static method which does not depend on runtime services like persistence, security
and so on, the performance in production context can be improved:
/**
* Returns a map of action method names and their parameters.
*
* @param \Neos\Flow\ObjectManagement\ObjectManagerInterface $objectManager
* @return array Array of method parameters by action name
* @Flow\CompileStatic
*/
static protected function getActionMethodParameters($objectManager) {
$reflectionService = $objectManager->get(\Neos\Flow\Reflection\ReflectionService::class);
$className = get_called_class();
$methodParameters = $reflectionService->getMethodParameters($className, get_class_methods($className));
foreach ($methodParameters as $parameterName => $parameterInfo) {
...
}
return $methodParameters;
}
The results of methods annotated with CompileStatic
will only be compile in
Production
context. When Flow is started in a different context, the method
will be executed during each run.
Enabling Other Package Classes For Object Management
As stated in the beginning of this part, all classes in packages not in one of the neos-*
types is not recognized for object management by default. If you still want that you can include
those classes via configuration in settings. The configuration consists of a map of package keys to
arrays of expressions which match classes to be included. In the following example we include all
classes of the Acme.Objects
package:
Neos:
Flow:
object:
includeClasses:
'Acme.Objects' : ['.*']
Note
If you use the includeClasses
setting on a flow package (which is already enabled for object
management) then only the classes that match at least one of the filter expressions are going to
be object managed. This can also be used to remove classes inside flow packages from object
management by specifying a non-matching expression or an empty array.
Note
The static method must except exactly one argument which is the Flow
Object Manager. You cannot use a type hint at this point (for the $objectManager
argument) because the argument passed could actually be a DependencyProxy
and
not the real ObjectManager. Please refer to the section about Lazy Dependency
Injection for more information about DependencyProxy
.
Virtual Objects
With the Objects.yaml
configuration, the default behavior of classes can be changed globally. Sometimes
it can be useful to configure the same class for multiple different use cases. For example two logger implementations
that use the same instance but are configured separately.
With Flow version 6.2 and above it’s possible to configure “Virtual Objects”. The syntax is the same as described above (see Configuring objects) with two differences:
The object name has to contain a colon (to tell it apart from regular object names)
The
className
configuration is required (since it can’t be inferred from the object name)
Example: Objects.yaml for two virtual logger objects
'Some.Package:SystemLogger':
className: Psr\Log\LoggerInterface
scope: singleton
factoryObjectName: Neos\Flow\Log\PsrLoggerFactoryInterface
factoryMethodName: get
arguments:
1:
value: systemLogger
'Some.Package:SecurityLogger':
className: Psr\Log\LoggerInterface
scope: singleton
factoryObjectName: Neos\Flow\Log\PsrLoggerFactoryInterface
factoryMethodName: get
arguments:
1:
value: securityLogger
With those objects configured, the respective loggers can be instantiated via:
$systemLogger = $objectManager->get('Some.Package:SystemLogger');
$securityLogger = $objectManager->get('Some.Package:SecurityLogger');
Injecting Virtual Objects
To inject a Virtual Object you can simply use the name
property of the Inject
annotation
to refer to the Virtual Object name:
Example: SomeClass.php
class SomeClass {
/**
* @Flow\Inject(name="Some.Package:SystemLogger")
* @var LoggerInterface
*/
protected $systemLogger;
Alternatively you can use constructor- or setter injection with a corresponding configuration:
Example: Objects.yaml
# ...
'Some\Package\SomeClass':
properties:
'systemLogger':
object: 'Some.Package:SystemLogger'
Example: SomeClass.php
class SomeClass {
public function injectSystemLogger(LoggerInterface $systemLogger): void
{
$this->systemLogger = $systemLogger;
}