Building better project skeletons with Composer

The more you use modern frameworks and the more modular you build your PHP applications, the more likely you'll use a skeleton (or template) for creating new projects. In fact, most of the better known frameworks provide skeletons for you to bootstrap your application with.

Those skeletons are great to get started, but it's very likely you'll have your own stack of composer packages that you integrate in each project after a while. Each skeleton will be slightly different, so you'll likely fork your own. This article is meant to provide you with an understanding on how to build a skeleton that will allow you to automate things as far as possible.

Before We Start

I'll assume you've used composer before, and I'll also assume you have it installed in a way that calling "composer" in your command line will execute the composer commands.

I will use the word "project" a lot, based on the name of the composer command "create-project". A skeleton, however, can be for either a complete project, a library, a module/bundle or what ever else you can put in a composer package. Hell, you can probably even abuse a skeleton that, in the end, isn't a composer package. (I wouldn't recommend that.) The point is, when you read "project," try to see it as a blank where you fill in whatever form of a project you require it to be -- don't get put off by the choice of the word.

Please keep in mind that the intent of the code you see here is to show you some of the possibilities. You can replace the example scripts with a task in your Makefile (or any other build system you are using), a shell script, a much more complex PHP script -- whatever gets the job done.

Basics

What's a skeleton?

You probably know this already, but lets start with what is a skeleton and how to use it. A skeleton is a pre-made project that is copied to create a new project. It contains all you need to start a certain type of project.

How do you use a skeleton?

Lets start with the most basic way to use a skeleton. Zend Framework2 currently suggests using their skeleton for a new ZF2 project by using the following commands:

cd my/project/dir
git clone git://github.com/zendframework/ZendSkeletonApplication.git
cd ZendSkeletonApplication
php composer.phar install

Why is ZF2 doing it this way? Basically, they've included composer.phar in their skeleton, so you don't need to install composer yourself. That's probably okay when getting a new developer new to their framework started, but it comes with a few issues:

  1. You end up with my/project/dir/ZendSkeletonApplication, rather than with my/project/dir
  2. A possibly out-dated composer.phar
  3. A .git dir in your project referencing the skeletons git repository.
  4. You can't use a script on create-project, as create-project is not executed.
  5. The my/project/dir/ZendSkeletonApplication contains several files that belong to the skeleton project, but are not a skeleton to your project. In this example it's the README.md, but with other skeletons it might also be build files, rmt config, a changelog...

(Note: Funnily enough, they've covered a better way in their README.md).

How can we improve it?

Composer allows for a command named create-project. This command allows us to have Composer deal with doing the actual cloning (and cleaning up the .git) for us. All we need to do is:

composer create-project thecomposer/package my/project/dir

As I've used ZF2 for our example so far, here's the line that you'd need for ZF2 to work:

composer create-project -sdev zendframework/skeleton-application my/project/dir

Notice the added -sdev. It's an option that allows installs with a dev stability set. Why? because ZF2 hasn't tagged stable releases for their skeleton.

If you run those commands, the results will be basically the same as in the previous example. Composer will locate the source through the repositories that it knows, download the source, and run an "install" on them (by default with dev-dependencies enabled). At the end it will ask you "Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]?", something you should answer with Y(es), as it will then remove the VCS respository that links to the skeleton (allowing you to add your newly created project to its own repository).

So, lets have a look at which of the previous problems have been solved by doing it this way:

  1. Solved: We now have our project in my/project/dir
  2. Solved-ish: We aren't using the composer.phar from the skeleton, so it doesn't really matter.
  3. Solved: Composer asked us about the VCF file types, and removed them.
  4. Solved: We solved several of our problem by using create-project, and can do even better by using a script on create-project.
  5. Unsolved: We still have files that we don't need.

How do we get rid of unnecessary files?

This problem needs to be addressed in the skeleton package itself. As you'll end up with your own skeletons that contain your own stack of packages sooner or later, this is not an issue. (However if you're a framework dev, you might want to look into incorporating such cleanup in your skeleton as well).

Composer allows for what are called scripts. Scripts are defined in the root-packages composer.json and will be executed for certain events. In our case, we're going to use a script for the post-create-project-cmd event, which will clean up our own skeleton for us.

Our Own Skeleton

Prerequisites

Creating our own skeleton can be as easy as forking an existing one, or as complete as creating it from scratch. It usually depends on our use case, and for the sake of this, I'll just assume you have forked the ZF2 skeleton, maybe changed a few files, replaced the README.md (with one giving more information on your own skeleton) and added a CHANGELOG.md (for the skeleton).

I suggest creating a sub directory, for example called "skel" in your skeleton, which contains the cleanup script and a sub directory named templates.

The Cleanup Script (skel/postcreateproject.php)

This script is supposed to automate those little tasks that remain after running a create-project. It'll...

  • ...copy files from the templates directory over the files that are for the skeleton, thus replacing them with project-specific files.
  • ...replace placeholders in those files with values that make sense for the project.
  • ...delete the skel directory, removing all for-skeleton files left.

Note: Remember to set this file to executable chmod +x skel/post_create_project.php.

#!/usr/bin/php
<?php

// We get the project name from the name of the path that Composer created for us.
$projectname = basename(realpath("."));
echo "projectname $projectname taken from directory name\n";

// We could do more replaces to our templates here, 
// for the example we only do {{ projectname }}
$replaces = [
    "{{ projectname }}" => $projectname
];


// Process templates from skel/templates dir. Notice that we only use files that end 
// with -dist again. This makes sense in the context of this example, but depending on your 
// requirements you might want to do a more complex things here (like if you want 
// to replace files somewhere
// else than in the projects root directory
foreach (glob("skel/templates/{,.}*-dist", GLOB_BRACE) as $distfile) {

    $target = substr($distfile, 15, -5);

    // First we copy the dist file to its new location,
    // overwriting files we might already have there.
    echo "creating clean file ($target) from dist ($distfile)...\n";
    copy($distfile, $target);

    // Then we apply our replaces for within those templates.
    echo "applying variables to $target...\n";
    applyValues($target, $replaces);
}
echo "removing dist files\n";

// Then we drop the skel dir, as it contains skeleton stuff.
delTree("skel");

// We could also remove the composer.phar that the zend skeleton has here, 
// but a much better choice is to remove that one from our fork directly.

echo "\033[0;32mdist script done...\n";


/**
 * A method that will read a file, run a strtr to replace placeholders with
 * values from our replace array and write it back to the file.
 *
 * @param string $target the filename of the target
 * @param array $replaces the replaces to be applied to this target
 */
function applyValues($target, $replaces)
{
    file_put_contents(
        $target,
        strtr(
            file_get_contents($target),
            $replaces
        )
    );
}


/**
 * A simple recursive delTree method
 *
 * @param string $dir
 * @return bool
 */
function delTree($dir)
{
    $files = array_diff(scandir($dir), array('.', '..'));
    foreach ($files as $file) {
        (is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
    }
    return rmdir($dir);
}

exit(0);

The Skeleton's composer.json

The skeleton's composer.json should look something like this example:

{
    "name": "ourvendorname/skeleton-web",
    "description": "Web Skeleton Application for ZF2",
    "license": "proprietary",
    "keywords": [
    ],
    "require": {
        "php": ">=5.5.0",
        "zendframework/zendframework": "2.3.*",
        "zf-commons/zfc-twig": "1.2.*"
    },
    "require-dev": {
        "phpunit/phpunit": "4.1.*"
    },
    "scripts": {
        "post-create-project-cmd": [
            "skel/post_create_project.php"
        ]
    }
}

The important parts being:

  • name is set to our skeleton package's name.
  • require and require-dev contains dependencies for our project.
  • scripts contains a list of scripts to call on once create-project is done, in this case the list has a size of 1 and contains our skel/postcreateproject.php

The Templates

Within the skel/templates directory we can now place files that replace those that are skeleton-specific. In the case of the example that was given in the prerequisites, those would be:

  • An empty / or basic README.md-dist for the project.
  • An empty CHANGELOG.md-dist
  • A project specific composer.json-dist

The first two should be rather obvious, the last one might not be, as we already have a composer.json, that we called install from. We need this one for two reasons, to replace the package name with the project specific one, and to not include the postcreateproject in the project itself. For example, our skel/templates/composer.json-dist could look like this

{
    "name": "ourvendorname/{{ projectname }}",
    "description": "undescribed package",
    "license": "proprietary",
    "keywords": [
    ],
    "require": {
        "php": ">=5.5.0",
        "zendframework/zendframework": "2.3.*",
        "zf-commons/zfc-twig": "1.2.*"
    },
    "require-dev": {
        "phpunit/phpunit": "4.1.*"
    },
    "scripts": {
    }
}

The changes to our composer.json being:

  • The name has one of the placeholders that our script replaces.
  • The script is not contained in this, as this belongs only to the skeleton.

What's left to do?

Test Your Skeleton

We can test the whole thing simply by making a copy of our skeleton's folder (for example applied-skel), and manually have composer run the script within that directory.

cp -r skeleton-web applied-skel && cd applied-skel && composer run-script post-create-project-cmd

Then we can check if applied-skel contains all changes that we wanted to happen.

Once done, we can put the skeleton on packagist, or in whatever private repository we have, and use it.

Summary

You should now be able to create one or more skeletons that you can derive your projects from, without having to repeat the task of editing and deleting. Obviously this isn't the end of the road. You can extend the script to ask for user input and do more things based on that, or you could customize it further rather than using a replace on the composer.json-dist (don't forget to have your script run another Composer install then). In the end its up to you and your workflow.

0 comments


Or enter your name and Email
No comments have been posted yet.