This article is part of a longer series covering the n98-magerun power tool
Now that we’ve got a build environment up and running, we can get to work creating our first n98-magerun
command. Our end goal for today is to add a helloworld
command to n98-magrun
$ ./n98-magerun.phar list
//...
Available commands:
helloworld Displays a Hello World message. (pedagogical
Development Stub
So far, all our examples have used the n98-magerun.phar
bundled application. Therefore, our first step today is to run the application from source code. Every phar
archive can have an optional “stub” file. This file is meant to encourage clean coding practices in your phar
archive, with the stub acting as a main entry-point into your phar
based application. Think of it like you would your index.php
bootstrap file in a web application, or a main
function in a c
program.
The n98-magerun.phar
bootstrap file is _cli_stub.php
. You might think we could run this ourselves. However, running
$ php _cli_stub.php
will produce the error
PHP Fatal error: Uncaught exception 'PharException' with message 'internal corruption of phar
Let’s look at the contents of the phar
#!/usr/bin/env php
<?php
Phar::mapPhar('n98-magerun.phar');
$application = require_once 'phar://n98-magerun.phar/src/bootstrap.php';
$application->setPharMode(true);
$application->run();
__HALT_COMPILER();
It turns out that phar
stub files are meant to be used from phar
archives only. The call to mapPhar
, the require
using a phar://
url scheme, and the __HALT_COMPILER()
call all produce errors if used outside of a phar
archive.
We’re going to need to create our own stub file. In the root of your repository, create the following file with the following contents
<?php
$application = require_once realpath(dirname(__FILE__)) . '/src/bootstrap.php';
$application->setPharMode(false);
$application->run();
This is almost the same code that’s the the phar
stub, but written with generic PHP.
Let’s try running this from the command line.
$ php dev-stub.php
___ ___
_ _/ _ ( _ )___ _ __ __ _ __ _ ___ _ _ _ _ _ _
| ' _, / _ ___| ' / _` / _` / -_) '_| || | '
|_||_/_/___/ |_|_|___,___, ___|_| _,_|_||_|
|___/
n98-magerun version 1.63.0 by netz98 new media GmbH
Usage:
[options] command [arguments]
...
Eureka! We’re now running n98-magerun
strictly from code. You can make sure everything’s working correctly by navigating to your Magento folder and running a simple command like sys:info
.
$ cd /path/to/magento
$ php /path/to/n98-magerun/dev-stub.php sys:info
Magento System Information
Version : 1.7.0.1
Edition : Community
Cache Backend : Zend_Cache_Backend_File
Cache Directory : /Users/alanstorm/Sites2012/magento1point7pointzeropoint1.dev/var/cache
Session : files
Crypt Key : 18cea96f92f35a540d83a2fe7f33c005
Install Date : Sun, 01 Jul 2012 17:06:09 +0000
We’re now ready to start with our helloworld
command.
Hello World
To create a new n98-magerun
command there’s five basic steps
- Register the command
- Create the command class
-
Configure the command name
-
Implement the command
-
Build our new command into the
phar
By the end of the article, you’ll know how to do all five.
Registering the Command Class and Namespaces
Our first task is to register the command class. Open up the main application file and look at the registerCommands
method
#File: src/N98/Magento/Application.php
//...
class Application extends BaseApplication
{
//...
protected function registerCommands()
{
$this->add(new GenerateLocalXmlConfigCommand());
$this->add(new DatabaseDumpCommand());
$this->add(new DatabaseDropCommand());
//...
}
//...
}
The n98-magerun
team made use of Symfony’s console package as the basis for their application. A Symfony console application’s add
method allows client programmers to tell their application about a command, which in turn exposes it to end users of the console application.
A few of you may be wondering why this code uses such generic class names. The name Application
, and BaseApplication
seem like things that could be used in other code bases — so why did n98-magerun
choose Application
and why did Symfony
choose BaseApplication
?
The short answer is namespaces. If you look at the top of this file
#File: src/N98/Magento/Application.php
namespace N98Magento;
//...
you’ll see we’re working in the N98Magerun
namespace. That means this application class’s full name is actually
N98MagentoApplication
Similarly, further down in the file, you can see (nestled in among many similar calls)
use SymfonyComponentConsoleApplication as BaseApplication;
This aliases the class with the full name of
SymfonyComponentConsoleApplication
as the class BaseApplication
in this file.
PHP namespace programming can be confusing if you’re not used to it. Even if you are used to it, the lack of a gold standard in how to approach namespaces can leave you feeling as though you’re flailing about. While this isn’t necessarily a namespace tutorial, we’ll try to explain how and why each classes interacts in this brave new namespaced world.
The namespace detour completed, let’s add our command! Add the following line to the registerCommands
method.
#File: src/N98/Magento/Application.php
//...
class Application extends BaseApplication
{
//...
protected function registerCommands()
{
$this->add(
new HelloWorldCommand()
);
//...
}
}
What we’ve done here is pass in an object, instantiated from the class HelloWorldCommand
. Let’s run our command with this in place
$ php /path/to/n98-magerun/dev-stub.php
PHP Fatal error: Class 'N98MagentoHelloWorldCommand' not found
A fatal error! PHP is complaining it can’t find a class named HelloWorldCommand
. Actually, it’s complaining it can’t find a class named N98MagentoHelloWorldCommand
since we’re working in the N98Magento
namespace. Regardless, this make sense since we haven’t actually created our class yet. Let’s get to that.
Creating the Class
Each command in n98-magerun
corresponds to a single PHP class file. These files are, by convention, created in the src/N98/Magento/Command
folder.
If you list out the contents of this folder, you’ll see a number of classes and directories
$ ls -1 src/N98/Magento/Command
AbstractMagentoCommand.php
AbstractMagentoStoreConfigCommand.php
Admin
Cache
Cms
Config
ConfigurationLoader.php
Customer
Database
Design
Developer
Indexer
Installer
LocalConfig
MagentoConnect
OpenBrowserCommand.php
PHPUnit
ScriptCommand.php
SelfUpdateCommand.php
ShellCommand.php
System
We’re going to create a new class file in this folder. The n98-magerun
autoloader expects file names and class names to match, so create the following file at the following location
#File: src/N98/Magento/Command/HelloWorldCommand.php
<?php
namespace N98MagentoCommand;
use N98MagentoCommandAbstractMagentoCommand;
class HelloWorldCommand extends AbstractMagentoCommand
{
}
The first line of this file puts us in the
N98MagentoCommand
namespace. That means the full name for the HelloWorldCommand
class we’ve declared is actually N98MagentoCommandHelloWorldCommand
. You’ll notice the namespace convention matches the file path convention.
N98/Magento/Command/HelloWorldCommand.php
N98MagentoCommandHelloWorldCommand
Without getting too deeply into it, the autoloader depends on this, so make sure you stick to this convention.
Our HelloWorldCommand
class extends the AbstractMagentoCommand
class. This is the abstract class that all n98-magerun
commands inherit from. The full name of this class is actually
N98MagentoCommandAbstractMagentoCommand
We’re able to refer to it as AbstractMagentoCommand
since the following code was used at the top of our file
use N98MagentoCommandAbstractMagentoCommand;
If we run our application with the above class in place
$ php /path/to/dev-stub.php
Class 'N98MagentoHelloWorldCommand' not found
We’re still getting the same error. That’s because our register function is trying to load a class in the current namespace of the Application
file. If we change our add
method call so it uses the full class name
#File: src/N98/Magento/Application.php
//...
class Application extends BaseApplication
{
//...
protected function registerCommands()
{
$this->add(
new N98MagentoCommandHelloWorldCommand()
);
}
}
we should be good to go. Notice the leading \
on the class name. This tells PHP to start looking from the global namespace, (vs. looking for a deeper namespace, starting at the current one) .
If we run our stub with the above in place
$ php /path/to/dev-stub.php
PHP Fatal error: Uncaught exception 'LogicException' with message 'The command name cannot be empty.
Sort of Eureka! We’ve gotten rid of the class not found error, and replaced with with a new “the command name cannot be empty” error. That’s progress of a sort, and brings us to step 3, configure the command name.
Update: Alexander Menk got in touch to point out the project ships with a stub file that works in a PHP (vs. phar
) environment. We regret the error — feel free to substitute this file for your own.
Configuring the Command
Every command has a configure
method which is called automatically. This is where we’ll want to assign our command a name. Add the following method to your command class
#File: src/N98/Magento/Command/HelloWorldCommand.php
<?php
namespace N98MagentoCommand;
use N98MagentoCommandAbstractMagentoCommand;
class HelloWorldCommand extends AbstractMagentoCommand
{
protected function configure()
{
$this->setName('helloworld');
}
}
With the above in place, lets run our command again
$ php /path/to/dev-stub.php
___ ___
_ _/ _ ( _ )___ _ __ __ _ __ _ ___ _ _ _ _ _ _
| ' _, / _ ___| ' / _` / _` / -_) '_| || | '
|_||_/_/___/ |_|_|___,___, ___|_| _,_|_||_|
|___/
n98-magerun version 1.63.0 by netz98 new media GmbH
Usage:
[options] command [arguments]
Options:
--help -h Display this help message.
--quiet -q Do not output any message.
--verbose -v Increase verbosity of messages.
--version -V Display this application version.
--ansi Force ANSI output.
--no-ansi Disable ANSI output.
--no-interaction -n Do not ask any interactive question.
Available commands:
helloworld
help Displays help for a command
Finally! We’ve got a clean run. Also, if you’re playing close attention, you’ll notice that helloworld
is now listed as an available command.
Available commands:
helloworld
help Displays help for a command
If you want to give your command a description, just call the setDescription
method in configure
#File: src/N98/Magento/Command/HelloWorldCommand.php
protected function configure()
{
$this->setName('helloworld');
$this->setDescription('Displays a Hello World message. (pedagogical)');
}
Give the stub another run, and you should see your command description.
$ php /path/to/dev-stub.php
...
Available commands:
helloworld Displays a Hello World message. (pedagogical)
Notice that the text we included in the “ tag has been automatically made into a parenthetical.
Implementing our Command
We’ve got our stub running clean again — lets press our luck and try calling our helloworld
command.
$ php /path/to/dev-stub.php helloworld
[LogicException]
You must override the execute() method in the concrete command class.
Drats! At least we’ we’ve replaced our gross PHP errors with pretty Symfony errors.
Every command in a Symfony console application needs an execute
method. The execute
method is where we implement our command logic. To create this method, add the following to your HelloWorldCommand
class
#File: src/N98/Magento/Command/HelloWorldCommand.php
<?php
//...
class HelloWorldCommand extends AbstractMagentoCommand
{
//...
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln("Hello World!");
}
}
As you can see, we’ve added an execute
method to our class. The method has two arguments. The first, $input
, allows you to access arguments and options passed to your command. The second, $output
, allows you to send feedback to the users of your command. You can also see we’ve called the writeln
method of the output object to print out our ubiquitous Hello World! message. Let’s give the command a final run and call it a day.
$ php /path/to/dev-stub.php helloworld
PHP Catchable fatal error: Argument 1 passed to N98MagentoCommandHelloWorldCommand::execute() must be an instance of N98MagentoCommandInputInterface, instance of SymfonyComponentConsoleInputArgvInput given
Crud. That would have been too easy, wouldn’t it have? PHP is complaining that the first argument to execute
failed to match the type safety check. PHP was expecting a N98MagentoCommandInputInterface
, but was provided with a SymfonyComponentConsoleInputArgvInput
. Again, namespaces rear their hydra like heads.
The InputInterface
interface we want is actually SymfonyComponentConsoleInputInputInterface
. Instead of changing the type check in front of the parameter, let’s import SymfonyComponentConsoleInputInputInterface
and SymfonyComponentConsoleOutputOutputInterface
into the current namespace. Add the following lines just below the namespace
declaration.
#File: src/N98/Magento/Command/HelloWorldCommand.php
namespace N98MagentoCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
//...
With the above in place, we should be able to run our command.
$ php ~/Documents/github_netz98/n98-magerun/dev-stub.php helloworld
Hello World!
There we have it, another Hello World example program added to the world.
Following Conventions
Before we finish things off by compiling our new code into a phar
, let’s jump back to our add
method call.
#File: src/N98/Magento/Application.php
//...
$this->add(
new N98MagentoCommandHelloWorldCommand()
);
$this->add(new GenerateLocalXmlConfigCommand());
While this code works, it’s not keeping with the conventions setup by the netz98 team. When they add a command object, they’re not using the full qualified namespace, they’re just using simple class names like GenerateLocalXmlConfigCommand
. How do they get away with this?
If you search this file for GenerateLocalXmlConfigCommand
, you’ll find your answer. Near the top of the file is the following line
#File: src/N98/Magento/Application.php
use N98MagentoCommandLocalConfigGenerateCommand as GenerateLocalXmlConfigCommand;
Using the use
operator, the fully qualified N98MagentoCommandLocalConfigGenerateCommand
class is being imported into the local namespace as GenerateLocalXmlConfigCommand
. Let’s bring our class into the local namespace. Add the following right above the GenerateLocalXmlConfigCommand
line
#File: src/N98/Magento/Application.php
//our code
use N98MagentoCommandHelloWorldCommand;
//their code
use N98MagentoCommandLocalConfigGenerateCommand as GenerateLocalXmlConfigCommand;
and then change your call to add
$this->add(new HelloWorldCommand());
Your command should behave exactly the same, but now you’re matching the current coding conventions used in the project. Some people might write this off as bike shedding, but a consistant codebase helps with team cohesion, and allows new programmers a stable base to learn the fundamentals of a project without having to worry about parsing multiple different coding styles. This is particularly true in the PHP world, since "idiomatic PHP” varies from project to project.
Building a New phar
The only thing left is to build a new phar
archive. As we learned last time, our build script is just a single call to
$ phing
...
BUILD FINISHED
Total time: 8.2973 seconds away.
After running phing
, make sure a new phar
was created with today’s date and time (May XX XX:XX
below)
$ ls -l n98-magerun.phar
-rwxrwxr-x 1 alanstorm staff 3617497 May XX XX:XX n98-magerun.phar
Then, try running your command
$ ./n98-magerun.phar helloworld
Hello World!
Congratulations, you’ve just created and built your first n98-magerun
command.