Git Product home page Git Product logo

nette / di Goto Github PK

View Code? Open in Web Editor NEW
852.0 40.0 69.0 2.27 MB

💎 Flexible, compiled and full-featured Dependency Injection Container with perfectly usable autowiring and support for all new PHP 7 features.

Home Page: https://doc.nette.org/dependency-injection

License: Other

PHP 99.18% HTML 0.82%
nette nette-framework dependency-injection php dependency-injection-container di-container factories

di's Introduction

Nette Dependency Injection (DI)

Downloads this Month Tests Coverage Status Latest Stable Version License

Introduction

Purpose of the Dependecy Injection (DI) is to free classes from the responsibility for obtaining objects that they need for its operation (these objects are called services). To pass them these services on their instantiation instead.

Nette DI is one of the most interesting part of framework. It is compiled DI container, extremely fast and easy to configure.

Documentation can be found on the website.

Do you like Nette DI? Are you looking forward to the new features?

Buy me a coffee

Thank you!

Installation

The recommended way to install is via Composer:

composer require nette/di

It requires PHP version 8.1 and supports PHP up to 8.3.

Usage

Let's have an application for sending newsletters. The code is maximally simplified and is available on the GitHub.

We have the object representing email:

class Mail
{
	public string $subject;
	public string $message;
}

An object which can send emails:

interface Mailer
{
	function send(Mail $mail, string $to): void;
}

A support for logging:

interface Logger
{
	function log(string $message): void;
}

And finally, a class that provides sending newsletters:

class NewsletterManager
{
	private Mailer $mailer;
	private Logger $logger;

	public function __construct(Mailer $mailer, Logger $logger)
	{
		$this->mailer = $mailer;
		$this->logger = $logger;
	}

	public function distribute(array $recipients): void
	{
		$mail = new Mail;
		$mail->subject = '...';
		$mail->message = '...';

		foreach ($recipients as $recipient) {
			$this->mailer->send($mail, $recipient);
		}
		$this->logger->log('...');
	}
}

The code respects Dependency Injection, ie. each object uses only variables which we had passed into it.

Also, we have a ability to implement own Logger or Mailer, like this:

class SendMailMailer implements Mailer
{
	public function send(Mail $mail, string $to): void
	{
		mail($to, $mail->subject, $mail->message);
	}
}

class FileLogger implements Logger
{
	private string $file;

	public function __construct(string $file)
	{
		$this->file = $file;
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

DI container is the supreme architect which can create individual objects (in the terminology DI called services) and assemble and configure them exactly according to our needs.

Container for our application might look like this:

class Container
{
	private ?Logger $logger;
	private ?Mailer $mailer;

	public function getLogger(): Logger
	{
		if (!isset($this->logger)) {
			$this->logger = new FileLogger('log.txt');
		}
		return $this->logger;
	}

	public function getMailer(): Mailer
	{
		if (!isset($this->mailer)) {
			$this->mailer = new SendMailMailer;
		}
		return $this->mailer;
	}

	public function createNewsletterManager(): NewsletterManager
	{
		return new NewsletterManager($this->getMailer(), $this->getLogger());
	}
}

The implementation looks like this because:

  • the individual services are created only on demand (lazy loading)
  • doubly called createNewsletterManager will use the same logger and mailer instances

Let's instantiate Container, let it create manager and we can start spamming users with newsletters :-)

$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);

Significant to Dependency Injection is that no class depends on the container. Thus it can be easily replaced with another one. For example with the container generated by Nette DI.

Nette DI

Nette DI is the generator of containers. We instruct it (usually) with configuration files. This is configuration that leads to generate nearly the same class as the class Container above:

services:
	- FileLogger( log.txt )
	- SendMailMailer
	- NewsletterManager

The big advantage is the shortness of configuration.

Nette DI actually generates PHP code of container. Therefore it is extremely fast. Developer can see the code, so he knows exactly what it is doing. He can even trace it.

Usage of Nette DI is very easy. Save the (above) configuration to the file config.neon and let's create a container:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
    $compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

and then use container to create object NewsletterManager and we can send e-mails:

$manager = $container->getByType(NewsletterManager::class);
$manager->distribute(['[email protected]', ...]);

The container will be generated only once and the code is stored in cache (in directory __DIR__ . '/temp'). Therefore the loading of configuration file is placed in the closure in $loader->load(), so it is called only once.

During development it is useful to activate auto-refresh mode which automatically regenerate the container when any class or configuration file is changed. Just in the constructor ContainerLoader append true as the second argument:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', autoRebuild: true);

Services

Services are registered in the DI container and their dependencies are automatically passed.

services:
	manager: NewsletterManager

All dependencies declared in the constructor of this service will be automatically passed. Constructor passing is the preferred way of dependency injection for services.

If we want to pass dependencies by the setter, we can add the setup section to the service definition:

services:
	manager:
		factory: NewsletterManager
		setup:
			- setAnotherService

Class of the service:

class NewsletterManager
{
	private AnotherService $anotherService;

	public function setAnotherService(AnotherService $service): void
	{
		$this->anotherService = $service;
	}

...

We can also add the inject: yes directive. This directive will enable automatic call of inject* methods and passing dependencies to public variables with #[Inject] attribute:

services:
	foo:
		factory: FooClass
		inject: yes

Dependency Service1 will be passed by calling the inject* method, dependency Service2 will be assigned to the $service2 variable:

use Nette\DI\Attributes\Inject;

class FooClass
{
	private Service1 $service1;

	// 1) inject* method:

	public function injectService1(Service1 $service): void
	{
		$this->service1 = $service1;
	}

	// 2) Assign to the variable with the #[Inject] attribute:

	#[Inject]
	public Service2 $service2;
}

However, this method is not ideal, because the variable must be declared as public and there is no way how you can ensure that the passed object will be of the given type. We also lose the ability to handle the assigned dependency in our code and we violate the principles of encapsulation.

Factories

We can use factories generated from an interface. The interface must declare the returning type of the method. Nette will generate a proper implementation of the interface.

The interface must have exactly one method named create. Our factory interface could be declared in the following way:

interface BarFactory
{
	function create(): Bar;
}

The create method will instantiate an Bar with the following definition:

class Bar
{
	private Logger $logger;

	public function __construct(Logger $logger)
	{
		$this->logger = $logger;
	}
}

The factory will be registered in the config.neon file:

services:
	- BarFactory

Nette will check if the declared service is an interface. If yes, it will also generate the corresponding implementation of the factory. The definition can be also written in a more verbose form:

services:
	barFactory:
		implement: BarFactory

This full definition allows us to declare additional configuration of the object using the arguments and setup sections, similarly as for all other services.

In our code, we only have to obtain the factory instance and call the create method:

class Foo
{
	private BarFactory $barFactory;

	function __construct(BarFactory $barFactory)
	{
		$this->barFactory = $barFactory;
	}

	function bar(): void
	{
		$bar = $this->barFactory->create();
	}
}

di's People

Contributors

adaamz avatar dg avatar enumag avatar f3l1x avatar fprochazka avatar greeny avatar h4kuna avatar hrach avatar janbarasek avatar jantvrdik avatar kravco avatar kukulich avatar lookyman avatar majkl578 avatar matej21 avatar milanpala avatar milo avatar ondrahb avatar ondrejmirtes avatar pavelkouril avatar pmachan avatar rixafy avatar sallyx avatar tomasvotruba avatar tomaswindsor avatar uestla avatar vasekpurchart avatar vrana avatar vrtak-cz avatar xificurk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

di's Issues

Changed service causes dependency checks on every request

There is a problem with current DIC expiration check.

Cosider following config:

services:
- MyService

Let's say I change content of MyService.php from

<?php
class MyService {}

to

<?php
class MyService
{
    public function foo();
}

(file changed, dependencies didn't)

On next request DependencyChecker compares modification times with meta file of generated container, which differs. Dependency hash is now calculated and matches the one in meta file, so container is considered non-expired.

The problem is that meta file is not updated and hash is calculated on every request, which leads to performance overhead for large containers.

BC Break in ServiceDefinition (SmartObject)

Introduced in 69b056e, related #106

It would be nice if somebody could fix this BC break - since this is pretty hard deprecation (while there is no reason for it) and it would be nice to have some transition time with soft-deprecation.
Yaay for removing magic, booo for BC Breaks :)

I may do it myself, but I would preffer if somebody else did it - since I have very little free time right now
Thanks.

Support for ENV variables

Syntax could leverage support for arrays a: %env.NAME% with all variables in %env%.

Things to consider:

  • Syntax for default values.
  • Whitelist by default.
  • Invalidate cache if used ENV has changed.
  • Interface to access them in extensions. (to complement config and add requested ENVs to whitelist)

I have created extension to add partial support for ENV variables. It has whitelist and default values and partial support for configs. Cache invalidation is not implemented

Rough start

Hi guys,

I'm having a rough start with nette/di. I'm trying to replace the PHP parser of ApiGen by nikic's and I'm already stuck with nette/di service definition:

https://github.com/ApiGen/ApiGen/blob/master/src/Parser/DI/ParserExtension.php#L28

I would expect to be able to define the service as follows:

$builder->addDefinition($this->prefix('broker'))
            ->setFactory(function() {
                return (new ParserFactory)->create(ParserFactory::PREFER_PHP7)
            });

Alas, this throws a InvalidArgumentException. I read the following docs, but found nothing involving closures for addDefinition, its often a class name and some arguments:

Please help :'(

why di config/loader parsed result is not compatible to neon format?

I am improved the config class with neon, now it can be write like this:

services:
    - App\Play\Provider
    - App\Play\Sender
    - App\Play\Logger(dev.log)

prod < services:
    - App\Play\Logger(prod.log)

test < services:
    - App\Play\Logger(test.log)

it can merge config in every env, I'd like it :-P

but when I try to use addConfig() method to set di config to compiler, it was error, because Nette\Neon\Entity is not Nette\DI\Statement

could you make they to be same class? or compatible neon entity in Validator?

Better exception with typo in service definition.

- 
    class: \MyClass
    autowire: false

produces: Unknown or deprecated key 'autowire' in definition of service.

Would be great to directly say if it's deprecated, or something like "did you mean" :-)

Multiple services from generated factory

Is it possible to generate multiple services from a single generated factory?

I've got several similar services with additional dependencies given at runtime (Strategy pattern) and I'd like to avoid writing my own factory class and repeat myself so many times when changing a single dependency as well as i'd like to avoid writing multiple single generated factories (I'm talking about ten up to twenty services / strategies, that means up to 20 factories).

Is this possible? Is it a good idea or not? What do you think?

DI\ContainerBuilder [2.3.13] - problem with php-generator

  • bug report? yes
  • feature request? no
  • version: 2.3.13

Description

I'm getting error in nette/di:2.3.13 container generation.

Steps To Reproduce

Composer: nette/di: ~2.3.0

Exception:

   ArgumentCountError: Too few arguments to function Nette\PhpGenerator\Parameter::__construct(), 0 passed in "vendor/nette/di/src/DI/ContainerBuilder.php" on line 672 and exactly 1 expected

Stacktrace:

   in PhpGenerator/Traits/NameAware.php(25) 
   in src/DI/ContainerBuilder.php(672) Nette\PhpGenerator\Parameter->__construct()
   in src/DI/ContainerBuilder.php(580) Nette\DI\ContainerBuilder->convertParameters()
   in src/DI/Compiler.php(231) Nette\DI\ContainerBuilder->generateClasses()
   in src/DI/Compiler.php(152) Nette\DI\Compiler->generateCode()
   in src/DI/ContainerLoader.php(115) Nette\DI\Compiler->compile()
   in src/DI/ContainerLoader.php(79) Nette\DI\ContainerLoader->generate()
   in src/DI/ContainerLoader.php(44) Nette\DI\ContainerLoader->loadFile()
   in tests/cases/Compare.phpt(37) Nette\DI\ContainerLoader->load()

Broken creation of services that have optional array constructor dependency

if you have a class with constructor like this

public function __construct(array $server = array(), History $history = null, CookieJar $cookieJar = null)
{
    $this->setServerParameters($server);
    $this->history = $history ?: new History();
    $this->cookieJar = $cookieJar ?: new CookieJar();
}

and config.neon

- Goutte\Client

this exception is raised exception 'Nette\DI\ServiceCreationException' with message 'Class array needed by Symfony\Component\BrowserKit\Client::__construct() not found. Check type hint and 'use' statements.'

this class is just an example, it happens with all array typehints. it started to happen in recent versions of nette

before it worked just fine

Decorator && generated factories

DecoratorExtension cannot handle types returned by generated factories.
e.g.

services:
    - {implement: FooFactoryCreatingFooControlExtendingBaseControl}
decorator:
    BaseControl:
        inject: true

Overriding service definition with factory always requires return annotation

After migrating to 2.3, following behaviour changed (and I don't know if it is intentional, so I'm reporting it):

# config.neon:
services:
    someService:
        class: Some\Class

# config.local.neon:
services:
    someService:
        factory: SomeFactory::create # doesn't have @return phpdoc

Newly in 2.3 it raises User Notice: Type of service 'someService' is unknown. because it requires the @return phpdoc annotation in the factory. This worked without it in 2.2.

Calling removeDefinition not affect list of classes

class MyExtension extends CompilerExtension
{
    function loadConfiguration()
    {
        $builder->addDefinition($this->prefix('myClass'))
            ->setFactory('My\Class');
        $builder->removeDefinition($this->prefix('myClass'));
        $builder->findByType('My\Class');
    }
}

This returning unexpected notice E_NOTICE: Undefined index: my.myClass when calling $builder->findByType('My\Class') in ContainerBuilder on line (220).

mysqli creation requires to specify all arguments

After migrating to version 2.3 I'm getting error Parameter $port in mysqli::mysqli() has no type hint, so its value must be specified when creating mysqli (it has default values taken from ini settings) like this:

    - mysqli(
        %database.host%,
        %database.username%,
        %database.password%,
        %database.database%
    )

This definitly worked on 2.2. To make it work as before, I must use:

    - mysqli(
        %database.host%,
        %database.username%,
        %database.password%,
        %database.database%,
        ::ini_get(mysqli.default_port),
        ::ini_get(mysqli.default_socket)
    )

Inheriting service from extension is not working

When I want to inherite some service from added extension (myService < extensionName.service:) I get notice Undefined index: extensionName.service. The notice origin from Compiler.php:172. I was able to figure out that problem is either in Compiler.php:processServices method (wrong parameters passed to parseServices method) or in Compiler.php: parseServices method where services and factories are merged.

I'm using the latest dev of Nette.

ContainerLoader - implode(): Invalid arguments passed

PHP: 7.0
nette/di: 2.4.0


This snippet of code triggered error.

    $loader = new ContainerLoader(TEMP_DIR, TRUE);
    $class = $loader->load(function (Compiler $compiler) {
        $compiler->addExtension('a', new FoobarExtension);
    }, 'foobar');
   Exited with error code 255 (expected 0)
   E_WARNING: implode(): Invalid arguments passed

   in src/DI/ContainerLoader.php(119) 
   in src/DI/ContainerLoader.php(119) implode()
   in src/DI/ContainerLoader.php(82) Nette\DI\ContainerLoader->generate()
   in src/DI/ContainerLoader.php(47) Nette\DI\ContainerLoader->loadFile()

I think that problem might be here (double implode).

https://github.com/nette/di/blob/v2.4.0/src/DI/Compiler.php#L161
https://github.com/nette/di/blob/v2.4.0/src/DI/ContainerLoader.php#L119


Is it wrong usage or it is really bug? :-)

Possibility to register core DIC extension before other extensions

  • bug report? no
  • feature request? yes
  • version: 3.0.0

Description

In v3.0.0-alpha1 there is final class ExtensionsExtension which is huge BC break for extensions replacing ExtensionsExtension. This cannot be easy workarounded. I tried to solve this issue in PR #142 but I understand that it's just another workaround and proper solution is needed.

@dg talked about prioritization of DIC extensions. I don't have idea what are the plans here but this should be (I wish) solved before stable release otherwise it's not possible to upgade uprojects with custom ExtensionsExtension.

Example of extension relying on this feature: adeira/compiler-extension

Thank you very much.

Broken InjectExtension with removed definition

#71 #72 broke InjectExtension. When another extension override definition with own definition, it removes class from $classes, but ->addDefinition does not add new.

certain description (see link):

  1. $classes is prepared by https://github.com/nette/di/blob/master/src/DI/Compiler.php#L202
  2. OrmExtension override definition https://github.com/Kdyby/Doctrine/blob/master/src/Kdyby/Doctrine/DI/OrmExtension.php#L741 (beforeCompile) - remove $classes https://github.com/nette/di/blob/master/src/DI/ContainerBuilder.php#L81
  3. Next extension (InjectExtension) want to service by class in beforeCompile https://github.com/nette/di/blob/master/src/DI/Extensions/InjectExtension.php#L126 (btw. see second param which missing)
  4. there is null https://github.com/nette/di/blob/master/src/DI/ContainerBuilder.php#L210

testcase:

$builder = new DI\ContainerBuilder;

$builder->addDefinition('one')
    ->setClass('stdClass');

$builder->prepareClassList();

$builder->removeDefinition('one');
$builder->addDefinition('one')
    ->setClass('stdClass');

Assert::count(1, $builder->findByType('stdClass')); // Failed: Count 0 should be 1
Assert::same('one', $builder->getByType('stdClass')); // Failed: NULL should be 'one'

Return annotation containing $this confuses ContainerBuilder

I have a project that is using @return $this annotations instead of @return self, and such classes cannot be registered as services. The class resolving fails with following:

Nette\DI\ServiceCreationException
Type Mautic\Auth$this used in service 'mauticApi' not found or is not class or interface.

You can see the file in question directly here. I think return $this is not according to the phpDoc spec, but nevertheless I think ContainerBuilder should support it.

2.3 bc breaks

Can't believe that nobody has complained yet. I'm trying to run my OrmExtension. I'm not sure if I understand the problem correctly, but I think there are 2 issues/BC breaks.:

https://github.com/nextras/orm/blob/fe20359d7b557d83157d4a71d610e21a36c574f1/src/Bridge/NetteDI/OrmExtension.php

  • Inject extension is basically added too early, so it can inject services, which are not there yet. I'm adding services in beforeCompile method. Maybe I'm doing it wrong, but loadConfiguration method doesn't seem to be the right one for adding services.
  • Inject extension works without resolved class names, so even if I add InjectionExtension after the OrmExtension, it doesn't work, because one more $this->builder->prepareClassList(); call is needed.

Unreliable setup ServiceDefinition of generated factory

The following code may or or may not work at all depending on whether the service implementation is generated by Nette DI.

$serviceName = $builder->getByType(Nette\Bridges\ApplicationLatte\ILatteFactory::class);
$serviceDef = $builder->getDefinition($serviceName);

// this may configure either ILatteFactory or Latte\Engine instance
$serviceDef->addSetup('addFilter', ...);

I think that the current behavior is highly confusing. Service definition of the implementation should use different instance (e.g. $serviceDef->getInstanceDef()->addSetup('addFilter', ...)).

When the factory implementation is not generated by Nette DI, modifying of the definition of instance service definition should ideally generate another implementation which would wrap the user provided implementation.

cc @matej21

DependencyChecker tries to load a class that should not be loaded

It's probably best to show this on the real case:

  • Kdyby\Events\EventsManager::setPanel() needs argument of Kdyby\Events\Diagnostics\Panel
  • Kdyby\Events\Diagnostics\Panel implements Tracy\IBarPanel
  • kdyby/events has tracy/tracy as an optional dependency
  • => Tracy\IBarPanel may not exist
  • but that does not matter because Kdyby\Events\Diagnostics\Panel is never loaded at all

The problem is that DependencyChecker is still trying to load the Kdyby\Events\Diagnostics\Panel class:

  • DependencyChecker::calculateHash() iterates over all public methods of Kdyby\Events\EventsManager which of course includes the setPanel method
  • calculateHash calls DependencyChecker::hashParameters() which iterates over all parameters of the setPanel() method (which is only the panel parameters).
  • hashParameters calls PhpReflection::getParameterType($param)
  • getParameterType calls) $param->getClass()
  • that will try to autoload the Kdyby\Events\Diagnostics\Panel class
  • which of course fail with Fatal error: Interface 'Tracy\IBarPanel' not found
  • (no ReflectionException to catch, just fatal error)

Add getDefinitionByType() method

While making extension, I often need some service definition in beforeCompile() method, e.g.:

After making service name optional, this is the only place where it is still required.

Everywhere I need some definition, I have to use these 2 methods:

$containerBuilder = $this->getContainerBuilder();
$serviceName = $containerBuilder->getByType(Nette\Bridges\ApplicationLatte\ILatteFactory::class);
$serviceDefinition = $containerBuilder->getDefinition($serviceName);

I would prefer having single method:

$serviceDefinition = $containerBuilder->getDefinitionByType(
    Nette\Bridges\ApplicationLatte\ILatteFactory::class
);

What do you think?

Config\Loader cannot be configured

While digging through config loading, I noticed that Loader cannot be configured in any way, because it is created in Compiler here https://github.com/nette/di/blob/master/src/DI/Compiler.php#L87.

The question is, how would we fix this? If one would like to provide his own instance of Loader (and for example add like some kind of YAML adapter), it would probably have to be set in Configurator and bubble from there, which is kind of a long path that it has to take to actually do something..

Factory with magic method

Currently, it is not possible to define service factory as call of magic method. It throws Factory 'Aws\Sdk::createSqs' used in service '237' is not callable. It's because this checks

try {
$reflection = Nette\Utils\Callback::toReflection($entity[0] === '' ? $entity[1] : $entity);
$refClass = $reflection instanceof \ReflectionMethod ? $reflection->getDeclaringClass() : NULL;
} catch (\ReflectionException $e) {
}
if (isset($e) || ($refClass && (!$reflection->isPublic()
|| ($refClass->isTrait() && !$reflection->isStatic())
))) {
$name = array_slice(array_keys($recursive), -1);
throw new ServiceCreationException(sprintf("Factory '%s' used in service '%s' is not callable.", Nette\Utils\Callback::toString($entity), $name[0]));
}
throws ReflectionException: Method Aws\Sdk::createSqs() does not exist. See Sdk class definition.

I would like to create client services for particular AWS services. It's much cleaner to inject client directly, than injecting Sdk and then creating a client in the class.

services:
    - Aws\Sdk({
        credentials: {
            key: %aws.access%,
            secret: %aws.secret%,
        },
        version: latest,
        region: 'eu-west-1',
    })
    -
        class: Aws\Sqs\SqsClient
        factory: @Aws\Sdk::createSqs

Generated factory cannot be overwritten

Let's have following setup

namespace My;

class Generated {}
interface IGeneratedFactory {
    /** @return Generated */ public function create();
}
services:
    a: My\IGeneratedFactory

Now image I wanna replace the instance of the factory.

$mock = \Mockery::mock(My\IGeneratedFactory::class);
$container->addService('a', $mock);

This throws unexpected exception, that type My\Generated was expected, but My\IGeneratedFactory was provided.


I'm using 9799449, so I'm not sure if this wasn't fixed already.

Performance issue of magic methods, getters and setters

I was wondering what takes so long while nette is generating container. And surprisingly (or not?) - it's a magic!
This is xdebug profiler output for my app:
di-magie2
(self and inclusive cost is in percent)

I've tried to make some optimization (replacing magic @method in nette/di and nette/phpgenerator with proper methods and removing usage of magic getters/setters) and this is the result:
di-magie3

Ouch!

Do you agree with removing this magic? Should I prepare PR?

DependencyChecker does not works with FileMock

I have had this code in my tests. It is failing with 2.4.0.

$loader = new ContainerLoader(TEMP_DIR);
$className = $loader->load(function (Compiler $compiler) {
    $compiler->loadConfig(FileMock::create('', 'neon'));
}, 'container');

Nette\InvalidStateException: Unexpected dependency boolean

I think its cause:

// Compiler.php

/**
 * Adds new configuration from file.
 * @return self
 */
public function loadConfig($file)
{
    $loader = new Config\Loader;
    $this->addConfig($loader->load($file));
    $this->dependencies->add($loader->getDependencies());
    return $this;
}
// Loader.php

/**
 * Reads configuration from file.
 * @param  string  file name
 * @param  string  optional section to load
 * @return array
 */
public function load($file, $section = NULL)
{
    if (!is_file($file) || !is_readable($file)) {
        throw new Nette\FileNotFoundException("File '$file' is missing or is not readable.");
    }
    $this->dependencies[] = realpath($file);
       ....
}

realpath(FileMock) == bool(FALSE)


It is maybe issue in nette/tester, but I'm not sure if it is issue at all. What do you think?

Support for PSR-11 container interop

  • bug report? no
  • feature request? yes

Description

There is PSR-11 for DI containers that aims to bring interoperability between different DI containers. PSR itself is simple and support in Nette DI would make interop with components from different frameworks easier and wouldn't bring any BC breaks.

There are already several frameworks with PSR-11 support - ZF, Symfony, ...

I can prepare PR if needed.

Incorrectly instantiated DateTimes

I attempted to use DateTime as parameter value in my config:

parameters:
    date: 2016-09-01
php:
    date.timezone: Europe/Prague

The parameter date is instanciated (that's cool), however this is done in the PHP default time zone which is (for example) UTC whereas I explicitly specify that I want to use Europe/Prague.

From brief look inside the source codes I understand that the sections are processed separetly, however this behaviour is highly unexpected and I would consider it a pitfall as it the bugs caused are not easy to find.

Lazy load interface definitions for generated factories

Currently calling $configurator->loadContainer() triggers autoloading for all interfaces implemented by generated factories (which because of parameter and return types cause loading their dependencies…)

The solution is to either move implementation of generated factories to different file or to move it to runtime (possible utilizing anonymous classes in PHP 7)

/**
 * @return App\Components\MyComponent
 */
public function createService__248()
{
    return new class($this) implements App\Components\IMyComponentFactory
    {
        private $container;


        public function __construct(Container_7bc42fd40e $container)
        {
            $this->container = $container;
        }


        public function create(): App\Components\MyComponent
        {
            return new App\Components\MyComponent();
        }
    };
}

Inject extension is not called as last extension

In Nette\DI\Compiler::processExtensions() (https://github.com/nette/di/blob/master/src/DI/Compiler.php#L159) is InjectExtension moved to end of list of extension.

If any extension is later added by ExtensionsExtension, then new extension is added after InjectExtension. This means, that any services added in extension in beforeCompile() can't take advantage of InjectExtension.

I believe, that line 159 (moving InjectExtension to end of extensions) should be after calling ExtensionsExtension::loadConfiguration() to line 168.

Unable to overwrite service arguments

Following configuration will merge arguments instead of overwriting them and I could not find any way to remove old arguments ($foo) or solve it differently than defining the services without using arguments. Is there any way to do it? Or maybe overwriting arguments would be better than merging (when I change class), what do you think?

# global.neon
services:
    a:
        class: Foo
        arguments:
            foo: value1
# local.neon
services:
    a:
        class: Bar
        arguments:
            bar: value2
class Foo {
    public function __construct($foo) {}
}
class Bar {
    public function __construct($bar) {}
}

$configurator = new \Nette\Configurator();
$configurator->setTempDirectory(__DIR__ . '/temp');
$configurator->addConfig(__DIR__ . '/global.neon');
$configurator->addConfig(__DIR__ . '/local.neon');
$container = $configurator->createContainer();

$container->getService('a'); // Unable to pass specified arguments to Bar::__construct()

Autowiring fails when using type hint in method

I have something like this:

namespace App\Forms;
use App\Model\Item;

interface IFormFactory
{
    /**
     * @param Item 
     * @return Form
     */
    function create($item);
}

And this works. But when i add type hint into method param like this:

namespace App\Forms;
use App\Model\Item;

interface IFormFactory
{
    /**
     * @return Form
     */
    function create(Item $item);
}

Autowiring fails because generated Container looks like this:

public function create($item) {
....
}

and function create isn't same as it has been declared in interface.

Incompatibility with neon

nette/di dev-master doesn't work with nette/neon 2.2 (it should according to composer.json). It fails with this error:

`Fatal Error
Undefined class constant 'CHAIN'

File: vendor\nette\di\src\DI\Config\Adapters\NeonAdapter.php:63

Občas nemůže otevřít Nette.Configurator/Container_0000f000f0.php

Ahoj,
obcas v průběhu používání applikace se mi stane, že dostanu http://postimg.org/image/qokdch9lj/
Není to třeba žebych smazal cache a pak se to dělo, na jedné stránce při práci s ní po několika refreshích se mu to prostě občas nepodaří otevřít.

Abych řekl pravdu, absolutně netuším jak to debugovat, ale když se podívám na disk tak tam ten file fyzicky existuje. Kdyby byl nějaký tip jak to pozorovat tak rád pomohu více.

New parameter %rootDir%

One of the very few things that prevents Nette application from beeing compiled before deployment (and therefore using it on readonly filesystems) are hardcoded absolute paths in DI Container.

Now imagine if you were to tell the container the root directory of the project.

// this ofcourse could be autoresolved by default, but the setter should be available
$configurator->setRootDir(dirname(__DIR__)); //  dirname(%appDir%)

Now that we have the absolute path, we can set the value "dynamically"

class SystemContainer
{
    public function __construct()
    {
        parent::__construct(array(
            // ....
            'rootDir' => __DIR__ . '/../..',
        ));
    }
}

and the strings in generated container that contain the rootDir string can be replaced with the dynamic value

"/var/www/hosts/kdyby.org/app/../www/images"
$this->parameters['rootDir'] . "/app/../www/images"

This is merely a concept and there are other obstacles, but IMHO this is the main one. Composer uses the same concept for the generated autoloader. I think we should do it similarly.

Better API for ContainerBuilder::findByType

ContainerBuilder::findByType and Container::findByType works similar:

  • returns names of services (because Container cannot return objects)
  • by default they return only autowired services

On the one hand it is good that these two functions behave consistently, but on the other hand, it is uncomfortable to use ContainerBuilder::findByType:

  • you can always need retrieve definition using $builder->getDefinition($name)
  • it's not clear what FALSE means (classic boolean arguments problem)

So I think that ContainerBuilder::findByType should return all services as pairs [name => definition].

The question is whether Container::findByType can be in future changed to return names of all (including non-autowired) services too? Is it big BC?

(ContainerBuilder::findByType and second argument of Container::findByType exist only in dev-version).

findByType not work properly

Example is more than text, so:

IMy:

interface IMy {

    /**
     * @return My
     */
    public function create();

}

My:

class My implements IMyInterface {

}

IMyInterface:

interface IMyInterface {}

Extension:

class Extension extends CompilerExtension {

    public function beforeCompiler() {
          $this->getContainerBuilder()->findByType('IMyInterface'); // Cannot find class 'My'
    }

}

If I changed IMy to:

class IMy extends IMyInterface { //... }

Now can builder find class 'My'

Question: It´s correct behavior?

CompilerExtension::validateConfig - only first level is validated

  • bug report? yes
  • feature request? yes

Description

nonExistingFirstLevel throws exception, nonExistingSecondLevel not

exampleModule:
    routeLists:
        admin:
            disabled: false
        restApi:
            disabled: true
        nonExistingSecondLevel: true
    nonExistingFirstLevel: true
$this->config = $this->validateConfig(
    $this->defaults, $this->config
);

Parameters auto-resolution for generated factories depends on variable name

Following example:

services:
    - ISomeControlFactory
interface ISomeControlFactory {
    /** @return SomeControl */
    public function create(SomeEntity $someEntity);
}

class SomeControl {
    public function __construct(SomeEntity $entity) {}
}

class SomeEntity {}

leads to exception

Service 'XX_ISomeControlFactory': Service of type SomeEntity needed by SomeControl::__construct() not found. Did you register it in configuration file?

just because the parameter variables have different names.

DI\Container::initialize(): usage of undefined $service variable

There is a bug if I add a panel into config:

nette:
    debugger:
        bar:
            - Panel

and the Panel has dependency on some service that is defined in this notation:

services:
    -
        class: MyService

It will trigger a notice about usage of undefined variable $service:

di-initialize-tracy-bar-problem

If the service is defined as:

services:
    - MyService

than everything is ok.

You can try it yourself in my sandbox branch with one commit added onto last 2.3.1 version:

VladaHejda/sandbox@b459baa

ContainerBuilder - Collector feature, Modular<Filters|Routes>, Commands, EventSubscribers...

  • bug report? no
  • feature request? yes

This should make CompilerExtension more usable.

Examples of current usage in Extension

This is how it's done now. Bellow is suggestion how to do it better.

Issue: add services that implement certain interface to one collecting service

Current solution is by tag, which has few disadvantages:

  • bothers user to remember the tag, even the class is already specific (by interface it implements or class it extends)
  • polutes config
services:
    - 
        class: SomeCommand
        tags: "console.command"

instead of

services:
    - SomeCommand
  • has origin in times of named services when duplicated info, context and service locator, was normal to use

Better solution

Based on new getDefinitionByType() method (#137) and findByType() I suggest adding following method to ContainerBuilder class:

(Feel free to improve the naming.)

public function setToCollectorByType(string $collectorClass, string $collectedClass, string $setterMethod) : void
{
    $containerBuilder = $this->getContainerBuilder();

    $collectorDefinition = $containerBuilder->getDefinitionByType($mediatorClass);
    $collectedDefinitions = $containerBuilder->findByType($collectedClass)    

    foreach ($collectedDefinition as $name => $definition) {
        $collectorDefinition->addSetup($setterMethod, ['@' . $name]);
    }
}

Before

use Nette\DI\CompilerExtension;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;

class MyExtension extends CompilerExtension
{
    public function beforeCompile()
    {
        $containerBuilder = $this->getContainerBuilder();
    
        $consoleApplication = $containerBuilder->getDefinitionByType(Application::class);
        foreach ($containerBuilder->findByType(Command::class) as $name => $definition) {
            $consoleApplication->addSetup('add', ['@' . $name]);
        }
}

After

use Nette\DI\CompilerExtension;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;

class MyExtension extends CompilerExtension
{
    public function beforeCompile()
    {
        $this->setToCollectorByType(Application::class, Command::class, 'set);
    }
}

This would simplify so many use cases and encourage this architecture pattern.

What do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.