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!
Have you find this post useful? Do you want more?
Follow me on Twitter, RSS or support me on GitHub Sponsors.