Code Monkey home page Code Monkey logo

disco's Introduction

bitexpert/disco

This package provides a PSR-11 compatible, annotation-based dependency injection container. Have a look at the disco-demos project to find out how to use Disco.

Build Status Coverage Status Mastodon Follow

Installation

The preferred way of installing bitexpert/disco is through Composer. You can add bitexpert/disco as a dependency, as follows:

composer.phar require bitexpert/disco

Usage

To instantiate Disco use the following code in your bootstrapping logic. Create an instance of the \bitExpert\Disco\AnnotationBeanFactory and register the instance with the \bitExpert\Disco\BeanFactoryRegistry. The second step is important as Disco needs to grab the active container instance in a few locations where it does not have access to the container instance itself.

<?php

$beanFactory = new \bitExpert\Disco\AnnotationBeanFactory(MyConfiguration::class);
\bitExpert\Disco\BeanFactoryRegistry::register($beanFactory);

Next up you need to create a configuration class MyConfiguration and document it with a @Configuration annotation.

<?php

use bitExpert\Disco\Annotations\Configuration;

/**
 * @Configuration
 */
class MyConfiguration
{
}

To declare a configuration entry, 1) add a method to your MyConfiguration class, and 2) annotate the method with the @Bean annotation. Doing this registers the instance with Disco and uses the type specified by the method’s return value. The primary identifier is the method name:

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Helper\SampleService;

/**
 * @Configuration
 */
class MyConfiguration
{
    /**
     * @Bean
     */
    public function mySampleService() : SampleService
    {
        return new SampleService();
    }
}

To let Disco return the entry with the id mySampleService call the get() method of \bitExpert\Disco\AnnotationBeanFactory, as follows:

<?php

$beanFactory->get('mySampleService');

Documentation

Documentation is in the docs tree, and can be compiled using bookdown.

$ php ./vendor/bin/bookdown docs/bookdown.json
$ php -S 0.0.0.0:8080 -t docs/html/

Then point your browser to http://localhost:8080/

Contribute

Please feel free to fork and extend existing or add new features and send a pull request with your changes! To establish a consistent code quality, please provide unit tests for all your changes and adapt the documentation.

Want To Contribute?

If you feel that you have something to share, then we’d love to have you. Check out the contributing guide to find out how, as well as what we expect from you.

Resources

License

Disco is released under the Apache 2.0 license.

disco's People

Contributors

be-heiglandreas avatar codeliner avatar dropdevcoding avatar heiglandreas avatar jaapio avatar kalessil avatar lavary avatar ocramius avatar senseexception avatar settermjd avatar shochdoerfer avatar skoop 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

disco's Issues

PHP7 type check problem

If strict_types=1 you can set types like string in function signature.

Seems like these types are removed while compilation resulting in following error:
PHP Warning: Declaration of ProxyManagerGeneratedProxy__PM__\src\company\application\Config\Generateda2fc343f86910958b2805ad36ffa91cc::application($sessionSecret = NULL) should be compatible with src\company\application\Config::application(string $sessionSecret = NULL) in /vagrant/api/cache/di/ProxyManagerGeneratedProxy__PM__srccompanyapplicationConfigGenerateda2fc343f86910958b2805ad36ffa91cc.php on line 314

[Feature ]Automatically generate alias from return type declaration

Given you have bean method configuration that looks like this:

/** @Bean({"alias" = "\My\Namespace\Service\ProductService"}) */
public function productService() : \My\Namespace\Service\ProductService
{
    return new \My\Namespace\Service\DefaultProductService();
}

The configured alias matches the return type definition - in this case either the name of an interface or a parent class. Since the alias is a hard-coded string it won't get updated during the usual refactorings (moving files to a different namespace and such) which is not what we want.

It would be nice to provide a sepcial keyword to let Disco automatically determine the return type declaration during the cached configuration generation process and automatically use the return type declaration as an alias, e.g.

/** @Bean({"alias" = "type"}) */
public function productService() : \My\Namespace\Service\ProductService
{
    return new \My\Namespace\Service\DefaultProductService();
}

That way I could access the bean like this:

$beanFactory->get('\My\Namespace\Service\ProductService');

[Feature] Allow ConfigurationGenerator to register custom annotation handlers

Currently the ConfigurationGenerator is working with a fixed set of annotations which is ok for now but in the future I`d like to see a way to add custom annotation handlers which should be able to generate methods (and maybe more?) in the generated proxy class so that frameworks could add own annotations (e.g. an annotation to configure the routing or security settings).

Insufficient boolean cast for @Parameter required attribute

Currently the @Parameter "required" option is casted to boolean via (bool) internally, while the @Bean "lazy" option is casted via a dedicated method called parseBooleanValue.

This leads to the inconvenience that you only may use "required"="0" since 0 as string gets casted correctly to false while "false" will result in true.

Solution suggestion:

Use an AnnotationAttributeParser which contains the method parseBooleanValue as static method and use it in the Bean annotation and Parameter Annotation

Hint: For testing you will have to clear the cache files after each configuration change to get the correct results.

Check given $id for being a non-empty string

Since the Container-Interop ContainerInterface does not use type hinting but the get() and has() method have declared $id as string param type, AnnotationBeanFactory should check the given id for being a non-empty string and throw an exception else. Otherwise at least the AliasContainer will throw a confusing exception concerning the AliasContainer because hasAlias has defined a string type hint in its signature.

Disco-Docs as github-pages

As disco is already hosted on github, wouldn't it be a great idea to serve the disco-documentation (that is currently only available via a local server) via github pages?

AFAIK that would require enabling the github-pages for the repo and setting the source to the docs-folder. Everything else should then work out from scratch. Probably linking the different pages needs to be taken care of.

Alternative would be to use the existing bookdown-setup to create the docs and deploy them to github pages…

It seems that Disco has performance issues

Hey Stephan,

In my container benchmark, Disco seems to extremely underperform in tests where autoloading and startup times are also counted (https://rawgit.com/kocsismate/php-di-container-benchmarks/master/var/benchmark.html)

Could you please tell me why it happens? Once, I saw a compiled Disco container in my "/tmp" directory and I have a feeling that it is generated run-time. Can you confirm it? If this is the case, why it doesn't happen build-time (as a separate step)? I saw in the readme that Disco's startup time can be slow but if my benchmark is correct, unfortunately this is way much than slow.

Problem occured during installation

There is a problem with installing the latest (v0.1.2) version of bitexpert/disco via composer.

$> composer require bitexpert/disco

Using version ^0.1.2 for bitexpert/disco ./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

Problem 1

  • Installation request for bitexpert/disco ^0.1.2 -> satisfiable by bitexpert/disco[0.1.2].
  • bitexpert/disco 0.1.2 requires phpdocumentor/reflection-common ^1.0@dev -> no matching package found.

Potential causes:

Read https://getcomposer.org/doc/articles/troubleshooting.md for further common problems.

Installation failed, deleting ./composer.json.

Handle "special" chars in the identifiers

While experimenting with Disco and Expressive I realized that currently Disco is not able to work with Expressive for one reason: Expressive uses pseudo class names to query service instances from the service container. Since Disco tries to call a method with the same name on the config class this currently does not work because "" is not a valid character to be used for a method name.

To solve the issue we need a mapper in between to translate the invalid characters into valid ones.

Disco needs a logo

Disco needs a logo which can be used in the documentation as well as for marketing purposes.

Exposing proxymanager configuration

Exposing proxymanager configuration to be able to customize the inner workings of the proxymanager, e.g. where to store the generated files. Currently the system temp dir will be used which can turn into a problem when multiple projects have configuration classes with the same name.

Add BeanFactoryBeanPostProcessor

Add BeanFactoryBeanPostProcessor which passes a beanFactory instance to classes implementing the BeanFactoryAware interface. Made sure the implementation is added to the list of the default post processors.

Use UniqueIdentifierGenerator::getIdentifier to generate unique names for helper methods

To avoid naming collisions make use of UniqueIdentifierGenerator::getIdentifier to generate unique method names for the following methods:

  • getParameter()
  • wrapBeanAsLazy()

Since these helper methods are accessible from the outside or "magic" methods needed by PHP we cannot generate unique methods names:

  • __construct()
  • getAlias()
  • hasAlias()
  • __sleep()
    Ideally the ConfigurationGenerator should be extended to check for naming collisions and report errors upfront.

Improve README file

Optimize the README.md file to make it easier for beginners to get started. In addition I would love to see the following changes:

  • add a rationale to explain why Disco makes sense and why/how it is different to the other DI containers in PHP
  • highlight the different use-cases that can be solved with Disco
  • highlight the different ways of injecting dependencies e.g. constructor injection, setter injection, interface injection
  • improve the documentation of the bean configuration settings and how they affect each other
  • fix some typos ;)

Use Phing as build system

Port the bitExpert internal Phing infrastructure to disco to make sure all our packages follow the same conventions.

Rising Code Coverage for Disco

Currently, Disco has a code coverage of ~95%, but most of its coverage happened by hardcoded instance creations over bitExpert\Disco\AnnotationBeanFactory and bitExpert\Disco\AnnotationBeanFactoryUnitTest. In case of possible refactorings, does it make sense to add coverage by testing every class' methods separated in custom unit tests?

And why 100% code coverage?

I started to take a look into the uncovered code and asked myself if there may be unreachable code by writing tests.

For example: https://github.com/bitExpert/disco/blob/v0.8.0/src/bitExpert/Disco/BeanFactoryConfiguration.php#L146

if (!spl_autoload_register($this->proxyAutoloader, false)) {
    throw new RuntimeException(
        sprintf('Cannot register autoloader "%s"', get_class($this->proxyAutoloader))
    );
}

spl_autoload_register can return false in cases of an error, like using a function name, that doesn't exist, in form of a string, but I didn't find a way to make this fail with a callable autoloader class using __invoke(). I haven't try the same for spl_autoload_unregister yet, but I expect the same like in spl_autoload_register.

Extend ConfigurationGenerator to also deal with protected methods.

Extend ConfigurationGenerator to also deal with protected methods. Protected methods could be used as "internal" dependencies which cannot retrieved externally by calling the get() method on the BeanFactory but can be used as dependencies for other instances. For example I do not want to expose my database connection object to the public but use it as an internal dependency.

Currently is this already possible but all annotations get ignored as no wrapper methods get generated in the proxy class.

Config

Hey,

I'd like to suggest another way to provide application wide configuration and a second source for parameter injecting.

https://framework.zend.com/manual/2.4/en/modules/zend.config.introduction.html

Zend config encapsulates config into an object and provides getter, setter and methods to make config values readonly.

This could be pretty useful instead of using arrays. For example, if a config is not properly set an exception will be thrown. Error handling using a config structure like this could be improved.

No need to use zend/config, but for me it's pretty nice and something similar or even zend/config could be helpful.

It also can parse config files as yaml, xml, json and so on, which is nice.
For example using zend/config get you could pass a second parameter as default.

I'm not sure how Parameters and Parameter annotations work at the moment and I didn't dig in, but would it work in general to make use of it?

If so and the idea sounds not bad I could try to implement it.

Regards

Upgrade to ProxyManager 2

In the process of upgrading to ProxyManager 2 do:

  • drop PHP 5.x support
  • use PHP 7 strict mode
  • get rid of phpdocumentor/type-resolver dependency
  • make use of return type hints instead of the @return annotation (which will help with the current issues we have using @return annotations in traits in different namespaces)
  • update README.md

Allow Disco to return basic types

Due to the changes #40 it is no longer possible to return primitive types. As pointed out by @settermjd it might make sense to return primitives sometimes.

This requires a few changes:

  • it is needed to check if type defined in @return is a primitive type. If it is ommit the type check
  • Lazy Beans need to check the type and do not use proxymanager for primitives

It make sense to wait for the ProxyManager 2 / PHP 7.x migration (see #46) before tackling this problem as with the built-in return typehint support it might be easier to handle the type detection.

Add a few more checks for the @Bean annotated methods.

Add a few more checks for the @bean annotated methods, e.g. the methods need to be publicly accessible, otherwise the methods cannot be called from the AnnotationBeanFactory. The generator should throw some exceptions when an "error" in the configuration was found.

Add PSR-3 logger to trace what`s going on

Add an PSR-3 logger instance via bitexpert/slf4psrlog to trace what`s going on during the cached configuration building phase. This should help debugging when things "go wrong".

PHP 8 and attributes

With PHP 8 and having attributes support, configuring beans can be even more simpler. I don't expect to have a complete picture of what's needed, since I have only experience with discolight where I did this proposal (sort of). Anyway here's my take:

There are some options to walk this path:

  1. Add support to the 0.x major versioning line
  2. Replace configuration by annotation with attributes which of course would require a major version release: 1.x

I can't think of a good reason to go for option 2. Adding it to 0.x allows for a gradual upgrade path allowing to deprecate features which can eventually be removed in 1.x.

Adding this feature can be done by:

  1. add support to current Annotation classes, or
  2. creating new Attribute classes.

While new Attribute classes allows you to start with a clean slate, current Annotation classes can quite easily be adapted in order to be used as Attribute classes as well.

As option 1 is in my opinion the most valuable approach I'll go a bit further on that:

Add support to current Annotation classes

Example of how that can be achieved in case of the Alias class:

/**
 * @Annotation
 * @Target({"ANNOTATION"})
 * @Attributes({
 *   @Attribute("name", type = "string"),
 *   @Attribute("type", type = "bool"),
 * })
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class Alias
{
    /**
     * @var string
     */
    private $name;

    /**
     * @var bool
     */
    private $type;

    /**
     * Creates a new {@link \bitExpert\Disco\Annotations\Bean\Alias}.
     *
     * @param array $attributes
     * @throws AnnotationException
     */
    public function __construct(array $attributes = [], ?string $name = null, bool $type = false)
    {
        // When configured by annotations the $attributes['value'] is not empty, so
        // in that case override the $name and $type variables from the $attributes['value'] array.
        if (!empty($attributes['value'])) {
            $attributes = $attributes['value'];
            // Assign local variables the value from $attributes meanwhile making sure the keys exist
            // with at least the default value as defined in the constructor.
            ['name' => $name, 'type' => $type] = $attributes + ['name' => $name, 'type' => $type];
        }

        if (!is_bool($type)) {
            $type = AnnotationAttributeParser::parseBooleanValue($type);
        }

        if ($type && $name) {
            throw new AnnotationException('Type alias should not have a name!');
        }

        if (!$type && !$name) {
            throw new AnnotationException('Alias should either be a named alias or a type alias!');
        }

        $this->type = $type;
        $this->name = $name;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function isTypeAlias(): bool
    {
        return $this->type;
    }
}

class Config
{
    /**
     * Using named arguments
     */
    #[Bean]
    #[Alias(type: true)]
    #[Alias(name: 'my.service.id')]
    public function myService(): Service
    {}

    /**
     * Using ordered arguments
     */
    #[Bean]
    #[Alias([], null, true)]
    #[Alias([], 'my.service.id')]
    public function myOtherService(): OtherService
    {}
}

As you can see named arguments allow for a clean usage of existing Annotation classes.

What about nested attributes?
In PHP's current implementation of attributes nesting is not supported, so explicitly configuring needs to be done on the same level, or instances of nested attributes/annotations should be able to be configured with scalars and instantiated in the "parent" attribute instance.

class Config {
    #[Bean(aliases: [
        ['type' => true],
        ['name' => 'my.service.id'],
    ])]
    public function myService(): Service
    {}
}

What does this means for each type of annotation?

Alias
Allow configuration of

  • name
  • type

Bean
Allow configuration of

  • scope
  • singleton
  • lazy
  • aliases discouraged, use (multiple) Alias attributes instead
  • parameters discouraged, use (multiple) Parameter attributes instead

BeanPostProcessor
Allow configuration of

  • parameters discouraged, use (multiple) Parameter attributes instead

Configuration
Just an identifier attribute

Parameter
The ordering of parameters is important since the method is called with the parameters in the order they are configured. Nesting the parameters inside the Bean annotation provides a natural way of respecting the ordering of the arguments in the methods signature.
But requiring attributes to be listed in a specific order feels a bit like a smell to me.

class Config {
    #[Bean(parameters: [
        ['name' => 'config.key1', 'default' => 'default value'],
        ['name' => 'config.key2', 'default' => 'other default value'],
    ])]
    public function myService($key1, $key2): Service
    {}

    // Correct ordering of the parameter attributes required, NOT IDEAL
    #[Bean]
    #[Parameter(name: 'config.key1', default => 'default value')]
    #[Parameter(name: 'config.key2', default => 'other default value')]
    public function myOtherService($key1, $key2): OtherService
    {}
}

Possible solution: When using the Parameter attribute, require a config key with the name of the argument so it can be used for calling the method with named arguments. The nicest would be something like this:

$parameters = [
    'config' => [
        'key1' => 'value of key1',
        'key2' => 'value of key2',
    ],
];
class Config {
    #[Bean]
    #[Parameter(name: 'arg2', key: 'config.key2', default => 'other default value')]
    #[Parameter(name: 'arg1', key: 'config.key1', default => 'default value')]
    public function myService($arg1, $arg2): Service
    {
        // $arg1 = 'value of key1;
        // $arg2 = 'value of key2;
    }
}

Unfortunately name is already in use as the name of the parameter key, but i.e. argname can perhaps be a good alternative.

How to incorporate this with the ConfigurationGenerator?

The doctrine AnnotationReader can be swapped out for a FallbackAttributeReader. The FallbackAttributeReader would prioritize attributes over Annotations.

I would not recommend mixing annotations and attributes on the same level.

Possible guidelines:

  • When the Bean attribute is found on a method ignore any annotations.
  • When the Bean attribute is found on a method look for Alias and Parameter attributes.
  • When the Bean attribute has nested aliases prioritize these over configured Alias attributes.
  • When the Bean attribute has nested parameters prioritize these over configured Parameter attributes.
  • When the BeanPostProcessor attribute is found on a method ignore any annotations.
  • When the Configuration attribute is found on a method ignore any annotations.

[Feature] Disco should be able to autowire bean instances

As discussed with @Ocramius today it would make sense for Disco to be able to support autowiring for bean instances to reduce the amount of configuration code needed and to be able to get rid of traits for structuring the configuration code.

The following configuration:

/** @Bean */
protected function database() : Database
{
    return new Database('mysql://user:secret@localhost/mydb');
}

/** @Bean */
public function productService() : ProductService
{
    return new ProductService($this->database());
}

could then be simplified like this:

/** @Bean */
protected function database() : Database
{
    return new Database('mysql://user:secret@localhost/mydb');
}

/** @Bean */
abstract public function productService() : ProductService;

At a later stage we could move the methods into separate interfaces - to be able to get rid of the abstract keyword and ultimately get rid of the trait approach.

A few problems need to be solved:

  • When using interfaces as configuration source, how to cope with method naming clashes?
  • When there are multiple database instances available how would Disco decide which one to use? We would need a way to pass the method name or alias name to the bean configuration of the ProductService.
  • For Constructor Injection things are simple, but to be able to decide which methods needs to be called for Setter Injection we might need annotations on the setter methods of the class. So far the goal of Disco was to keep annotations just in the configuration class. Maybe we can solve this differently?
  • How to cope with primitive values that should be injected? Maybe we could rely on the same naming scheme - e.g. when the parameter of the bean definition method is called $dsn we check if the class has a parameter with that name as constructor argument, same for the setter methods.
  • For primitives we obviously need to skip the whole injection logic.
  • When adding this feature it might make sense to enable the production autoloader of Proxy Manager by default as the constant parsing could slow things down quite a bit.

has() returns true for internal dependencies.

AnnotationBeanFactory::has() returns true for "internal dependencies" (methods in config class marked as protected) since method_exists() simply checks if the method does exists not if the method can actually be called from the current context. This is a problem when AnnotationBeanFactory::get() is called since call_user_func() is not able to call the method as intended.

To fix the problem substitute the method_exists() call with is_callable(). This seems to be a side-effect of #10.

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.