Enough Composer To Be Dangerous

This article (and the accompanying talk) exist to teach you enough about Composer to get a few things done.

This topic was a talk I gave at DrupalCamp LA 2014: https://2014.drupalcampla.com/sessions/just-enough-composer-be-dangerous Unfortunately, the recording didn't work. But don't despair, you can come to PNW Drupal Summit 2014 and see it again: http://2014.pnwdrupalsummit.org/pacific-nw-drupal-summit-2014/sessions/enough-composer-be-dangerous

This is a beginner level article for Composer, but you'll need to be able to follow along in bash to see what's happening.

There are plenty of circumstances where Composer can help us manage our dependencies... I'm going to walk through making a project with only one dependency. And one dev dependency, but don't let that confuse you just yet.

Some technical requirements for this article:

  • Unix/bash: This article assumes you have a Unix-flavored bash shell environment. I'm not sure how to map any of this to Windows.
  • PHP: You should have PHP 5.3.2 or greater installed. I'm using a Mac running OS X 10.9, which comes with sufficient PHP. You should be able to open a terminal, type which php, and get a result.
  • curl: You should have curl. We only use it once, but it's kind of important.
  • Internet: You'll need a connection to the internet. Duh.
  • Editor: I'm using nano as a text editor in this article. It's easy to use, and easy for people to see what's going on in a presentation. Use emacs or vi or NetBeans or Coda or PHPStorm or whatever if you'd like.

Let's begin.

Some Definitions

Composer

Composer is a command-line tool to help you manage dependencies for PHP projects.

You need to add some frameworks to your project? Use Composer.

Composer also manages autoloading for the various dependencies, as well as for namespaces your project.

You can get it here: http://getcomposer.org/

Packages

The unit of measure for Composer is a 'package.'

Each package has a composer.json file which contains all the information Composer will use to manage the package and its dependencies.

Packagist.org

You can list your package on packagist.org. This site is where Composer looks to find packages you'll use in your project.

Install Composer

Composer installs as a single file which can be anywhere in your filesystem.

$ curl -sS https://getcomposer.org/installer | php

This downloads a file called composer.phar which you can run like this:

$ php ./composer.phar

We want a more global installation, so let's make a ~/bin directory and put Composer there.

$ mkdir ~/bin
$ mv composer.phar ~/bin/composer

bash still can't find it though, which we can prove by typing which composer.

Let's tell bash about our new path:

$ nano .profile

Add PATH=/Users/[user]/bin:$PATH to .profile. A similar line might already exist in your .profile file, and you'll have to figure out where to add your new path. Also, this is how you do it on Mac OS X; you'll need to learn how to add a directory to your search path on other systems.

$ source .profile

We have to tell bash to use this new .profile, and the source command is how we do it.

After we do all this, which composer should show us the location where we just put Composer.

Spec Our Project

Before we go much further we really should spec out our project. We always do that first, right? Design first, code second? Yes? :-)

We want a simple CLI tool that does this:

  • Says 'hello' back to us.

We'll use Symfony/Console for various reasons:

  • Easy to demo.
  • I repeat: Really easy to demo. We'll be using the GreetCommand source code from this page: http://symfony.com/doc/current/components/console/introduction.html
  • Allows us to demo PSR-4 autoloading for a single file.

So let's start:

$ mkdir ~/composer_demo
$ cd ~/composer_demo

Start Using Composer Already!

So let's prove to ourselves that the project directory really is empty:

$ ls -al

Let's add the console framework:

$ composer require symfony/console:~2.5

Watch Composer do its thing...

$ composer require symfony/console:~2.5
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/console (v2.5.3)
    Loading from cache

symfony/console suggests installing symfony/event-dispatcher ()
symfony/console suggests installing psr/log (For using the console logger)
Writing lock file
Generating autoload files

Look at directory contents. Not empty any more.

$ ls -al
total 16
drwxr-xr-x   5 paulmitchum  staff   170 Sep  1 12:37 .
drwxr-xr-x  23 paulmitchum  staff   782 Sep  1 10:47 ..
-rw-r--r--   1 paulmitchum  staff    61 Sep  1 12:37 composer.json
-rw-r--r--   1 paulmitchum  staff  2438 Sep  1 12:37 composer.lock
drwxr-xr-x   5 paulmitchum  staff   170 Sep  1 12:37 vendor 

What do all these files do?

composer.json

Look at composer.json:

$ more composer.json 
{
    "require": {
        "symfony/console": "~2.5"
    }
}

We see that Composer has added symfony/console as a requirement.

composer.lock

What's that composer.lock file?

$ more composer.lock

While it's working out all the web of dependencies for all the requirements, Composer collects a bunch of information. It stores that information in the lock file, so it doesn't have to look it all up again.

If you say composer update, it will work through the requirements gathering again.

If you say composer install, however, it will use the lock file in order to avoid having to re-discover the dependency tree.

vendor/

What's in the vendor/ directory?

$ cd ~/composer_demo/vendor
$ ls

You'll see autoload.php which is the second most important file in your Composer-based project. We'll talk about it later.

You'll see the composer directory, which is where Composer stores some stuff, such as the classmap autoload implementation.

You'll also see vendor directories. In our case, symfony/. If you drill down into symfony/ you'll see console/. Note that we required symfony/console, so the directory structure matches the vendor/package naming.

Packagist.org: Santa Claus For PHP

Where does Composer find these packages, such as symfony/console?

Packagist.org.

Here's the listing for symfony/console: https://packagist.org/packages/symfony/console

All of the packages on packagist.org are based on their composer.json file. There are some considerations for distributing a package this way, but that's outside the scope of this article.

Suffice it to say it's very easy to list your framework on Packagist.org, and it's also very easy to find what you need there, too.

Validating Our Composer.json File

Back to our project...

$ cd ~/composer_demo
$ composer validate

Composer barks at us for not having a name or license.

We could just edit composer.json, but let's get radical:

$ rm -rf *

For the purposes of this demo, deleting all your work is a good idea. It really isn't. :-)

$ composer init

This walks us through various options for our composer.json file.

Enter 'GPL-2.0+' for license. Specify stable for minimum stability.

Answer 'no' on entering our requirements and dev-requirements; we'll do that later.

Answer 'yes' to generate a new composer.json file.

$ composer validate

Now composer validate is happy with us.

Let's Make A Console App

Now we can do proper app coding. Note that if we were at all knowledgeable about Composer, all the preceding would have taken about just a few seconds.

Since we totally emptied the directory, we have to re-require Symfony Console:

$ composer require symfony/console:~2.5

Let's talk about the ~.

~2.5 means any version of the minimum stability, 2.5 or better. Even 2.6 or 3.0 or 10.0.1. You could also say ~2 to specify 2.0 or later.

2.5.* means any version of the minimum stability that fits the pattern, so 2.5.0 or 2.5.999, but not 2.6.

Anyway... Composer re-initializes console from its cache. Yes, it has a cache. You can poke around in it at ~/.composer, but save that for later.

Now let's make our top-level CLI application script:

$ nano hello.php

Here's what we'll put in that file:

<?php

use Symfony\Component\Console\Application;

$app = new Application('Hello, world!', '0.0.1-alpha');

$app->run();

This is just three lines of code, but it should show us a working CLI application, due to the power of the framework.

So let's try:

$ php hello.php

It doesn't work yet, does it? :-)

We use a namespaced class (Symfony\Component\Console\Application), but how will PHP know where to find it?

Good question...

Autoloading I: Loading Classes From The Framework

Now we have directory that looks like this:

.
├── composer.json
├── composer.lock
├── hello.php
└── vendor
    ├── autoload.php <-- NOTE THIS FILE
    ├── composer
    │   ├── ClassLoader.php
    │   ├── autoload_classmap.php
    │   ├── autoload_namespaces.php
    │   ├── autoload_psr4.php
    │   ├── autoload_real.php
    │   └── installed.json
    └── symfony
        └── console
            └── .....etc.....

Do you see ./vendor/autoload.php?

Let's require that into our CLI app:

$ nano hello.php

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Console\Application;

$app = new Application('Hello, world!', '0.0.1-alpha');

$app->run();

Now PHP can find Application, because Composer's autoloading capabilities have enabled that. If everyone plays by the same autoloading rules, Composer can sort it so you don't have to.

Let's try it and see if it works:

$ php hello.php
Hello, world! version 0.0.1-alpha

Usage:
  [options] command [arguments]
[.. etc ..]

Yay! Tentative, marginal, first-steps success!

If we poke around in vendor/autoload.php, we see that it's including a class file and then instantiating it. That class file is auto-generated when we update or install using Composer.

Autoloading, Part II: Loading Our Own Classes

This section is easily the most complicated part, and the easiest place to get tripped up. It's still pretty quick and easy once you get the hang of it though.

In the console framework, we have to define commands that the console can perform.

We'll do this by creating a class to represent our command. This class will be autoloaded into our project by Composer, using PSR-4.

What the heck is PSR-4? It's a strategy for mapping a PHP namespace to a directory hierarchy to enable autoloading. Keep reading... :-)

In The Application Script...

So to begin, let's modify our CLI application script to reflect what we hope to accomplish. We want to make a new instantiation of a class we'll call GreetCommand, and then add that to the application object.

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Mile23\Command\GreetCommand;
use Symfony\Component\Console\Application;

$app = new Application('Hello, world!', '0.0.1-alpha');
$app->add(new GreetCommand());
$app->run();

As you can see we've used the namespace Mile23\Command for our command class. We could have just used Mile23, but that wouldn't demo how to extend the namespace in the filesystem. There are some conventions for how to organize namespaces, but again... Out of scope for this article.

Add A Source File

Let's add a place to store our source files. This will be explained later. Just go with it, ok? :-)

$ mkdir src
$ mkdir src/Command

And let's add our command class file:

$ touch src/Command/GreetCommand.php

Let's edit the command class file:

$ nano src/Command/GreetCommand.php

<?php

namespace Mile23\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('demo:greet')
            ->setDescription('Greet someone')
            ->addArgument(
                'name',
                InputArgument::OPTIONAL,
                'Who do you want to greet?'
            )
            ->addOption(
               'yell',
               null,
               InputOption::VALUE_NONE,
               'If set, the task will yell in uppercase letters'
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input->getOption('yell')) {
            $text = strtoupper($text);
        }

        $output->writeln($text);
    }
}

This is basically a copy-paste of the GreetCommand from http://symfony.com/doc/current/components/console/introduction.html with the namespace changed to Mile23\Command.

See where it says use Symfony\Component\Console\Command\Command? We can say that in this file without loading vendor/autoload.php because we assume that such loading occurs in the app script. The effects of including that file will continue throughout the app runtime. This is one of the major benefits of having an ordered, Composer-based project.

Tell Composer About Our Namespace

So we have a command class in src/Command/GreetCommand.php, and we know we can have Composer autoload this for us. But how?

We get to edit composer.json. Within composer.json we say this:

    "autoload": {
        "psr-4": {
            "Mile23\\": "src/"
        }
    }

Why do we have two backslashes after Mile23? We have to include the backslash at the end of Mile23\ for autoloading to work properly. And then we have to escape the backslash, because that's how JSON works.

A Brief Explanation Of PSR-4

PSR-4 autoloading just means that if Composer encounters a namespace starting in Mile23\, it will look in our src directory for the rest of the namespace.

In our case, that's Mile23\Command\GreetCommand, so Composer looks in src/Command for GreetCommand.php.

If we had specified PSR-0, Composer would have looked for src/Mile23/Command/GreetCommand.php.

Choose whichever PSR system you prefer.

Regenerate The Classmap

OK... So we've told Composer about our namespace, so it should work right?

Wrong.

We still have to re-generate the autoload map:

$ composer dumpautoload

This will give Composer enough of a hint to find our new class. We have to do this whenever we change the 'autoload' section of our composer.json file. It also happens automatically when we install or update.

Did It Work??

So: The moment of truth:

$ php hello.php 
Hello, world! version 0.0.1-alpha

Usage:
  [options] command [arguments]

Options:
  --help           -h Display this help message.
  --quiet          -q Do not output any message.
  --verbose        -v|vv|vvv Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
  --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:
  help         Displays help for a command
  list         Lists commands
demo
  demo:greet   Greet someone

See where it says demo:greet? That shows us that our command class was loaded and instantiated. Yay! Success!

We can now show that we met our design requirements:

$ php hello.php demo:greet world
Hello world

Yes, we are geniuses. :-)

$ php hello.php demo:greet --yell world
HELLO WORLD

Wait, Let's Test It Please

Now we need some testing infrastructure. Let's add PHPUnit.

$ composer require --dev phpunit/phpunit:~3.7

Composer does some stuff and now we have PHPUnit.

Note the --dev argument. This specifies that PHPUnit is a requirement during development.

I'm not going to write a test here because the article is already too long. :-)

If we look at the composer.json file now, we see this:

    "require-dev": {
        "phpunit/phpunit": "~3.7"
    }

This looks exactly like the require block, but with -dev added, doesn't it?

We can now say the following:

$ composer install --no-dev
Loading composer repositories with package information
Installing dependencies from lock file
  - Removing phpunit/phpunit (3.7.37)
  - Removing phpunit/php-code-coverage (1.2.17)
  - Removing phpunit/php-file-iterator (1.3.4)
  - Removing phpunit/php-token-stream (1.3.0)
  - Removing phpunit/php-timer (1.0.5)
  - Removing phpunit/phpunit-mock-objects (1.2.3)
  - Removing phpunit/php-text-template (1.2.0)
  - Removing symfony/yaml (v2.5.3)
Generating autoload files

This tells Composer to do an installation without any of the development requirements, thanks to the --no-dev argument. Composer removes the packages we just told it to add, because we told it --no-dev.

By default, composer install and composer update will include dev requirements, but you can also explicitly require them by passing the --dev argument.

The point here is that when it comes time to deploy your PHP app, you can just say composer install --no-dev and Composer will do the work for you, without your testing infrastructure. Or whatever else you've relegated to dev status.

The End

Not really the end, but just the beginning.

There's ten zillion other things to know about composer, but this is enough to get you started.