LARAVEL PACKAGE: FOLDER STRUCTURE TEMPLATE

August 12th, 2019 by Kevin Pimentel

As I continue to do more Laravel package development it is becoming important to document the process. My goal is to produce a package template that I can reuse without having to go through the steps each time.


LazyElePHPant / mypackage


1) Create the folder structure


I like to keep the folder structure very close to Laravel. I take this approach since the packages I develop are specifically to extend the Laravel CMS that I have built. Maintaining a similar folder structure helps when others need to navigate the code. By providing a familiar folder structure you can get up and running much faster.


mypackage/
    src/
        Models/
        Controllers/
	       Requests/
        tests/
            Feature/
        resources/
            views/
        database/
            migrations/
    	    factories/
            seeds/
        routes/


2) Init Composer


You need composer for autoloading, managing any packages you may need, and to let Laravel know where the ServiceProvider is. I always include PHPUnit since it is a default for me. The rest of the json file is pretty standard composer.


{
    "name": "lazyelephpant/mypackage",
    "description": "Package boilerplate to speed up the package development process.",
    "type": "library",
    "authors": [
        {
            "name": "LazyElePHPant",
            "email": "kevin@kevinpimentel.com"
        }
    ],
    "require": {},
    "require-dev": {
      "phpunit/phpunit": "^8.3"
    },
    "autoload": {
      "psr-4": {
        "LazyElePHPant\\MyPackage\\":"src/"
      }
    },
    "extra": {
      "laravel": {
        "providers": [
          "LazyElePHPant\\MyPackage\\MyPackageServiceProvider"
        ],
        "aliases": {}
      }
    }
}


3) Service Provider


The MyPackageServiceProvider is what connects our mypackage package to Laravel. It enables us to load routes, views, migrations, seeds, factories, and even publish assets. 


You can find more information on service providers here


In our MyPackageServiceProvider.php we can use the boot and register methods to tell Laravel about our resources. You may even consider publishing a config file that is overridden by the main application using the package. Because my Laravel codebase is meant to be user friendly, I stay away from config files and instead opt to use settings stored in a table. This way a user that doesn't know a lot about coding can still store his Google Map pixel, or MailChimp site key, or whatever the case may be.


<?php

namespace LazyElePHPant\MyPackage;

use LazyElePHPant\Menu\Models\MyPackage;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Console\Output\ConsoleOutput;

class MyPackageServiceProvider extends ServiceProvider
{
    protected $seeds_path = '/Seeds';
    protected $seeds_path_from_parent = 'LazyElePHPant\\MyPackage\\Seeds\\';

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        $this->loadMigrationsFrom(__DIR__.'/database/migrations');
        $this->loadRoutesFrom(__DIR__.'/routes/web.php');
        $this->loadViewsFrom(__DIR__.'/resources/views', 'mypackage');

        $this->app->make('Illuminate\Database\Eloquent\Factory')->load(__DIR__ . 'database/factories');

        // if ($this->app->runningInConsole()) {
        //     if ($this->isConsoleCommandContains([ 'db:seed', '--seed' ], [ '--class', 'help', '-h' ])) {
        //         $this->addSeedsAfterConsoleCommandFinished();
        //     }
        // }
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
 $this->registerEloquentFactoriesFrom(__DIR__.'/database/factories');
    }

    /**
     * Get a value that indicates whether the current command in console
     * contains a string in the specified $fields.
     *
     * @param string|array $contain_options
     * @param string|array $exclude_options
     *
     * @return bool
     */
    protected function isConsoleCommandContains($contain_options, $exclude_options = null) : bool
    {
        $args = Request::server('argv', null);
        if (is_array($args)) {
            $command = implode(' ', $args);
            if (str_contains($command, $contain_options) && ($exclude_options == null || !str_contains($command, $exclude_options))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Add seeds from the $seed_path after the current command in console finished.
     */
    protected function addSeedsAfterConsoleCommandFinished()
    {
        Event::listen(CommandFinished::class, function (CommandFinished $event) {
            if ($event->output instanceof ConsoleOutput) {
                $this->addSeedsFrom(__DIR__ . $this->seeds_path);
            }
        });
    }

    /**
     * Register seeds.
     *
     * @param string  $seeds_path
     * @return void
     */
    protected function addSeedsFrom($seeds_path)
    {
        $file_names = glob( $seeds_path . '/*.php');

        foreach ($file_names as $filename) {
            $classes = $this->getClassesFromFile($filename);

            foreach ($classes as $class) {
                Artisan::call('db:seed', [ '--class' => $class, '--force' => '' ]);
            }
        }
    }

    /**
     * Register factories.
     *
     * @param  string  $path
     * @return void
     */
    protected function registerEloquentFactoriesFrom($path)
    {
        $this->app->make(EloquentFactory::class)->load($path);
    }

    /**
     * Get full class names declared in the specified file.
     *
     * @param string $filename
     * @return array an array of class names.
     */
    private function getClassesFromFile(string $filename) : array
    {
        $namespace = "";
        $lines = file($filename);
        $namespaceLines = preg_grep('/^namespace /', $lines);

        if (is_array($namespaceLines)) {
            $namespaceLine = array_shift($namespaceLines);
            $match = array();
            preg_match('/^namespace (.*);$/', $namespaceLine, $match);
            $namespace = array_pop($match);
        }

        $classes = array();
        $php_code = file_get_contents($filename);
        $tokens = token_get_all($php_code);
        $count = count($tokens);

        for ($i = 2; $i < $count; $i++) {
            if ($tokens[$i - 2][0] == T_CLASS && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
                $class_name = $tokens[$i][1];

                if ($namespace !== "") {
                    $classes[] = $this->seeds_path_from_parent . $class_name;
                } else {
                    $classes[] = $class_name;
                }
            }
        }

        return $classes;
    }
}


4) Routes


As I’ve written about before, it’s important to know that all routes need to have the web middleware applied to them. Otherwise, your routes will not bind, CSRF protection will not be enabled, and anything involving Laravel sessions will not be available to our routes.


Route::group(['middleware' => ['web']], function () {
	// Routes here ...
});


Conclusion


Building Laravel Packages is very convenient and easy way to reuse code. Having a template boilerplate like this one is a great way to jump start package development. This is my first iteration of this package template and I'll update it as needed.


To get started developing a package I add the following line to my require in composer.json


"lazyelephpant/mypackage": "dev-master"


I then create a brand new repositories section like this:


"repositories":[
 {
  "type":"path",
  "url":"../mypackage",
  "options":{
   "symlink":true
  }
 }
],


Finally, I run composer update. When the package is pulled in from the main application here's what that looks like in development:


$ composer update
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 0 installs, 1 update, 0 removals
 - Removing junction for lazyelephpant/mypackage (dev-master)
 - Installing lazyelephpant/mypackage (dev-master): Junctioning from ../mypackage
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: beyondcode/laravel-dump-server
Discovered Package: fideloper/proxy
Discovered Package: laravel/tinker
Discovered Package: lazyelephpant/mypackage
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.


That's it!


LazyElePHPant / mypackage


Kevin Pimentel

There are two types of people in the world: those that code, and those that don’t. I said that! Quote me. My name is Kevin and I’m one of the ones that codes.