5 Gotchas of the Bin File in PHP CLI Applications

This post focuses on bin files. It's the smallest part of PHP CLI Application, so I usually start with it.

Yet, there are still a few blind paths you can struggle with. I'll drop a few extra tricks to make your bin file clean and easy to maintain.

What is the Bin File?

The bin file is not a trash bin. It's a binary file, the entry point to your application the same way www/index.php is. You probably already use them:

1. Create it

The Name

The bin file should be named after the application, short and easy to type. So when I first released EasyCodingStandard, I used easy-coding-standard name. It was easy to remember, but when I had a talk I often miss-typed such a long name. After a while, I moved to ecs.

It should be also easy to remember and unique. Imagine that php-cs-fixer would be phpcf or phpcf. Since there is already phpcs taken, it might be trouble to remember. I think that's why the name is a little bit longer.

The Location

Where to put the file? Few people in the past put it in the root directory (only php-cs-fixer from 4 projects above have it that way). But the trend is to use bin directory. The same way index.php was moved to www/index.php or public/index.php.

bin/your-bin-file

2. Composer Autoload

With structure like this:

/bin/your-bind-file
/src
/vendor/autoload.php

The obvious code to add to /bin/your-bind-file is:

<?php declare(strict_types=1);

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

And that's it!


Not that fast. It might work for your www/index.php file in your application, but is that application ever installed as a dependency?

How do we cover autoload for a dependency? Imagine somebody would install your-vendor/your-package on his application. The file structure would look like this:

/src/
/vendor/autoload.php
/vendor/your-vendor/your-package/bin/your-bin-file

Now we need to get to /vendor/autoload.php of that application:

 <?php declare(strict_types=1);

-require_once  __DIR__ . '/../vendor/autoload.php';
+require_once  __DIR__ . '/../../../../vendor/autoload.php',

Great, people can use our package now. But it stopped working for our local repository. We'll probably have to seek for both of them:

<?php declare(strict_types=1);

$possibleAutoloadPaths = [
    // local dev repository
    __DIR__ . '/../vendor/autoload.php',
    // dependency
    __DIR__ . '/../../../../vendor/autoload.php',
];

foreach ($possibleAutoloadPaths as $possibleAutoloadPath) {
    if (file_exists($possibleAutoloadPath)) {
        require_once $possibleAutoloadPath;
        break;
    }
}

Comments are very important because this is very easy to get lost in. Trust me, I managed to fail a dozen times. Also, other people will appreciate it because it's WTF to see loading more than one vendor/autoload.php.

Imagine you'd move your package to a monorepo structure:

 $possibleAutoloadPaths = [
-    // local dev repository
+    // after split repository
     __DIR__ . '/../vendor/autoload.php',
     // dependency
     __DIR__ . '/../../../../vendor/autoload.php',
+    // monorepo
+    __DIR__ . '/../../../vendor/autoload.php',
 ];

Exceptionally Well Done

 <?php declare(strict_types=1);

 $possibleAutoloadPaths = [
     // local dev repository
     __DIR__ . '/../vendor/autoload.php',
     // dependency
     __DIR__ . '/../../../../vendor/autoload.php',
 ];

+$isAutoloadFound = false;
 foreach ($possibleAutoloadPaths as $possibleAutoloadPath) {
     if (file_exists($possibleAutoloadPath)) {
         require_once $possibleAutoloadPath;
+        $isAutoloadFound = true;
         break;
     }
 }
+
+if ($isAutoloadFound === false) {
+    throw new RuntimeException(sprintf(
+        'Unable to find "vendor/autoload.php" in "%s" paths.',
+        implode('", "', $possibleAutoloadPaths)
+    ));
+}

3. She Bangs

Since the bin file doesn't have a .php suffix by convention a system doesn't know, what language it's in. What happens when we run the bin file?

bin/your-bin-file

vendor/bin/your-bin-file: 1: vendor/bin/your-bin-file: Syntax error: "(" unexpected

Well, we know it's in PHP so run it with PHP:

php bin/your-bin-file

All works! But do we ever run this?

php composer

No, because we're lazy and we want to type as less as possible. How do we achieve the same effect for our file?

We add a shebang - a special line that will tell the system what interpret should be used:

#!/usr/bin/env php
<?php declare(strict_types=1);

// ...

It can be translated to:

/usr/bin/env php bin/your-bin-file

Try it. Does it work?

4. Free Access Rights

This allows to run the bin file on other people's computer:

chmod +x bin/your-bin-file

5. The Composer Symlink

If we install your package, we'll find the bin file here:

/vendor/your-vendor/your-package/bin/your-bin-file

But not in:

/vendor/bin/your-bin-file

Too bad. We're super lazy, so we want it there. How can we make it happen?

The Composer has special bin section, where we can define the symlink path for your bin file. Just add this to composer.json of your package:

{
    "bin": "bin/your-bin-file"
}

Tada! After we install such a package, we'll find it in the right place.

/vendor/bin/your-bin-file

Final Version

#!/usr/bin/env php
<?php declare(strict_types=1);

$possibleAutoloadPaths = [
     // local dev repository
     __DIR__ . '/../vendor/autoload.php',
     // dependency
     __DIR__ . '/../../../autoload.php',
];

foreach ($possibleAutoloadPaths as $possibleAutoloadPath) {
    if (file_exists($possibleAutoloadPath)) {
        require_once $possibleAutoloadPath;
        break;
    }
}

// your PHP code to run

$container = (new ContainerFactory)->create();
$application = $container->get(Application::class);
exit($application->run());

And that's it!


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!