How Laravel Auto Discover Package

Sky Chin
4 min readMay 4, 2018

In the last couple of weeks, I was learning to develop a package for Laravel application. This package is to help the developer develops onboarding flow inside their applications.

That package is not the subject of this article. Let’s move on to the main subject.

Package Auto-Discovery In Laravel 5.5

Package maintainers’ and developers’ job are easier since Package auto-discovery is introduced in Laravel 5.5.

Package developers will now be able to add a new section to their composer.json files that inform the framework of any service providers or facade aliases that should be registered.

Check out the pull request sent by Taylor Otwell to Laravel Debugbar by Barry vd. Heuvel

During the package development, these few questions kept repeating in my mind.

  • How does Laravel discover my package?
  • What is the information I can allow Laravel to discover? Service Provider, Facade, what else?
  • Why do I need “dont-discover”?

I couldn’t let go my curious mind. I decided to dive deep into Laravel framework.

How does it work?

After some time of diving in the framework, I figured it out. Laravel discovers the packages from vendor/installed.json.

What is installed.json?

installed.json is an internal file of Composer. It’s used when you remove a package manually from composer.json to remove the files from the vendor directory. Otherwise, the old vendor package would be around forever.

What is happening?

A PackageManifest is passing to PackageDiscoverCommand.

// vendor/laravel/framework/src/Illuminate/Foundation/Console/PackageDiscoverCommand.php
public function handle(PackageManifest $manifest)
{
$manifest->build();
foreach (array_keys($manifest->manifest) as $package) {
$this->line("<info>Discovered Package:</info> {$package}");
}
$this->info('Package manifest generated successfully.');
}

Inside PackageManifest’s build method, it read the installed.json from vendor path and convert the JSON string to an associative array.

// vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php
public function build()
{
...
if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) {
$packages = json_decode($this->files->get($path), true);
}
...
}

Then, it read the declared ignore packages array from the composer.json.

$ignoreAll is a boolean indicating that whether ignore all the discovered packages.
$ignore is an array that store all the packages to ignore.

// vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php
public function build()
{
...
$ignoreAll = in_array('*', $ignore = $this->packagesToIgnore());...
}
...protected function packagesToIgnore()
{
if (! file_exists($this->basePath.'/composer.json')) {
return [];
}
return json_decode(file_get_contents(
$this->basePath.'/composer.json'
), true)['extra']['laravel']['dont-discover'] ?? [];
}

To not discover any packages, you can set as below:

"extra": {
"laravel": {
"dont-discover": [
"*"
]
}
},

If you had ignored all the packages before, and if you hit the error below after you remove the “*” from the dont-discover array.

/ Terminal
php artisan package:discover
In Container.php line 767:

Class onboarding does not exist

You can resolve this error by deleting the bootstrap/cache/packages.php and execute the package:discover command again.

After that, a couple of steps to process the packages array before writing the result into a PHP file.

First, use the array to create a collection with necessary values and desired structure by using collect and mapWithKeys methods.

Second, iterate through the new collection and find more packages to ignore. This is very convenient when the time Laravel discovering your packages automatically, you have the control to manage which package to not being discovered by the framework.

Third, take out the ignore packages from the new collection by using reject method.

Lastly, return the filtered collection which contains all the discovered packages.

// vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php
public function build()
{
...
$this->write(collect($packages)->mapWithKeys(function ($package) {
return [$this->format($package['name']) => $package['extra']['laravel'] ?? []];
})->each(function ($configuration) use (&$ignore) {
$ignore = array_merge($ignore, $configuration['dont-discover'] ?? []);
})->reject(function ($configuration, $package) use ($ignore, $ignoreAll) {
return $ignoreAll || in_array($package, $ignore);
})->filter()->all());
}

The discovered packages are written to bootstrap/cache/packages.php.

// vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php
protected function write(array $manifest)
{
if (! is_writable(dirname($this->manifestPath))) {
throw new Exception('The '.dirname($this->manifestPath).' directory must be present and writable.');
}
$this->files->put(
$this->manifestPath, '<?php return '.var_export($manifest, true).';'
);
}

How to know it is written to bootstrap/cache/packages.php?

I spent some time to figure it out.

While registering the base bindings, a new instance of PackageManifest is created with the cached packages path.

// vendor/laravel/framework/src/Illuminate/Foundation/Application.php
protected function registerBaseBindings()
{
static::setInstance($this);
$this->instance('app', $this);$this->instance(Container::class, $this);$this->instance(PackageManifest::class, new PackageManifest(
new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));
}

public function getCachedPackagesPath()
{
return $this->bootstrapPath().'/cache/packages.php';
}
// bootstrap/cache/packages.php
<?php return array (
'fideloper/proxy' =>
array (
'providers' =>
array (
0 => 'Fideloper\\Proxy\\TrustedProxyServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'co0lsky/laravel-onboarding' =>
array (
'providers' =>
array (
0 => 'co0lsky\\Onboarding\\OnboardingServiceProvider',
),
'aliases' =>
array (
'onboarding' => 'co0lsky\\Onboarding\\OnboardingFacade',
),
),
);

It’s not done yet

Remember the three questions I was asking?

  • How does Laravel discover my package?
  • What is the information I can allow Laravel to discover? Service Provider, Facade, what else?
  • Why do I need “dont-discover”?

One is clear. Two to go. I will continue finding. Stay tuned.

Happy learning!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Sky Chin
Sky Chin

Written by Sky Chin

Engineering Manager @ Mindvalley

No responses yet

Write a response