Symfony provides a great infrastructure for writing console-oriented applications. But, as the lazy person I must confess I am, it feels like a great deal starting from scratch with a of a lot of code just for a "tiny" utility.
Well, as laziness is a great motivation to invest in potential time-saving actions for the future, I wrote a working skeleton for a Symfony CLI application. The thing can be downloaded from GitHub (just pick the last version tag) or clone the whole repository as usual.
You can run but you can't hide
The application entry point isrun.php
file with this contents:<?php // run.php /* * Run the application */ namespace YourNamespace; use Skel\DependencyInjection\Application; // the autoloader $loader = require __DIR__ . '/vendor/autoload.php'; // create application $application = new Application(__NAMESPACE__); // and run $application->run();(the YourNamespace namespace is intended to be replaced with your own across the application).
Nothing to customize here. Just run it with
php run.php
and it will work.It Depends
As you can see in the above example, the skeleton uses a Dependency Injection Container but, surprisingly as it may be for some, is not Pimple. Why? Because I wanted to keep things simple but not too simple. Pimple is great and easy to use, but Symfony Dependency Injection component offers, among a lot of other goodies, the ability to be configured via a configuration file (i.e.services.yml
).So we have a
config/services.yml
file with the following contents:# config/services.yml services: filesystem: class: Symfony\Component\Filesystem\Filesystem version: class: Skel\Lib\VersionYou can (and should) add any new dependency there as usual.
Application class
This class is pretty straightforward for the most part but has two remarkable pieces:<?php // src/Skel/DependencyInjection/Application.php /** * @param string $baseNamespaceName Highest level namespace for the application */ public function __construct($baseNamespaceName) { // our error handler ErrorHandler::register(); // create and populate the container $this->container = new ContainerBuilder(); // some useful paths $paths = array(); $paths['root'] = __DIR__ . '/../../../'; $paths['config'] = $paths['root'] . 'app/config/'; $paths['build'] = $paths['root'] . 'build/'; $this->container->setParameter('paths', $paths); // the main config $loader = new YamlFileLoader($this->container, new FileLocator($paths['config'])); $loader->load('config.yml'); // construct the application $app = $this->container->getParameter('application'); $version = $this->container->getParameter('version'); parent::__construct($app['name'],$version['current']); // and add commands to it $this->addConsoleCommands($baseNamespaceName); // may be we have some commands also $this->addConsoleCommands(__NAMESPACE__); }The configuration is read with:
<?php // src/Skel/DependencyInjection/Application.php // ... $loader = new YamlFileLoader($this->container, new FileLocator($paths['config'])); $loader->load('config.yml'); // ...And
config.yml
has the following contents:# config/config.yml imports: - { resource: build.yml } - { resource: services.yml } parameters: application: name: Symfony CLI skeleton slug: symcliskel # Used to name the built phar description: "Basic skeleton for building Symfony Command Line Interface (CLI) applications"First of all it imports files 'build.yml' (more on that later) and 'services.yml' that was covered before; then it defines a few useful parameters to be used in the application.
In 'Application.php' we also have method 'addConsoleCommands()' that does the magic for autoregistering the commands it finds in 'src/YourNamespace/Console/Command'.
<?php // src/Skel/DependencyInjection/Application.php /** * Adds all existing console commands * * @param string $baseNamespaceName Base namespace for the commands */ protected function addConsoleCommands($baseNamespaceName) { // get all namespaces from the composer autoload list $paths = $this->container->getParameter('paths'); $namespaces = include $paths['root'] . '/vendor/composer/autoload_namespaces.php'; // find the path for the namespace foreach ($namespaces as $namespace => $lookupPaths) { if ($namespace == $baseNamespaceName) { // add all existing commands foreach ($lookupPaths as $path) { $commandPath = $path . '/' . $namespace . '/Console/Command/'; if (is_dir($commandPath)) { $files = Finder::create()->files()->name('*Command.php')->in($path . '/' . $namespace . '/Console/Command/'); foreach ($files as $file) { $className = $file->getBasename('.php'); // strip .php extension // ignore Base*Command classes (base for commands) if (strpos($className, 'Base') === false) { $r = new \ReflectionClass($baseNamespaceName . '\Console\Command' . '\\' . $className); $this->add($r->newInstance()); } } } } break; } } }
Build it
Once you have your CLI application up and running it should be distributed to is users. The equivalent of a compiled application in PHP-land is a PHAR (PHP archive).As useful as PHAR's are, its creation is not so straigthforward as one could wish. No problem: the skeleton comes with its own PHAR builder:
<?php // build.php /* * Build the application */ namespace YourNamespace; use Skel\DependencyInjection\Application; use Skel\Lib\Version; use Symfony\Component\Yaml\Yaml; // the autoloader $loader = require __DIR__.'/vendor/autoload.php'; // create application and get the container $app = new Application(__NAMESPACE__); $container = $app->getContainer(); // get the version manager $vm = new Version('app/config/build.yml'); // start $pathsParam = $container->getParameter('paths'); $appParam = $container->getParameter('application'); $buildPath = $pathsParam['build']; $buildTarget = $appParam['slug'].'.phar'; $output = $buildPath.$buildTarget; echo sprintf('Building "%s" version "%s" into "%s"'."\n", $buildTarget, $vm->getVersion(), realpath($buildPath)); // remove old @unlink($output); // start phar creation $phar = new \Phar($output); $phar->startBuffering(); // the runner $phar->addFile('run.php'); // add other paths if needed $phar->buildFromDirectory(__DIR__, '/\/app\//'); $phar->buildFromDirectory(__DIR__, '/\/src\//'); $phar->buildFromDirectory(__DIR__, '/\/vendor\//'); // this will make the phar autoexecutable $defaultStub = $phar->createDefaultStub('run.php'); $stub = "#!/usr/bin/env php \n" . $defaultStub; $phar->setStub($stub); $phar->compressFiles(\Phar::GZ); $phar->stopBuffering(); // set execution rights chmod($output, 0755); // increment version $vm->incrementVersion(); echo "Finished!!\n";It packages the
app
, src
and vendor
together with run.php
to produce an standalone .phar
file that can be renamed or copied anywhere.Versions
build.php
updates the application version after the build, so the version that is shown when you run php run.php
and when you build the application with php build.php
are the same.Get it
All code is available in GitHub:https://github.com/magabriel/symfony-cli-skeleton
As usual, follow the instructions in the README document (do not forget to install vendors using
composer install
or composer update
).
No comments:
Post a Comment