Using components inside a Cake shell

You can easily include and use models inside cake shells/tasks as you would normally do inside a controller - via the $uses class attribute. But the cake shell doesn't give you the same functionality for loading components. So how do you load and use them inside shells/tasks?

Depending on the component you are loading, you can do a simple App::import():

class FooComponent extends Object {
    function doFoo(){
        echo 'foo';
    }
}

In your shell:
==

<br />
class CandyShell extends Shell {</p>

<p>    function main() {</p>

<p>        App::import('Component', 'Foo');</p>

<p>        $foo = new FooComponent();</p>

<p>        $foo-&gt;doFoo();</p>

<p>    }<br />
}<br />

==

There are some drawbacks for loading components this way, though:

  • - Component callbacks are not automatically executed (initialize, startup, etc). If you need it, you need to explicitly execute it.
  • - Components inside the component you are not loaded.

Both drawbacks suck but the latter sucks a bit harder since it could involve a rewrite of the component you are loading. It will be troublesome specially if there are several (and nested) dependencies among the components the require to be loaded.

I was faced with this problem a while ago and the idea of rewriting the component to use App::import for components did not appeal to me. Butt ugly - why do I have to rewrite and load it that way when I can use $components?

So I went on searching to find a solution but it always pointed me to the App::import() direction. Or maybe I wasn't searching hard enough? Or maybe it was just one of those days that I would like to stick my nose deeper inside things to see how it works? I decided to poke around some core files to see how components are being loaded inside controllers.

I won't go into details - but basically, there is Component attribute/member that takes care of the loading and initialization of components inside a controller. It is also responsible for initializing the components inside the component.

Let's say we need a Bar component inside Foo:

class FooComponent extends Object {
    var $components = array('Bar');

    // I just want to check if the initialize and startup  callbacks
    // are being triggered

    function initialize(&$controller){
        echo "Foo is initializing \n";
    }

    function startup(&$controller){
        echo "Foo Start Up \n";
    }


    function doFoo(){
        echo 'foo';

        $this->Bar->doBar();
    }
}
class BarComponent extends Object{

    // I just want to check if the initialize and startup  callbacks
    // are being triggered

    function initialize(&$controller){
        echo "Bar is initializing \n";
    }

    function startup(&$controller){
        echo "Bar Start Up \n";
    }

    function doBar(){
        echo "bar \n";
    }

}

Running CandyShell will give you an "Undefined property: FooComponent::$Bar" error. To remedy this, I modified CandyShell:

// Load the core controller and component class
App::import('Core', 'Controller');
App::import('Core', 'Component');

class CandyShell extends Shell {

    var $controller = null;
    var $Component = null;

    // Wouldn't it be fun if we can emulate the $components attribute
    // so we can use as if we were inside of a controller?
    var $components = array('Foo');

    // Override the shell's initialize callback
    function initialize() {

        // Do what initialize() used to do
        parent::initialize();


        // This is were the magic begins

        // Create dummy Component and Controller objects
        $this->Component = new Component();
        $this->controller = new Controller();

        // Attach the components attribute to the dummy controller
        $this->controller->components = $this->components;

        // Load the components by feeding it with the dummy controller
        // which contains our list components
        $this->Component->init($this->controller);


        // Since we are inside the initialize() callback of the shell,
        // I think it is appropriate that we fire the initialize callbacks
        // of the components
        $this->Component->initialize($this->controller);


        // Iterate through our shell's components list
        // and create a class member for each component
        // so we can access them just like
        // how components are used inside controllers
        // Note that the components are now members of our dummy controller
        foreach($this->components as $key => $val) {
            if (!is_numeric($key)) {
                $this->$key = $this->controller->$key;
            } else {
                $this->$val = $this->controller->$val;
            }
        }


    }

    // Override the shell's startup callback
    function startup(){

        // If you don't want to see CakePHP's console message
        // don't call the parent's startup();
        //parent::startup();


        // During shell's starup callback
        // trigger the components' callback method
        $this->Component->triggerCallback('startup', $this->controller);
    }


    function main() {
        // Will it run?
        $this->Foo->doFoo();
    }
}

I can declare a $components attribute and use it just like inside a controller. I can also use the components in the same way you would use them inside a controller. The components inside the component are also loaded and initialized. The components' (directly loaded using $components) fire their startup() callbacks. The startup() callback of the components inside the components don't fire, though. I guess they don't really get triggered since I tried using the component in an actual controller and it didn't trigger either.

So far, this solution worked for me. Once I am able able to get deeper into this or find faults and alternatives to it, I'll give the post an update.

 

Comments: 2

Leave a reply »

 
 
 

dude. you the man!!! genius. had me scratching my head forever. nice one.

 

Dear Rolan,

thanks so much for this helpfull post.
It seems to be for Cakephp 1.3 it doesn't work anymore in 2.X. But it was enough help to get it working again.

For all that read this blog post maybe the following change helps you out.
Iam still in integration so I didn't make sure it works completely yet.

function initialize() {

// Do what initialize() used to do
parent::initialize();

// This is were the magic begins

// Create dummy Component and Controller objects
$this->Component = new Component(new ComponentCollection());
$this->controller = new Controller();

// Attach the components attribute to the dummy controller
$this->controller->components = $this->components;

// Load the components by feeding it with the dummy controller
// which contains our list components
// $this->Component->init($this->controller);

// Since we are inside the initialize() callback of the shell,
// I think it is appropriate that we fire the initialize callbacks
// of the components
$this->Component->initialize($this->controller);

// Iterate through our shell's components list
// and create a class member for each component
// so we can access them just like
// how components are used inside controllers
// Note that the components are now members of our dummy controller
foreach($this->components as $key => $val) {
if (!is_numeric($key)) {
$this->$key = $this->controller->Components->load($key);
$this->$key->startup($this->controller);
} else {
$this->$val = $this->controller->Components->load($val);
$this->$val->startup($this->controller);
}
}
}

// Override the shell's startup callback
function startup(){

// If you don't want to see CakePHP's console message
// don't call the parent's startup();
//parent::startup();

// During shell's starup callback
// trigger the components' callback method
//$this->Component->startup();
//$this->Component->triggerCallback('startup', $this->controller);
}

 

Leave a Reply

 
(will not be published)
 
 
Comment
 
 

 

Resources