Writing a custom CakePHP console script

Published on and tagged with cakephp  console

As you may have noticed, last weekend all command line related files in CakePHP 1.2 have been moved from /cake/scripts to /cake/console. Coupled with this move was a change in the way you use the command line scripts. Instead of

php bake.php -app /path_to_app_dir

you now have to use

./cake bake -app /path_to_app_dir

to run the bake script. The ACL script can be used in a similar way.

This switch to the new console is probably the end of bake2, as it doesn’t (yet) work with the console, and the console provides a similar functionality as bake2, i.e. the console allows you to execute your own command line scripts. In the meantime the bake2 script has been removed from the repository. (Update 2007-05-15: The bake2 script has been removed from the repository)

Writing such a command line script (or shell scripts as they are called in CakePHP) is easy, so let us write a simple script which creates a file specified by the user. The custom scripts are placed in /app/vendors/shells, if they are specific to an application, or in /vendors/shells, if they are generic.

In our case it doesn’t matter where we place the file as it is only an example, so we put it to /vendors/shells. A shell class has to extend the class “Shell” and the class name has to end with “Shell”. All public functions of a shell class are treated as “commands”, i.e. they can be called directly via command line. What the “index” function is for controllers, is the “main” function for shell scripts. It is automatically called if you don’t specify a different command.

In our case we also have to override the “initialize” function, as we don’t have a database.php file in app/config (else we would get an error). The code for our shell script is straight-forward and should be self-explanatory:

// vendors/shells/demo.php
class DemoShell extends Shell {

    function initialize() {
        // empty
    }
		
    function main() {
        $this->out('Demo Script');
        $this->hr();
			
        if (count($this->args) === 0) {
            $filename = $this->in('Please enter the filename:');
        } else {
            $filename = $this->args[0];
        }
        $this->createFile(TMP.$filename, 'Test content');
    }
		
    function help() {
        $this->out('Here comes the help message');
    }
}

This script can now be executed in one of the following ways:

./cake demo  // asks the user for a file name
./cake demo test.txt   // uses the specified file name
./cake demo help   // shows the help message

Happy baking :)

Update (2007-05-13): Adapted to the latest changes in the code (CakeScript has been renamed to Shell)

45 comments baked

  • Brandon

    I smell a new package installer … CakeBox perhaps? :)

  • mazoo

    Hi Daniel, first, thanks a lot for your efforts!

    The idea with the demo script is nice. I would like to write such a script and use it from crontab, doing some periodic update/insert with my models. Now u mentioned the initalize function. Would it, if not overridden, connect to my database? And what approach should be used to work with Models (find,save,delete,…)?

    Good night! ;)
    mazoo

  • Mike

    Hi Daniel,

    that looks very promising! And I was wondering the same thing as mazoo. Maybe you could follow up with how one could work with db models? Please? :)

    Thanks for your blog!

  • nate

    Small correction to the way scripts are called. While it is valid and possible to call scripts like

    ./cake bake -app /path_to_app_dir

    as you have shown above, this defeats half the purpose of having a shell script wrapper. Instead, you can do the following:

    cd /path_to_app_dir
    ./cake bake

    That way, you can execute multiple commands without having to continually re-type your app path.

  • Darian

    Nate,

    I’m confused. The cake script lives at cake/scripts/cake. How would one call it from app/ as ./cake?

  • nate

    You add cake/console to your system path. (It was moved from cake/scripts to cake/console.)

  • cakebaker

    @all: Thanks for your comments!

    @mazoo, Mike: At the moment it seems that the functionality to access the db is broken due to a bug, see https://trac.cakephp.org/ticket/2544. When this bug is fixed, I will write a follow-up.

    @nate: Thanks for the correction.

  • Dieter@be

    How does it differentiate between function name and argument for the default function?
    in your example:
    [quote]
    ./cake demo test.txt // uses the specified file name
    ./cake demo help // shows the help message
    [/quote]
    what if “test.txt” was meant to be an action that needs to be called, or “help” the argument for the main function?
    Does cake search for all available functions first, and when it doesn’t find any match, it uses it as argument for the main function?

  • http://getopenid.com/carlosz

    Cool, any guess on the possibility to have an interactive shell like RoR has?

    This days i’ve been looking for one, since the one that comes with php5 really sucks. But this one works great: http://jan.kneschke.de/projects/php-shell

    maybe they could implement something like it. :)

  • cakebaker

    @Dieter: Yes, first it looks whether there is an action with the specified name. If there is no such action, the main action is used. To use a parameter in the main action with the same name as an action, you have to specify the action name explicitly:

    ./cake demo main help  // passes help as parameter to the main action
    ./cake demo help  // executes the help action
    

    @Carlos: I don’t know what features are planned for the console. If you think something could be useful in CakePHP, don’t hesitate to open an enhancement ticket on https://trac.cakephp.org

  • cakebaker » Shells and Tasks

    […] scripts are now called “Shells” instead of “CakeScripts”, so I adapted the previous article to those […]

  • mjk

    Hello Everybody,

    Could anybody please tell me if there is way where a console script could be used to access models. I am thinking of an approach where I could use something like “loadModel” in the console script file.

    Thanks in advance for the help! :)

  • cakebaker

    @mjk: The answer is already in the question ;-)

    Use something like:

    loadModel('MyModel');
    $myModel = new MyModel();
    // do something with the model
    

    HTH

  • mjk

    Thanks for the reply! But I think something is wrong with how I used the app_model as it creates an error when I load a model from the cake script:

    Fatal error: Class ‘CakeSession’ not found in c:\wamp\www\cake\src\apps\mjk\app_model.php on line 10

    that line corresponds to a line within the constructor in the app_model.php

    function __construct($id=false, $table=null, $ds=null)
    {
    // If the database was not specifically set, then use the value from the logged in user
    if ($this->useDbConfig == ‘default’)
    {
    $session = new CakeSession();
    $sessionVars = $session->__returnSessionVars();
    if (isset($sessionVars[‘Auth’][‘User’][‘databasename’]))
    {
    $this->useDbConfig = $sessionVars[‘Auth’][‘User’][‘databasename’];
    }
    else
    {
    // User may ask for models they do not have access to, just ignore request
    return;
    }
    }

    parent::__construct($id, $table, $ds);
    }

    … and line 10 is $session = new CakeSession();

    Do i need to call the Cake Session in the cake script? If so can anybody please help me how to do it?
    I have just been working with cakephp for 4 months now and I’m not yet attuned to working on the “deeper” parts of the framework. Thanks in advance. and more power to cakephp and the people who use it! :)

  • cakebaker

    @mjk: You can probably use the uses() function to load the CakeSession class:

    uses('session');
    

    But I am not sure whether there is a session available on the command line, probably not.

  • mjk

    Thanks a lot for the tip! I have dropped this part of my project for a while to move on to other things.

  • mjk

    thanks a lot for the tip. I have dropped this part of my project for the meantime. I’ll try to do as suggested when I come around it again. Thanks again.

  • cakebaker

    @mjk: Good luck :)

  • Matt McDonald’s Website » My del.icio.us bookmarks for December 10th through December 12th

    […] cakebaker » Writing a custom CakePHP console script – […]

  • ใช้ CakePHP จาก command line interface (terminal, console, etc.) « वीर
  • Matt Huggins

    I’d like to build a console script in Cake that sends emails (newsletters) to users via a cron job. In doing so, I’d like to create a newsletter view template. I see that there is a “View” shell task, but if I’m not mistaken, it looks like this is intended to be used for baking a new project, not for accessing views within a custom shell. So my question is, is there an easy way to access views from a shell console script?

  • cakebaker

    @Matt: The “View” shell task is for creating new views, not for accessing views from within a shell script.

    To access a view you could try something like:

    class DemoShell extends Shell {
        public function initialize() {
            // empty
        }
    		
        public function main() {
            App::import('Core', array('View', 'Controller'));
            $view = new View(new Controller());
            $viewContent = $view->render('/controller_name/view_name');
        }
    }

    Hope that helps!

  • Matt Huggins

    Ahh, right…I’m so used to using App::import for nothing other than vendor files, but I remember seeing this elsewhere now that you mention it. Thanks a bunch! :)

  • cakebaker

    @Matt: You are welcome!

  • calzone

    Aside from the cake manual, this is apparently one of only two pages on the internet that really discusses using the console for something other than baking.

    Unfortunately, I haven’t seen anyone actually describe how you can invoke and use your controllers from the console. The trouble I’m having is that if I do the following code, the controller action executes just fine, but in that action, there is a call to the model which uses findAll(). php throws “Fatal error: Call to a member function on a non-object .”

    Am I wrong in thinking the biggest selling point of the console is to be able to script and automate the running of your standard controller actions site-wide from behind the scenes? If I can’t invoke a model method from an imported controller, what’s the point of importing a controller at all since with precious few exceptions, virtually all controller actions use a model method? Why can I access the model and all it’s methods from the shell yet I can’t access those same methods from a controller that I’m trying to trigger from the same shell?

    Is the only way to accomplish this to re-write all my desired controller code as Shell code?

    <?php 
    class ReportShell extends Shell
    {
    	var $uses = array('City');
    	var $tasks = array('Project',
    				'DbConfig',
    				'Model',
    				'Controller',
    				'View',
    				'Plugin');
    
    	var $CitiesController = null;
    		
    	function initialize()
    	{
    		$this->_loadModels();
    	}
    
    	function main() 
    	{
    #		$city=$this->City->find(array('name'=>'Boston'));
    #		$this->out(pr($city));
    
    		App::import('Controller', 'Cities');
    		$this->CitiesController = new CitiesController();
    		$this->out($this->CitiesController->get_city('Massachusetts'));
    	}
    }
    ?>
  • cakebaker

    @calzone: Thanks for your comment!

    To avoid the “Call to a member function on a non-object .” error you have to initialize the controller after it is instantiated:

    $this->CitiesController = new CitiesController();
    $this->CitiesController->constructClasses();
    ...

    Hope that helps!

  • calzone

    Brilliant cakebaker!

    I’ve been chasing my tail over this one for a couple days now and I just knew it had to be some key element I was missing somewhere.

    Thank you so much, your help has been invaluable.

    –btw, happy holidays!

  • cakebaker

    @calzone: I’m glad I could help!

  • Ulises Figueroa

    I set up a Shell and a Task, Im using a Model to reach some data and send an email. But when I execute the shell I got the following error:

    Error: Missing Database Connection. Try ‘cake bake’

    This is the code for the Shell and Task

    class MemberShell extends Shell {
    var $tasks = array(‘NotifyNewMessage’);

    function main() {

    pr(‘Aqui va’);
    $this->NotifyNewMessage->execute();
    }
    }

    class NotifyNewMessageTask extends Shell {
    var $uses = array(‘Message’, ‘Member’);
    var $components = array (‘MemberMailer’);

    function execute() {
    //Reach data and send Email
    }
    }

  • Ulises Figueroa

    It works now, I changed the app directory when calling the script from shell.

    /home/user/public_html/website/cake/console/cake member -app /home/user/public_html/website/app

    But now it doesnt show up any message, how do I debugg the script from shell ?? Any ideas?

  • cakebaker

    @Ulises: Good to hear you could figure out your first issue in the meantime :)

    What do you mean with “it doesn’t show up any message”? Do you mean the output of the “pr” function is not shown? Is it possible that the debug level is set to 0? In that case the “pr” function doesn’t output anything by design (see cake/basics.php). Try to add the following snippet to your script:

    Configure::write('debug', 1);
    

    Hope that helps!

  • Ulises Figueroa

    @cakebaker: I mean that even though I have set the Debug Mode to 1 or 2 I doesnt shows up any message from pr() or echo. I’m not getting any error from Linux bash nor the CakePHP shell. :S

  • cakebaker

    @Ulises: Hm, no idea why it doesn’t work on your system… Do you get any output if you use $this->output(‘some text’);?

  • Gonzalo

    I can´t load any cotnroller, I just get this message:

    “Fatal error: Class ‘Controller’ not found in C:\wamp\www\integracion\app\app_controller.php on line 2′”

    the line 2 of my app_controller only have:
    class AppController extends Controller {

    my code is:

    class GiftShell extends Shell {
        var $uses = array('Player');
    	var $tasks = array('Project',
    				'DbConfig',
    				'Controller',
    				);
    
    	var $PlayersController = null;
    
        function initialize()
    	{
    		$this->_loadModels();
    	}
    
        function main() {
            App::import('Controller', 'Players');
    	$this->PlayersController = new PlayersController();
            $this->PlayersController->constructClasses();
         
    	}
    }
    ?>

    Please if somebody can help me :S

  • Gonzalo

    I tried too to do:

    $player = $this->requestAction('/players/getProfile/6');

    But I only get:
    Error: Missing Controller ‘Players’

    and my controller´s code is printed in the console

  • cakebaker

    @Gonzalo: To avoid this error you also have to import the controller class with:

    App::import('Core', 'Controller');
    

    Hope this helps!

  • Gonzalo

    It works! thanks :)

  • Frederick D.

    I am having a similar problem as Gonzalo, only with the message “Error: Missing Database Connection”. I have posted information about the issue here: http://groups.google.com/group/cake-php/browse_thread/thread/e54fa739b47cc61f#

    Your posts are impressive with the breadth of your knowledge of CakePHP. Would you please take a look at my posting above? Should I replace the $uses with an App::import statement? If so, what would that statement look like?

    I found this in the cook book at http://book.cakephp.org/view/531/Importing-Controllers-Models-Components-Behaviors-

    constructClasses();
    ?>

    Is this what I need to do in my shell? Thanks in advance for taking a look.

  • cakebaker

    @Frederick: I don’t think the error will disappear if you replace $uses with an App::import() statement, it will probably lead to the same error. But if you want, you can try the following:

    App::import('Model', 'YourModel');
    $myModel = ClassRegistry::init('YourModel');

    I guess the problem is, as the error message suggests, related to the database connection. So I would first check whether the settings in database.php are correct. Also make sure the mysql extension is enabled in the php.ini for the cli environment (some systems use one php.ini for Apache, and another php.ini for the cli).

    Hope that helps!

  • Frederick D.

    Thanks very much for your response. I will try that. I’ve never used those statements before. This all works swell on DreamHost, but not on HostGator. The web site itself works fine for the database connection. I’m having a problem with the cron job.

    With the App::import of the model, and the ClassRegistry, do I still reference the model in the same way or do I use the $myModel?

    I appreciate your help.

  • cakebaker

    @Frederick: You would use $myModel to access the model, though you could also use:

    $this->YourModel = ClassRegistry::init('YourModel');
    

    if you want to access the model in the usual way.

  • Frederick D.

    Awesome! It worked!

    Plus I went back and re-read the document about how to use SwiftMailer 4.x in CakePHP with shells. (http://bakery.cakephp.org/articles/view/updated-swiftmailer-4-xx-component-with-attachments-and-plugins)

    I had been reading multiple documents about SwiftMailer and shells and for some reason put the shell and task in /public_html/vendors. The article referenced above clearly said put it in /app/vendors/shells. Doing that, plus using the hints you gave me above, and the shell task works from SSH as well as in cron. The command I ended up with looks like this:

    php /home/USER NAME/public_html/cake/console/cake.php -app /home/USER NAME/public_html/app reminders lof

    Thank you VERY much for your help!

  • cakebaker

    @Frederick: Good to hear you could make it work :)

  • redthor

    That comment from Gonzalo and your reply just saved me from the
    PHP Fatal error: Class Controller not found error.

    thanks!

  • Exportar Sesion CakePHP

    […] por que no haces el script en Cake ? Writing a custom CakePHP console script – cakebaker visita mi blog […]

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License