Handling managed objects

I often run into this problem.

I create a CApplicationComponent designed to manage a set of objects, typically models.

Example: a user activity tracking component, which needs to create user activity log entry models and save them.

My problem is, there is often some aspect of the application component’s configuration that the created models need to know about, or some feature that they need to invoke. That is, there is often a need for the created/managed objects to talk to their manager.

So in my managed object/model, I might do something like Yii::app()->manager->getSomething() to obtain a value from the manager.

The problem with this approach is, you need to know the name of the application component in the application, e.g. "manager" - but someone might configure the component with a different name.

To work around this, I attempted in init() to store a static reference to the application component itself - so that by calling a static method, getInstance(), I can get to the current instance.

However, sometimes you have a model/object before the manager has initialized, and then this approach fails.

Has anyone else run into this problem? How do you work around that?

Is pre-loading the component in the config file feasible and solve your problem?

It would solve the problem, but it defeats the purpose of having an application component in the first place - if it can’t autoload only when it’s actually used, rarely used application components (such as e-mail sender) will autoload on every request, adding unnecessary overhead.

Pre-loading should only be done for components which are aften used, like the ‘log’ component that comes with Yii.

A user activity logger is, in my opinion, a similar often used component and therefore it shouldn’t be a problem pre-loading it.

But an e-mail sender is not, so the problem remains :slight_smile:

Maybe you can exploit


CModule::getComponents

It only returns components that have already been loaded.

Even so, you would still need to know the name of the component to find it in that collection.

It’s a “chicken and egg” issue, I’m baffled as to how this issue can be resolved at all, even at the framework-level…

yes, this is a ‘chicken and egg’ issue.

Almost all application components are inherently singletons in the first place.

Having the named instance attached to the application object has a number of advantages, beyond the (perhaps rarely used) ability to create more than one instance of the same component, so I’m not suggesting we change the way this already works.

But could we have a second method that retrieves/initializes a component by it’s class-name, rather than by it’s instance name?

Should be easy enough to implement, e.g.:




private $_classes=array();


public function setComponents($components)

{

  foreach($components as $id=>$component)

  {

    if($component instanceof IApplicationComponent)

    {

      $this->setComponent($id,$component);

      $this->_classes[get_class($component)] = $id;

      continue;

    }

    else if(isset($this->_componentConfig[$id]))

    {

      unset($this->_classes[$this->_componentConfig[$id]['class']]);

      $this->_componentConfig[$id]=CMap::mergeArray($this->_componentConfig[$id],$component);

    }

    else

    {

      $this->_componentConfig[$id]=$component;

    }

    $this->_classes[$this->_componentConfig[$id]['class']] = $id;

  }

}


public function getComponentByClass($className,$createIfNull=true)

{

  if (isset($this->_classes[$className]))

    return $this->getComponent($this->_classes[$className], $createIfNull);

  else

    return null;

}



I didn’t test this, just a rough draft.

I did not take into account the fact that there may be more than one instance of the same application component - in that case, this would return the last registered instance. Perhaps better, if two instances of the same class are registered, $_classes[$id] should be set to false or null, so that only "singletons" (within the module or application) can be referenced using getComponentByClassName().

What do you think?

Some ideas I thought of are as follows:

What I look for is a type not a name (by hypothesis your manager is a singleton). So

Create an interface : IManager




interface IManager {

//nothing

}






YourClass extends... implements IManager {

}



Now when you get your components look for IManager.

Again, this would only work if you preload all managers.

If it is a singleton, then there is no need to register it with application because you can simply use ClassName::instance() to use the component.

Unfortunately that means your component can no longer be a CApplicationComponent, or take part in the natural life-cycle of a Yii application/module. For example, you can’t configure it via the Yii configuration files - which is highly impractical in situations where you need different configurations for different environments.

Furthermore, different modules may rely on different configurations of the same component - meaning it’s not a “true” singleton. While there may be only instance per application/module, other modules may have their own instance, with a different configuration.

An attachment or image manager is a good example - while you would need (or want) only one instance for an application, another module (say, a forum) may need it’s own attachment or image manager.

Perhaps another intermediary class is needed here? - a kind of CApplicationComponent that enforces that there be only one instance per application/module. CModule might have a new method that would create/return one of these per-module instances, perhaps by looking internally for a configured instance first, and if none if present, then proceed up the ladder to the parent module to see if there’s a configured instance there, and so on.

What do you think?

How about enforcing the users to use component class name as its ID, since it is like a "singleton"?

Trouble is, this relies on a convention rather than a standard - so it’s not much better (or really any different) than asking users to give their components a specific name.

Here’s another take on a way to find components by class-name:




<?php


abstract class CModule extends CComponent

{

  

  ...

  

  /**

   * @var array registry for getComponentByClass, where $className => $componentId

   */

  protected $_componentRegistry;

  

  /**

   * Obtain a component by it's class-name rather than it's component id.

   *

   * Note that this works only when there is a single instance of a given component

   * in the same application or module.

   *

   * @param $name the class-name of the component you wish to find

   * @return mixed the initialized component, or null if no component with the given class-name was found.

   */

  public function getComponentByClass($name)

  {

    if (!isset($this->_componentRegistry))

      $this->buildComponentRegistry();

    

    if (isset($this->_componentRegistry[$name]))

      return $this->getComponent($this->_componentRegistry[$name]);

    else

      return null;

  }

  

  /**

   * Builds the internal component registry, mapping class-names to component ids.

   */

  public function buildComponentRegistry()

  {

    $this->_componentRegistry = array();

    

    foreach ($this->_componentConfig as $id=>$config)

      if (array_key_exists($config['class'], $this->_componentRegistry))

        $this->_componentRegistry[$config['class']] = null;

      else

        $this->_componentRegistry[$config['class']] = $id;

    }

  }


}



Two important differences from my first idea:

  1. There is no runtime overhead, unless an application/component actually needs this functionality, since the classname => component-id registry is built the first time the function is called.

  2. Only components with a single instance will be registered - if you have two instances of the same application component in the same application or module, it cannot be found this way.

This is untested code still, just meant to demonstrate the idea.

One limitation of this implementation, is that only components that were configured using CModule::setComponents() can be found in this way. This could actually be seen as a benefit - the class-name registry is really only intended to find a particular type of component that has been configured for the application or module. If a component was assigned explicitly using CModule::setComponent(), the function that performed this call would already know the id it’s using, and thus would be able to find the component via it’s name.

Alternatively, here’s another (and much simpler) approach:

Simply make it convention, that all registered components are automatically registered both with their configured component id, and their class-name.

Once configured or registered, you could access the same component using Yii::app()->myManager or Yii::app()->CMyManager as you please.

The only potential problem with that idea, is that (as you suggested) some people may already be registering their components with the component’s class-name as it’s id. Of course this is only a problem if it’s not taken into account in the implementation.

I do not have a code sample for this idea, but let me know if you’d like to see one?