Ankündigung

Einklappen
Keine Ankündigung bisher.

DI-Container

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • DI-Container

    Ich habe jetzt einige Zeit an einer eigenen Implementierung eines DI-Containers gesessen und was dabei heraus gekommen ist, würde ich gerne mal zur Diskussion in den Raum werfen. Es ist noch nicht 100%ig durch getestet, aber man erkennt die Funktionsweise, denke ich. Schwerpunkte waren Performance und Flexibilität. So - los geht's:

    Der eigentliche Container:
    PHP-Code:
    class ZN_DI_Container {

            
    /**
             * @var ZN_DI_Container
             */
            
    protected static $instance null;

            
    /**
             * @var array
             */
            
    protected $config          = array();

            
    /**
             * @var array
             */
            
    protected $storage         = array();

            
    /**
             * @var array
             */
            
    protected $loading         = array();
            
            
    /**
             * Indicator for binding references
             *
             * @var string
             */
            
    protected $bindingPrefix   ':';
            
            
    /**
             * Indicator for service references
             *
             * @var string
             */
            
    protected $servicePrefix   '@';

            
    /**
             * Affords a singleton mechanism if needed
             *
             * @param boolean $new
             * @return ZN_DI_Container
             */
            
    public static function instance($new false) {
                if (
    $new) {
                    return new 
    self();
                }
                if (
    self::$instance === null) {
                    
    self::$instance = new self();
                }

                return 
    self::$instance;
            }

            public function 
    __construct() {
            }

            
    /**
             * @param array $config
             * @return ZN_DI_Container
             */
            
    public function setConfig(array $config) {
                
    $this->config $config;

                return 
    $this;
            }

            
    /**
             * @return array
             */
            
    public function getConfig() {
                return 
    $this->config;
            }

            
    /**
             * Retrieves an assembled service using the specified di configuration
             *
             * @param string $serviceId
             * @param boolean $singleton = true
             * @return object
             */
            
    public function get($serviceId$singleton true) {
                if (!
    $this->hasServiceConfig($serviceId) && !$this->hasStoredService($serviceId)) {
                    
    //If no config specified, the service must be injected manually
                    
    throw new Exception('Service "'.$serviceId.'" must be set manually');
                }
                
                if (isset(
    $this->loading[$serviceId])) {
                    
    //prevent endless loops
                    
    throw new Exception('Circular reference on "'.$serviceId.'"');
                }
                
                
    $this->loading[$serviceId] = true;
                
                if (
                    (
    $singleton && $this->hasStoredService($serviceId)) ||
                    (!
    $this->hasServiceConfig($serviceId) && $this->hasStoredService($serviceId)) //Manually set service
                
    ) {
                    
    //If singleton or manually injected service -> return existing instance
                    
    unset($this->loading[$serviceId]);
                    return 
    $this->storage[$serviceId];
                }
                
                
    $serviceConfig $this->getServiceConfig($serviceId);
                
    $class         $serviceConfig['class'];
                
    $service       null;
                
    $args          = array();
                if (isset(
    $serviceConfig['args']) && is_array($serviceConfig['args']) && count($serviceConfig['args']) > 0) {
                    
    //Constructor args available
                    
    $ref     = new ReflectionClass($class);
                    
    $service $ref->newInstanceArgs($this->prepareParamSetForMethodCall($serviceConfig['args']));
                } else {
                    
    $service = new $class();
                }
                
                
    //Call methods which are specified in the configuration
                
    if (isset($serviceConfig['methods']) && is_array($serviceConfig['methods']) && count($serviceConfig['methods']) > 0) {
                    foreach (
    $serviceConfig['methods'] as $method=>$calls) {
                        foreach (
    $calls as $call=>$params) {
                            
    $this->callServiceMethod($service$method$this->prepareParamSetForMethodCall($params));
                        }
                    }
                }
                unset(
    $this->loading[$serviceId]);

                if (
    $singleton) {
                    
    $this->storage[$serviceId] = $service;
                    return 
    $this->storage[$serviceId];
                } else {
                    return 
    $service;
                }
            }
            
            
    /**
             * Calls a method on a service depending on given arguments
             *
             * @param object $service
             * @param string method
             * @param array $args
             * @return mixed
             */
            
    protected function callServiceMethod($service$method, array $args) {
                if (
    count($args) > 1) {
                    
    //if there are more than one argument, use call_user_func_array
                    
    return call_user_func_array(array($service$method$this->prepareParamSetForMethodCall($args)));
                } else if (
    count($args) == 1) {
                    return 
    $service->$method($args[0]);
                } else {
                    return 
    $service->$method();
                }
            }
            
            
    /**
             * Prepares argument lists based on the specified di configuration
             *
             * @param mixed
             * @return mixed
             */
            
    public function prepareParamSetForMethodCall($params) {
                if (
    is_object($params)) {
                    throw new 
    UnexpectedValueException('Invalid data format');
                }
                if (
    is_array($params)) {
                    foreach (
    $params as $k=>$v) {
                        if (
    is_array($v)) {
                            
    $params[$k] = $this->prepareParamSetForMethodCall($v);
                        } else if (
    $this->isBindingReference($v)) {
                            
    $params[$k] = $this->getBinding($this->getBindingIdFromString($v));
                        } else if (
    $this->isServiceReference($v)) {
                            
    $params[$k] = $this->get($this->getServiceIdFromString($v));
                        }
                    }
                } else {
                    if (
    $this->isBindingReference($params)) {
                        
    $params $this->getBinding($this->getBindingIdFromString($params));
                    } else if (
    $this->isServiceReference($params)) {
                        
    $params $this->get($this->getServiceIdFromString($params));
                    }
                }
                
                return 
    $params;
            }

            
    /**
             * Injects a service object
             *
             * @param string $serviceId
             * @param object $service
             * @param boolean $overwrite = true
             */
            
    public function set($serviceId$service$overwrite true) {
                if (
    $this->hasStoredService($serviceId) && !$overwrite) {
                    throw new 
    Exception('Service "'.$serviceId.'" is already set');
                }
                
                
    $this->storage[$serviceId] = $service;

                return 
    $this;
            }
            
            
    /**
             * Checks if a service is already stored (injected or singleton)
             *
             * @param string $serviceId
             * @return boolean
             */
            
    public function hasStoredService($serviceId) {
                return isset(
    $this->storage[$serviceId]);
            }
            
            
    /**
             * Checks if a configuration set for a service is set
             *
             * @param string $serviceId
             * @return boolean
             */
            
    public function hasServiceConfig($serviceId) {
                return isset(
    $this->config['services'][$serviceId]);
            }
            
            
    /**
             * Retrieves a configuration set for a cetain service
             *
             * @param string $serviceId
             * @return array
             */
            
    public function getServiceConfig($serviceId) {
                if (!
    $this->hasServiceConfig($serviceId)) {
                    throw new 
    UnexpectedValueException('Config for service "'.$serviceId.'" not specified');
                }
                return 
    $this->config['services'][$serviceId];
            }
            
            
    /**
             * Specifies a configuration set for a service
             *
             * @param string $serviceId
             * @param array $config
             * @$param boolean $overwrite = true
             * @return ZN_DI_Container
             */
            
    public function setServiceConfig($serviceId, array $config$overwrite true) {
                if (
    $this->hasServiceConfig($serviceId) && !$overwrite) {
                    throw new 
    Exception('Service config for "'.$serviceId.'" is already set');
                }
                
                if (!isset(
    $config['class'])) {
                    throw new 
    Exception('No class set for serice "'.$serviceId.'"');
                }
                
                
    $this->config['services'][$serviceId] = $config;
                
                return 
    $this;
            }
            
            
    /**
             * Checks if a string is a reference to another service
             *
             * @param string
             * @return boolean
             */
            
    public function isServiceReference($serviceId) {
                return 
    is_string($serviceId) && strpos($serviceId$this->getServicePrefix()) === 0;
            }
            
            
    /**
             * @param string $servicePrefix
             * @return ZN_DI_Container
             */
            
    public function setServicePrefix($servicePrefix) {
                
    $this->servicePrefix $servicePrefix;
                
                return 
    $this;
            }
            
            
    /**
             * @return string
             */
            
    public function getServicePrefix() {
                return 
    $this->servicePrefix;
            }
            
            
    /**
             * Retrieves the service id from a string which indicates a service reference
             *
             * @param string $str
             * @return string
             */
            
    public function getServiceIdFromString($str) {
                return 
    substr($strstrlen($this->getServicePrefix()));
            }
            
            
    /**
             * @param $bindingId
             * @return boolean
             */
            
    public function hasBinding($bindingId) {
                return isset(
    $this->config['bindings'][$bindingId]);
            }
            
            
    /**
             * Checks if a string is a reference to binding
             *
             * @param string
             * @return boolean
             */
            
    public function isBindingReference($str) {
                return 
    is_string($str) && strpos($str$this->getBindingPrefix()) === 0;
            }
            
            
    /**
             * Retrieves the binding id from a string which indicates a binding reference
             *
             * @param string $str
             * @return string
             */
            
    public function getBindingIdFromString($str) {
                return 
    substr($strstrlen($this->getBindingPrefix()));
            }
            
            
    /**
             * Injects a binding
             *
             * @param string bindingId
             * @param mixed $value
             * @return ZN_DI_Container
             */
            
    public function setBinding($bindingId$value) {
                
    $this->config['bindings'][$bindingId] = $value;
                
                return 
    $this;
            }
            
            
    /**
             * Retrieves a specified binding by an id. If $recursive
             * is true the result is checked if it is a binding reference
             * itself
             *
             * @param string $bindingId
             * @param boolean $recursive = true
             */
            
    public function getBinding($bindingId$recursive true) {
                if (!
    $this->hasBinding($bindingId)) {
                    throw new 
    UnexpectedValueException('Binding "'.$bindingId.'" not specified or invalid');
                }
                
                
    $rsl $this->config['bindings'][$bindingId];
                
                if (
    $this->isBindingReference($rsl)) {
                    return 
    $this->getBinding($this->getBindingIdFromString($rsl), $recursive);
                } else {
                    return 
    $rsl;
                }
            }
            
            
    /**
             * @param string $bindingPrefix
             * @return ZN_DI_Container
             */
            
    public function setBindingPrefix($bindingPrefix) {
                
    $this->bindingPrefix $bindingPrefix;
                
                return 
    $this;
            }
            
            
    /**
             * @return string
             */
            
    public function getBindingPrefix() {
                return 
    $this->bindingPrefix;
            }
        } 
    Eine Bsp.-Konfiguration:
    PHP-Code:

        
    return array(
            
    'bindings'=>array(
                
    'dbDSN'=>'mysql:host=localhost;dbname=...',
                
    'dbUser'=>'root',
                
    'dbPassword'=>'123456',
                
    'dbEncodingCommand'=>'SET NAMES UTF8',
                
    'dbDefaultFetchMode'=>PDO::FETCH_ASSOC
            
    ),
            
    'services'=>array(
                
    'test'=>array(
                    
    'class'=>'TestClass',
                    
    'args'=>array(),
                    
    'methods'=>array(
                        
    'setService'=>array(
                            array(
    '@service'//Muss manuell injiziert werden, da nicht vorhanden
                        
    ),
                        
    'setDb'=>array(
                            array(
    '@dbConnection')
                        )
                    )
                ),
                
    'dbConnection'=>array(
                    
    'class'=>'PDO',
                    
    'args'=>array(
                        
    ':dbDSN',
                        
    ':dbUser',
                        
    ':dbPassword',
                        array(
                            
    PDO::MYSQL_ATTR_INIT_COMMAND=>':dbEncodingCommand',
                            
    PDO::ATTR_DEFAULT_FETCH_MODE=>':dbDefaultFetchMode'
                        
    )
                    )
                )
            )
        ); 
    Ein paar Beispiele:
    PHP-Code:
        class TestClass {
            
            public function 
    setService(StdClass $service) {
            
            }
            
            public function 
    setDb(PDO $db) {
            
            }
        }
        
        require_once(
    dirname(__FILE__).'/zn/di/container.php');
        
        
    $di ZN_DI_Container::instance();
        
    $di->setConfig(require(dirname(__FILE__).'/config.php'));
        
        
        
    //Binding setzen
        
    $di->setBinding('key''value');
        
        
    //Service manuell setzen
        
    $obj = new StdClass();
        
    $di->set('service'$obj);
        
        
    var_dump($di->get('test'));
        
        
    //$db = $di->get('dbConnection'); 
    Ich denke, die Konfiguration ist selbst erklärend. Wer sich wundert, dass es bei den "methods" immer doppelt verschachtelte Arrays gibt: Das hat den Grund, dass man einen Funktionsaufruf mehrmal durchführen kann (z. B. mit verschiedenen Parametern).

    Was haltet ihr von der ganzen Sache? Gibt es was, was verbessert werden könnte? Was, was gar nicht geht?

  • #2
    Wozu sol ldas gut sein, den Singleton umgehen zu können?
    Zudem kommt man an die neue Instanz nie wieder ran, sofern man sie nicht händisch weiterreicht.
    Ausserdem würde ich den Konstruktor auf protected setzen, soll ja eigentlich nicht von extern instanziierbar sein.
    VokeIT GmbH & Co. KG - VokeIT-oss @ github

    Kommentar


    • #3
      Der Gedanke war, dass man einerseits immer eine Singleton-Instanz verfügbar haben kann, was zu 99,9% der Fall sein wird. Wenn es aber notwendig sein sollte - aus was für Gründen auch immer - dann kann man sich eben eine weitere Instanz holen. Das mit dem protected Konstruktor stimmt.

      Kommentar


      • #4
        Schaut printipiell ganz gut aus. Die Bindings sind nett. Mir persönlich ist das zu viel Konfiguration. Da kriegst du unter Umständen Probleme falls du mit Third-Party-Modulen/Bundles arbeitest. Die müssen nämlich alle auf die gleiche Config..

        Kommentar


        • #5
          Mhja.. Das ist halt die Sache - Mir ist keine Lösung eingefallen, mit der man die Flexibilität so weit erhalten kann. Viele DI-Container, die ich gesehen hatte, unterstützen nur Setter und bei verschachtelten Parametern war sowieso bei allen Schluss.

          Was mir noch vorschwebt, ist ein Konfigurationsobjekt, das man auch mit yaml, ini oder xml füttern kann.

          Kommentar


          • #6
            Ich favorisiere den Ansatz von MEF bei .NET. Da MEF prinzipiell dafür sorgen soll, dass man Extensions einfach nur in das Programmverzeichnis kopieren muss und alles funktioniert, gibt es dort überhaupt keine starre Konfiguration. Die verwenden Autodiscovery auf Basis von dynamischen Katalogen und Annotationen.

            Verschachtelte Parameter sind immer kompliziert. Ob MEF das kann, weiß ich jetzt gar nicht. Ggf. müsste man dafür auch ne Kombination aus MEF und Unity verwenden (eigenständiger DI Container für .NET). Wobei man sicherlich seine Strukturen auch so wählen kann, dass man sich die Verschachtelei sparen kann.

            Kommentar


            • #7
              Wenn ich das richtig verstanden habe, wird da die Konfiguration quasi programmatisch injiziert. Das ist wiederum was, worauf ich nicht so stehe. Die zentrale Verwaltung in einer Datei gefällt mir besser, weil ich immer weiß, wo ich was zu ändern habe und ich muss im Falle einer Änderung die eigentliche Applikation nicht anfassen. Oder habe ich da was falsch verstanden?

              Kommentar


              • #8
                Wobei man sicherlich seine Strukturen auch so wählen kann, dass man sich die Verschachtelei sparen kann.
                Ich glaube, das Verschachteln von Parametern ist eine Spezialität von PHP. Ich kenne keine andere Sprache, in der man als Parameter assoziative Arrays als Parameter benutzt. Das ist der einzige Hinkefuß an der der ganzen Sache. Ansonsten könnte man alles mit Service-Referenzen erledigen.

                Ich muss leider zugeben, dass meine .Net-Kenntnisse nicht ausreichen, um durchzublicken, was mef und unity letztendlich machen, außer, dass sie für DI genutzt werden können.


                EDIT: Bzgl. der Konfiguration: Was auch Prämisse war, war, dass man fremde Objekte benutzen kann, ohne irgendwas an denen oder für die anpassen zu müssen, sondern einen einzigen generischen Weg hat, die zu holen.

                Kommentar


                • #9
                  In allen anderen Sprachen würde man dafür eine Klasse anlegen

                  Bei MEF (Managed Extensibility Framework) geht es darum Plugin-Architekturen umzusetzen. Die tatsächlich verwendeten Plugins / Module stehen zur Design-Time nicht fest, entsprechend kann auch keine feste Kopplung verwendet werden. Jedes Modul kann eigene Services zur Verfügung stellen (Export) oder Services konsumieren (Import). Da die Module aber nicht bekannt sind, entfällt natürlich auch die Möglichkeit Abhängigkeiten zu konfigurieren. Unity ist ein klassischer DI-Container mit Konfiguration.

                  Sicher, über Konfiguration kannst du fremde Objekte nutzen ohne diese zu ändern. Du kannst allerdings keine Dienste konfigurieren, von denen du noch gar nicht weißt, dass die später mal zur Verfügung gestellt werden Beide Ansätze sind valide. Man könnte sogar einen hybriden Ansatz nehmen. Autodiscovery für Module, die für dieses Framework erstellt wurden und Konfiguration für Third-Party-Libraries.

                  Kommentar


                  • #10
                    Du kannst allerdings keine Dienste konfigurieren, von denen du noch gar nicht weißt, dass die später mal zur Verfügung gestellt werden
                    Da DI quasi global arbeitet, ist es sowieso recht gefährlich, Dienste zu nutzen, die nur vielleicht mal zur Verfügung gestellt werden
                    [COLOR="#F5F5FF"]--[/COLOR]
                    [COLOR="Gray"][SIZE="6"][FONT="Georgia"][B]^^ O.O[/B][/FONT] [/SIZE]
                    „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
                    [URL="http://www.php.de/javascript-ajax-und-mehr/107400-draggable-sorttable-setattribute.html#post788799"][B]Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“[/B][/URL][/COLOR]
                    [COLOR="#F5F5FF"]
                    --[/COLOR]

                    Kommentar


                    • #11
                      Zitat von xm22 Beitrag anzeigen
                      Mhja.. Das ist halt die Sache - Mir ist keine Lösung eingefallen, mit der man die Flexibilität so weit erhalten kann. Viele DI-Container, die ich gesehen hatte, unterstützen nur Setter und bei verschachtelten Parametern war sowieso bei allen Schluss.

                      Was mir noch vorschwebt, ist ein Konfigurationsobjekt, das man auch mit yaml, ini oder xml füttern kann.
                      Schau Dir mal die Argument-Typattribute "Service" und "Collection" vom SF2 DIC an. Damit ist es sehr wohl möglich, "verschachtetelte" Parameter zu definieren. Der Usecase ist hoffentlich eher selten.

                      Der Sf Container arbeitet auch mit verteilter Konfiguration, die dann zu einer großen Service Definition "kompiliert" wird - von variablen Formaten (php, yaml, xml, Annotation durch Erweiterung) ZU einem variablen Format (php, yaml, xml).

                      Dadurch ist das Ding leichtgewichtig genug - die Konfiguration wird nicht zur Laufzeit geparst (was bei deinem Prototypen - wenn ich das richtig verstehe, - durchaus noch der Fall ist).

                      Im Grunde würde ich Dir zu DRTW raten - es sei denn, Du bist halt heiß drauf, es konzeptuell unbedingt selbst zu erfassen, was natürlich reizvoll ist.

                      Kommentar


                      • #12
                        Schau Dir mal die Argument-Typattribute "Service" und "Collection" vom SF2 DIC an. Damit ist es sehr wohl möglich, "verschachtetelte" Parameter zu definieren.
                        Stimmt.

                        Der Usecase ist hoffentlich eher selten.
                        Glücklicherweise - Wie schon weiter vorne beschrieben, gibt es diese Problematik auch fast nur bei PHP.

                        die Konfiguration wird nicht zur Laufzeit geparst
                        Was meinst Du genau? Momentan wird der Container ja einfach mit einem nativen Array gefüttert. Wenn man mal einen Konfigurations-Provider ins Auge fasst, dann kann man ja einen Cache-Mechanismus implementieren.

                        Was mich bei SF2 (Ich denke, Du meinst Symfony2) abschreckt, ist dieses riesige Konstrukt drum herum - auch wenn das im Sinne der Entwicklung einer größeren Applikation jetzt nicht so die Rolle spielt.

                        Ich finde gerade nichts: Was ist DRTW?

                        Kommentar


                        • #13
                          Zitat von nikosch Beitrag anzeigen
                          Da DI quasi global arbeitet, ist es sowieso recht gefährlich, Dienste zu nutzen, die nur vielleicht mal zur Verfügung gestellt werden
                          Es kann ja ein Plugin geben und dafür wiederum eine Erweiterung. Dann hat der Kern der Anwendung keine Ahnung, dass es diesen Service irgendwann mal geben wird. Low-Level darf natürlich nie einen High-Level Service nutzen, das versteht sich ja von selbst.

                          Der Sicherheitsgedanke spielt natürlich auch eine Rolle. Sprich was passiert, wenn zwei Module den gleichen Service anbieten. Welcher wird verwendet? Kann man damit ggf. Schadcode einschleußen etc.


                          @xm

                          Den DI-Container von Symfony kann man doch auch einzeln nutzen

                          Kommentar


                          • #14
                            Hallo xm22,

                            or manually injected service
                            -->
                            Was, was gar nicht geht?
                            Entweder wir arbeiten mit DI oder eben nicht. Ebenso gefällt mir nicht, dass du in der Applikation eine Konfiguration manuell zum Container hinzufügen musst. Das sollte IMHO durch den Container und die Mechanismen des Frameworks schon selbst erledigt werden können. Innerhalb einer Komponente solltest du nur noch sagen, welchen Service du beziehen möchtest und das war's.
                            Viele Grüße,
                            Dr.E.

                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                            1. Think about software design [B]before[/B] you start to write code!
                            2. Discuss and review it together with [B]experts[/B]!
                            3. Choose [B]good[/B] tools (-> [URL="http://adventure-php-framework.org/Seite/088-Why-APF"]Adventure PHP Framework (APF)[/URL][URL="http://adventure-php-framework.org"][/URL])!
                            4. Write [I][B]clean and reusable[/B][/I] software only!
                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                            Kommentar


                            • #15
                              Entweder wir arbeiten mit DI oder eben nicht.
                              Ich verstehe jetzt nicht, was das eine mit dem anderen zu tun hat. In den DI-Containern, die mir bisher über den Weg gelaufen sind, hat man i. d. R. mehrere Möglichkeiten, Services oder Konfigurationen über mehrere Wege hinzuzufügen. Hier gibt es halt:
                              - Komplette Konfiguration
                              - Instanzen als fertige Services
                              - Einzelne Konfigurationssets für Services

                              Das sollte IMHO durch den Container und die Mechanismen des Frameworks schon selbst erledigt werden können.
                              Die Intention ist, dass das Ganze so flexibel wie möglich ist. Wenn der Container stand-alone laufen können soll, bringt ja eine automatische Zuweisung der Konfiguration nichts. Wenn man ihn jedoch in ein Framework integrieren will, ist es doch nur eine Detailfrage, wie man Konfigurationen dafür automatisch hinzufügt - Jeder Anwendungsfall unterscheidet sich und da ist es in meinen Augen nicht möglich, eine allgemeingültige Lösung einzubauen - Vor allem, weil man da dann eben wieder Flexibilität in der Benutzung einbüßen wurde.

                              Innerhalb einer Komponente solltest du nur noch sagen, welchen Service du beziehen möchtest und das war's.
                              Für mich sollte sich die wie auch immer geartete Komponente nicht dafür interessieren, wie die Services in den Container kommen oder in ihm generiert werden.


                              Ich habe mir gerade den DI-Service-Manager des APF angeschaut. Dessen Mechanismus zum Laden der Konfiguration ist halt direkt auf das APF zugeschnitten..

                              Kommentar

                              Lädt...
                              X