Composer will generate an autoload.php file in the /vendor directory after each installation or update. By including this single file, you’ll be able to access all classes provided by your installed libraries.
+
Looking at a Laravel project, you’ll see that the public/index.php file in the application root (which handles all incoming requests) requires the autoloader, which then makes all required libraries usable within the scope of your application. This includes Laravel’s first-party Illuminate components as well as any required third party packages.
In general (and by convention), a package contains a src/ (short for “source”) folder containing all package specific logic (classes) and a composer.json file containing information about the package itself. Additionally, most packages also include a license and documentation.
+
If we look at the general directory structure of a generic package, you’ll notice how it looks quite different from a standard Laravel project.
There's a big chance that you already have Composer installed. However, if you haven't installed Composer already, the quickest way to get up and running is by copying the script provided on the download page of Composer. By copying and pasting the provided script in your command line, the composer.phar installer will be downloaded, run, and removed again. You can verify a successful installation by running composer --version. To update Composer to the latest version, run composer self-update.
+
Package Skeleton
+
To start with developing a package, first, create an empty directory. It is not necessary to nest packages in an existing Laravel project. I would highly recommend organizing your packages separate from your (Laravel) projects for the sake of clarity.
+
For example, I store all packages in ~/packages/ and my Laravel apps live in ~/websites/.
+
Composer.json
+
Let's start by creating a composer.json file in the root of your package directory, having a minimal configuration (as shown below). Replace all details from the example with your own.
+
It is best to be consistent with naming your packages. The standard convention is to use your GitHub / Gitlab / Bitbucket / etc.` username followed by a forward-slash ("/") and then a kebab cased version of your package name.
Alternatively, you can create your composer.json file by running composer init in your empty package directory.
+
If you're planning to publish the package, it is important to choose an appropriate package type (in our case, a "library") and license (e.g., "MIT"). Learn more about open source licenses at ChooseALicense.com.
+
Namespacing
+
Since we want to use the (conventional) src/ directory to store our code, we need to tell Composer to map the package's namespace to that specific directory when creating the autoloader (vendor/autoload.php).
+
We can register our namespace under the "psr-4" autoload key in the composer.json file as follows (replace the namespace with your own):
Now, you might wonder why we needed a "psr-4" key. PSR stands for PHP Standards Recommendations devised by the PHP Framework Interoperability Group (PHP-FIG). This group of 20 members, representing a cross-section of the PHP community, proposed a series of PSR's.
+
In the list, PSR-4 represents a recommendation regarding autoloading classes from file paths, replacing the until then prevailing PSR-0 autoloading standard.
+
The significant difference between PSR-0 and PSR-4 is that PSR-4 allows to map a base directory to a particular namespace and therefore permits shorter namespaces. I think this comment on StackOverflow has a clear description of how PSR-0 and PSR-4 work.
Looking for Book\History\UnitedStates in src/History/UnitedStates.php
+
+
+
Looking for Vehicle\Air\Wings\Airplane in src/Air/Wings/Airplane.php
+
+
+
Importing the Package Locally
+
To help with development, you can require a local package in a local Laravel project.
+
If you have a local Laravel project, you can require your package locally by defining a custom so-called "repository" in the composer.json file of your Laravel application.
+
Add the following "repositories" key below the "scripts" section in composer.json file of your Laravel app (replace the "url" with the directory where your package lives):
You can now require your local package in the Laravel application using your chosen namespace of the package. Following our example, this would be:
+
composerrequirejohndoe/blogpackage
+
+
Note: This documentation assumes you are using Laravel 8.0. If you are using Laravel 10, the above command will throw a minimum stability constraint error. You can fix the issue by adding the version key below the name key in the composer.json file of the package.
+
composer.json
{
+"version":"1.0.0",
+}
+
+
By default, the package is added under vendor folder as a symlink if possible. If you would like to make a physical copy instead (i.e. mirroring), add the field "symlink": false to the repository definition's options property:
If you have multiple packages in the same directory and want to instruct Composer to look for all of them, you can list the package location by using a wildcard * as follows:
Important: you will need to perform a composer update in your Laravel application whenever you make changes to the composer.json file of your package or any providers it registers.
+
Orchestra Testbench
+
We now have a composer.json file and an empty src/ directory. However, we don't have access to any Laravel specific functionality provided by the Illuminate components.
+
To use these components in our package, we'll require the Orchestra Testbench. Note that each version of the Laravel framework has a corresponding version of Orchestra Testbench. In this section, I'll assume we're developing a package for Laravel 8.0, which is the latest version at the moment of writing this section.
+
composerrequire--dev"orchestra/testbench=^6.0"
+
+
The full compatibility table of the Orchestra Testbench is shown below, taken from the original documentation.
+
+
+
+
Laravel
+
Testbench
+
+
+
+
+
11.x
+
9.x
+
+
+
10.x
+
8.x
+
+
+
9.x
+
7.x
+
+
+
8.x
+
6.x
+
+
+
7.x
+
5.x
+
+
+
6.x
+
4.x
+
+
+
5.x.x
+
3.x.x
+
+
+
+
With Orchestra Testbench installed, you'll find a vendor/orchestra/testbench-core directory, containing a laravel and src directory. The laravel directory resembles the structure of an actual Laravel application, and the src directory provides the Laravel helpers that involve interaction with the project's directory structure (for example, related to file manipulation).
+
Before each test, TestBench creates a testing environment including a fully booted (test) application. If we use the Orchestra TestBench's basic TestCase for our tests, the methods as provided by the CreatesApplication trait in the Orchestra\Testbench\Concerns namespace will be responsible for creating this test application. If we look at one of these methods, getBasePath(), we'll see it directly points to the laravel folder that comes with Orchestra Testbench.
An essential part of a package is its Service Provider. Before creating our own, I'll explain what service providers are about in this section first. If you are familiar with service providers, please continue to the next section.
+
As you might know, Laravel comes with a series of service providers, namely the AppServiceProvider, AuthServiceProvider, BroadcastServiceProvider, EventServiceProvider and RouteServiceProvider. These providers take care of "bootstrapping" (or "registering") application-specific services (as service container bindings), event listeners, middleware, and routes.
+
Every service provider extends the Illuminate\Support\ServiceProvider and implements a register() and a boot() method.
+
The boot() method is used to bind things in the service container. After all other service providers have been registered (i.e., all register() methods of all service providers were called, including third-party packages), Laravel will call the boot() method on all service providers.
+
In the register() method, you might register a class binding in the service container, enabling a class to be resolved from the container. However, sometimes you will need to reference another class, in which case the boot() method can be used.
+
Here is an example of how a service provider may look and which things you might implement in a register() and boot() method.
+
<?php
+
+useApp\Calculator;
+useIlluminate\Support\Collection;
+useIlluminate\Support\Facades\Gate;
+useIlluminate\Support\ServiceProvider;
+
+classAppServiceProviderextendsServiceProvider
+{
+publicfunctionregister()
+{
+// Register a class in the service container
+$this->app->bind('calculator',function($app){
+returnnewCalculator();
+});
+}
+
+publicfunctionboot()
+{
+// Register a macro, extending the Illuminate\Collection class
+Collection::macro('rejectEmptyFields',function(){
+return$this->reject(function($entry){
+return$entry===null;
+});
+});
+
+// Register an authorization policy
+Gate::define('delete-post',function($user,$post){
+return$user->is($post->author);
+});
+}
+}
+
+
Creating a Service Provider
+
We will create a service provider for our package, which contains specific information about our package's core. The package might use a config file, maybe some views, routes, controllers, database migrations, model factories, custom commands, etc. The service provider needs to register them. We will discuss each of these in subsequent chapters.
+
Since we've pulled in Orchestra Testbench, we can extend the Illuminate\Support\ServiceProvider and create our service provider in the src/ directory as shown (replace naming with your details):
To automatically register it with a Laravel project using Laravel's package auto-discovery we add our service provider to the "extra"> "laravel"> "providers" key in our package's composer.json:
Now, whenever someone includes our package, the service provider will be loaded, and everything we've registered will be available in the application. Now let's see what we might want to register in this service provider.
+
Important: this feature is available starting from Laravel 5.5. With version 5.4 or below, you must register your service providers manually in the providers section of the config/app.php configuration file in your laravel project.
+
config/app.php
<?php
+
+'providers'=>[
+// Other Service Providers
+
+App\Providers\ComposerServiceProvider::class,
+],
+
It is essential to have proper test coverage for the package's provided code. Adding tests to our package can confirm the existing code's behavior, verify everything still works whenever adding new functionality, and ensure we can safely refactor our package with confidence at a later stage.
+
Additionally, having good code coverage can motivate potential contributors by giving them more confidence that their addition does not break something else in the package. Tests also allow other developers to understand how specific features of your package are to be used and give them confidence about your package's reliability.
+
Installing PHPUnit
+
There are many options to test behavior in PHP, Laravel is built with testing in mind and provides support for testing with Pest and PHPUnit. We can install PHPUnit, which is the default testing framework for Laravel.
+
Install PHPUnit as a dev-dependency in our package:
+
composerrequire--devphpunit/phpunit
+
+
Installing Pest
+
Alternatively, we can use Pest as our testing framework. It's important to note that Pest is built on top of PHPUnit, which means that all the options offered by PHPUnit can also be used in Pest. Therefore, our configurations for PHPUnit will also apply to Pest tests.
+
Install Pest as a dev-dependency in our package and initialize it:
The --init flag will create a tests directory with example tests, a TestCase.php file and a Pest.php file. It will also create a phpunit.xml file with the necessary configurations for Pest, this saves us from having to create these files manually.
+
In the Pest.php file, we can uncomment the uses method and add the TestCase class to the uses method. This will allow us to use the TestCase class in our tests.
Note: you might need to install a specific version if you're developing a package for an older version of Laravel. Also to install orchestra/testbench, please refer to Orchestra Testbench set up on the Development Environment page.
+
To configure PHPUnit, create a phpunit.xml file in the root directory of the package.
+Then, copy the following template to use an in-memory sqlite database and enable colorful reporting.
Note the dummy APP_KEY in the example above. This environment variable is consumed by Laravel's encrypter, which your tests might be making use of. For most cases, the dummy value will be sufficient. However, you are free to either change this value to reflect an actual app key (of your Laravel application) or leave it off entirely if your test suite does not interact with the encrypter.
+
Directory Structure
+
To accommodate Feature and Unit tests, create a tests/ directory with a Unit and Feature subdirectory and a base TestCase.php file. The structure looks as follows:
+
- tests
+ - Feature
+ - Unit
+ TestCase.php
+
+
The TestCase.php extends \Orchestra\Testbench\TestCase (see example below) and contains tasks related to setting up our “world” before each test is executed. In the TestCase class we will implement three important set-up methods: setUp(), getEnvironmentSetUp() and getPackageProviders().
+
Let's look at these methods one by one:
+
+
+
setUp(): You might have already used this method in your tests. Often it is used when you need a certain model in all following tests. The instantiation of that model can therefore be extracted to a setUp() method which is called before each test. Within the tests, the desired model can be retrieved from the Test class instance variable. When using this method, don't forget to call the parent setUp() method (and make sure to return void).
+
+
+
getEnvironmentSetUp(): As suggested by Orchestra Testbench: "If you need to add something early in the application bootstrapping process, you could use the getEnvironmentSetUp() method". Therefore, I suggest it is called before the setUp() method(s).
+
+
+
getPackageProviders(): As the name suggests, we can load our service provider(s) within the getPackageProviders() method. We'll do that by returning an array containing all providers. For now, we'll just include the package specific package provider, but imagine that if the package uses an EventServiceProvider, we would also register it here.
+
+
+
+
In a package, TestCase will inherit from the Orchestra Testbench TestCase:
Before we can run the PHPUnit test suite, we first need to map our testing namespace to the appropriate folder in the composer.json file under an "autoload-dev" (psr-4) key:
Finally, re-render the autoload file by running composer dump-autoload.
+
Authentication
+
In some cases you might want to use Laravel's User::class to be able to use an authenticated user in your tests.
+There are several approaches, as discussed in the Models related to App\User section. However, if you don't have any relationships with the User model, and only want to test authentication logic, the easiest option is to create your own User class, extending the Illuminate\Foundation\Auth\User class:
After defining this custom User model within your package, you should execute the migrate command from the Orchestra package to create the users table in your test database:
The word 'facade' refers to a "superficial appearance or illusion of something," according to Dictionary.com. In architecture, the term refers to the front of a building.
+
A facade in Laravel is a class that redirects static method calls to the dynamic methods of an underlying class. A facade's goal is to provide a memorable and expressive syntax to access an underlying class's functionality.
To learn more about facades and how they work, refer to the excellent Laravel documentation.
+
Practically, it boils down to calling static methods on a Facade, which are "proxied" (redirected) to the non-static methods of an underlying class you have specified. This means that you're not actually using static methods. An example is discussed below, using a Calculator class as an example.
+
Creating a Facade
+
Let’s assume that we provide a Calculator class as part of our package and want to make this class available as a facade.
+
First create a Calculator.php file in the src/ directory. To keep things simple, the calculator provides an add(), subtract() and clear() method. All methods return the object itself allowing for a fluent API (chaining the method calls, like: ->add()->subtract()->subtract()->getResult()).
The end user can now use the Calculator facade after importing it from the appropriate namespace: use JohnDoe\BlogPackage\Facades\Calculator;. However, Laravel allows us to register an alias that can register a facade in the root namespace. We can define our alias under an “alias” key below the “providers” in the composer.json file:
Important: this feature is available starting from Laravel 5.5. With version 5.4 or below, you must register your facades manually in the aliases section of the config/app.php configuration file.
+
You can also load an alias from a Service Provider (or anywhere else) by using the AliasLoader singleton class:
Laravel ships with an executable artisan file, which offers a number of helpful commands through a command-line interface (CLI).
+
Via this CLI, you can access commands as php artisan migrate and php artisan make:model Post. There are a lot of things you could do with commands. Make sure to read up on the artisan console in the Laravel documentation.
+
Let's say that we want to provide an easy artisan command for our end user to publish the config file, via: php artisan blogpackage:install.
+
Creating a new Command
+
Create a new Console folder in the src/ directory and create a new file named InstallBlogPackage.php. This class will extend Laravel's Command class and provide a $signature (the command) and a $description property. In the handle() method, we specify what our command will do. In this case we provide some feedback that we're "installing" the package, and we'll call another artisan command to publish the config file. Using the File facade we can check if the configuration file already exists. If so, we'll ask if we should overwrite it or cancel publishing of the config file. Finally, we let the user know that we're done.
We need to present this package functionality to the end-user, thus registering it in the package's service provider.
+
Since we only want to provide this functionality when used from the command-line we'll add it within a conditional which checks if the application instance is running in the console:
+
BlogPackageServiceProvider.php
<?php
+
+useJohnDoe\BlogPackage\Console\InstallBlogPackage;
+
+publicfunctionboot()
+{
+// Register the command if we are using the application via the CLI
+if($this->app->runningInConsole()){
+$this->commands([
+InstallBlogPackage::class,
+]);
+}
+}
+
+
Scheduling a Command in the Service Provider
+
If you want to schedule a command from your package instead of app/Console/Kernel.php, inside your service provider, you need to wait until after the Application has booted and the Schedule instance has been defined:
+
BlogPackageServiceProvider.php
<?php
+
+useIlluminate\Console\Scheduling\Schedule;
+
+publicfunctionboot()
+{
+// Schedule the command if we are using the application via the CLI
+if($this->app->runningInConsole()){
+$this->app->booted(function(){
+$schedule=$this->app->make(Schedule::class);
+$schedule->command('some:command')->everyMinute();
+});
+}
+}
+
+
Testing a Command
+
To test that our Command class works, let's create a new unit test called InstallBlogPackageTest.php in the Unit test folder.
+
Since we're using Orchestra Testbench, we have a config folder at config_path() containing every file a typical Laravel installation would have. (You can check where this directory lives yourself if you dd(config_path())). Therefore, we can easily assert that this directory should have our blogpackage.php config file after running our artisan command. To ensure we're starting clean, let's delete any remainder configuration file from the previous test first.
+
tests/Unit/InstallBlogPackageTest.php
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Unit;
+
+useIlluminate\Support\Facades\Artisan;
+useIlluminate\Support\Facades\File;
+useJohnDoe\BlogPackage\Tests\TestCase;
+
+classInstallBlogPackageTestextendsTestCase
+{
+/** @test */
+functionthe_install_command_copies_the_configuration()
+{
+// make sure we're starting from a clean state
+if(File::exists(config_path('blogpackage.php'))){
+unlink(config_path('blogpackage.php'));
+}
+
+$this->assertFalse(File::exists(config_path('blogpackage.php')));
+
+Artisan::call('blogpackage:install');
+
+$this->assertTrue(File::exists(config_path('blogpackage.php')));
+}
+}
+
+
In addition to the basic test which asserts that a configuration file is present after installation, we can add several tests which assert the appropriate installation process of our package. Let's add tests for the other scenarios where the user already has a configuration with the name blogpackage.php published. We will utilize the assertions expectsQuestion, expectsOutput, doesntExpectOutput, and assertExitCode.
+
tests/Unit/InstallBlogPackageTest.php
<?php
+
+/** @test */
+publicfunctionwhen_a_config_file_is_present_users_can_choose_to_not_overwrite_it()
+{
+// Given we have already have an existing config file
+File::put(config_path('blogpackage.php'),'test contents');
+$this->assertTrue(File::exists(config_path('blogpackage.php')));
+
+// When we run the install command
+$command=$this->artisan('blogpackage:install');
+
+// We expect a warning that our configuration file exists
+$command->expectsConfirmation(
+'Config file already exists. Do you want to overwrite it?',
+// When answered with "no"
+'no'
+);
+
+// We should see a message that our file was not overwritten
+$command->expectsOutput('Existing configuration was not overwritten');
+
+// Assert that the original contents of the config file remain
+$this->assertEquals('test contents',file_get_contents(config_path('blogpackage.php')));
+
+// Clean up
+unlink(config_path('blogpackage.php'));
+}
+
+/** @test */
+publicfunctionwhen_a_config_file_is_present_users_can_choose_to_do_overwrite_it()
+{
+// Given we have already have an existing config file
+File::put(config_path('blogpackage.php'),'test contents');
+$this->assertTrue(File::exists(config_path('blogpackage.php')));
+
+// When we run the install command
+$command=$this->artisan('blogpackage:install');
+
+// We expect a warning that our configuration file exists
+$command->expectsConfirmation(
+'Config file already exists. Do you want to overwrite it?',
+// When answered with "yes"
+'yes'
+);
+
+// execute the command to force override
+$command->execute();
+
+$command->expectsOutput('Overwriting configuration file...');
+
+// Assert that the original contents are overwritten
+$this->assertEquals(
+file_get_contents(__DIR__.'/../config/config.php'),
+file_get_contents(config_path('blogpackage.php'))
+);
+
+// Clean up
+unlink(config_path('blogpackage.php'));
+}
+
+
Hiding a Command
+
There might be cases where you'd like to exclude the command from the list of Artisan commands. You can define a $hidden property on the command class, which will not show the specific command in the list of Artisan commands. NB: you can still use the command while hidden.
Laravel provides an easy way to create Generator Commands, i.e., commands with signatures such as php artisan make:controller. Those commands modify a general, predefined template (stub) to a specific application. For example, by automatically injecting the correct namespace.
+
To create a Generator Command, you have to extend the Illuminate\Console\GeneratorCommand class, and override the following properties and methods:
+
+
protected $name: name of the command
+
protected $description: description of the command
+
protected $type: the type of class the command generates
+
protected function getStub(): method returning the path of the stub template file
+
protected function getDefaultNamespace($rootNamespace): the default namespace of the generated class
+
public function handle(): the body of the command
+
+
The GeneratorCommand base class provides some helper methods:
+
+
getNameInput(): returns the name passed from command line execution
+
qualifyClass(string $name): returns the qualified class name for a given class name
+
getPath(string $name): returns the file path for a given name
+
+
Consider the following example for the php artisan make:foo MyFoo command:
+
<?php
+
+namespaceJohnDoe\BlogPackage\Console;
+
+useIlluminate\Console\GeneratorCommand;
+
+classMakeFooCommandextendsGeneratorCommand
+{
+protected$name='make:foo';
+
+protected$description='Create a new foo class';
+
+protected$type='Foo';
+
+protectedfunctiongetStub()
+{
+return__DIR__.'/stubs/foo.php.stub';
+}
+
+protectedfunctiongetDefaultNamespace($rootNamespace)
+{
+return$rootNamespace.'\Foo';
+}
+
+publicfunctionhandle()
+{
+parent::handle();
+
+$this->doOtherOperations();
+}
+
+protectedfunctiondoOtherOperations()
+{
+// Get the fully qualified class name (FQN)
+$class=$this->qualifyClass($this->getNameInput());
+
+// get the destination path, based on the default namespace
+$path=$this->getPath($class);
+
+$content=file_get_contents($path);
+
+// Update the file content with additional data (regular expressions)
+
+file_put_contents($path,$content);
+}
+}
+
+
Note that the Generator Command will export the class to a directory based on the namespace specified in the getDefaultNamespace() method.
+
As with the InstallBlogPackage command, we have to register this new command in the BlogPackageServiceProvider:
You are free to store stubs in a different directory, but we'll store the stubs in the Console/stubs directory in this example. For our Foo class generator, the stub could look as follows:
Note that DummyNamespace and DummyClass are placeholders, strictly defined in the GeneratorCommand base class. Laravel expects these specific names to replace them automatically with the correct values.
+
Testing Generator Commands
+
We can add a feature test for this command in the tests/Feature directory, called MakeFooCommandTest.php, which verifies that a new file is created and contains the correct contents:
+
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Feature;
+
+useIlluminate\Support\Facades\File;
+useIlluminate\Support\Facades\Artisan;
+useJohnDoe\BlogPackage\Tests\TestCase;
+
+classMakeFooCommandTestextendsTestCase
+{
+/** @test */
+functionit_creates_a_new_foo_class()
+{
+// destination path of the Foo class
+$fooClass=app_path('Foo/MyFooClass.php');
+
+// make sure we're starting from a clean state
+if(File::exists($fooClass)){
+unlink($fooClass);
+}
+
+$this->assertFalse(File::exists($fooClass));
+
+// Run the make command
+Artisan::call('make:foo MyFooClass');
+
+// Assert a new file is created
+$this->assertTrue(File::exists($fooClass));
+
+// Assert the file contains the right contents
+$expectedContents=<<<CLASS
+<?php
+
+namespace App\Foo;
+
+use JohnDoe\BlogPackage\Foo;
+
+class MyFooClass implements Foo
+{
+ public function myFoo()
+ {
+ // foo
+ }
+}
+CLASS;
+
+$this->assertEquals($expectedContents,file_get_contents($fooClass));
+}
+}
+
+
Creating a Test-Only Command
+
There are some situations where you would like to only use a particular command for testing and not in your application itself. For example, when your package provides a Trait that Command classes can use. To test the trait, you want to use an actual command.
+
Using an actual command solely for test purposes doesn't add functionality to the package and should not be published. A viable solution is to register the Command only in the tests, by hooking into Laravel's Application::starting() method as proposed by Marcel Pociot:
It is quite likely that your package allows configuration by the end-user.
+
If you want to offer custom configuration options, create a new config directory in the package's root and add a file called config.php, which returns an array of options.
+
config/config.php
<?php
+
+return[
+'posts_table'=>'posts',
+// other options...
+];
+
+
Merging Into the Existing Configuration
+
After registering the config file in the register() method of our service provider under a specific "key" ('blogpackage' in our demo), we can access the config values from the config helper by prefixing our "key" as follows: config('blogpackage.posts_table').
To allow users to modify the default config values, we need to provide them with the option to export the config file. We can register all "publishables" within the boot() method of the package's service provider. Since we only want to offer this functionality whenever the package is booted from the console, we'll first check if the current app runs in the console. We'll register the publishable config file under the 'config' tag (the second parameter of the $this->publishes() function call).
The config file can now be exported using the command listed below, creating a blogpackage.php file in the /config directory of the Laravel project using this package.
There are scenarios where you'll need to ship one or more Eloquent models with your package. For example, when you're developing a Blog related package that includes a Post model.
+
This chapter will cover how to provide Eloquent models within your package, including migrations, tests, and how to possibly add a relationship to the App\User model that ships with Laravel.
+
Models
+
Models in our package do not differ from models we would use in a standard Laravel application. Since we required the Orchestra Testbench, we can create a model extending the Laravel Eloquent model and save it within the src/Models directory:
There are multiple ways to generate models together with a migration automatically. The straightforward approach is to use a regular Laravel application and then copy over the artisan-generated files to your package and then update the namespaces.
+
If you are looking for ways to automate the scaffolding within your package, you might install one of the following tools as a dev dependency within your package and use a CLI command to generate the scaffolds.
Migrations live in the database/migrations folder in a Laravel application. In our package we mimic this file structure. Therefore, database migrations will not live in the src/ directory but in their own database/migrations folder. Our package's root directory now contains at least two folders: src/ and database/.
+
After you’ve generated a migration, copy it from your “dummy” Laravel application to the package’s database/migrations folder.
From this point on, there are two possible approaches to present the end-user with our migration(s). We can either publish (specific) migrations (method 1) or load all migrations from our package automatically (method 2).
+
Publishing Migrations (method 1)
+
In this approach, we register that our package “publishes” its migrations. We can do that as follows in the boot() method of our package’s service provider, employing the publishes() method, which takes two arguments:
+
+
+
an array of file paths ("source path" => "destination path")
+
+
+
the name (“tag”) we assign to this group of related publishable assets.
+
+
+
In this approach, it is conventional to use a "stubbed" migration. This stub is exported to a real migration when the user of our package publishes the migrations. Therefore, rename any migrations to remove the timestamp and add a .stub extension. In our example migration, this would lead to: create_posts_table.php.stub.
+
Next, we can implement exporting the migration(s) as follows:
+
<?php
+
+classBlogPackageServiceProviderextendsServiceProvider
+{
+publicfunctionboot()
+{
+if($this->app->runningInConsole()){
+// Export the migration
+if(!class_exists('CreatePostsTable')){
+$this->publishes([
+__DIR__.'/../database/migrations/create_posts_table.php.stub'=>database_path('migrations/'.date('Y_m_d_His',time()).'_create_posts_table.php'),
+// you can add any number of migrations here
+],'migrations');
+}
+}
+}
+}
+
+
In the code listed above, we first check if the application is running in the console. Next, we'll check if the user already published the migrations. If not, we will publish the create_posts_table migration in the migrations folder in the database path, prefixed with the current date and time.
+
The migrations of this package are now publishable under the “migrations” tag via:
While the method described above gives full control over which migrations are published, Laravel offers an alternative approach making use of the loadMigrationsFrom helper (see docs). By specifying a migrations directory in the package's service provider, all migrations will be executed when the end-user executes php artisan migrate from within their Laravel application.
Make sure to include a proper timestamp to your migrations, otherwise, Laravel can't process them. For example: 2018_08_08_100000_example_migration.php. You can not use a stub (like in method 1) when choosing this approach.
+
Testing Models and Migrations
+
As we create an example test, we will follow some of the basics of test-driven-development (TDD) here. Whether or not you practice TDD in your typical workflow, explaining the steps here helps expose possible problems you might encounter along the way, thus making troubleshooting simpler. Let's get started:
+
Writing a Unit Test
+
Now that we’ve set up PHPunit, let’s create a unit test for our Post model in the tests/Unit directory called PostTest.php. Let's write a test that verifies a Post has a title:
Note: we're using the RefreshDatabase trait to be sure that we start with a clean database state before every test.
+
Running the Tests
+
We can run our test suite by calling the PHPUnit binary in our vendor directory using ./vendor/bin/phpunit. However, let’s alias this to test in our composer.json file by adding a “script”:
As with the src folder, for our package users to be able to use our model factories, we'll need to register the database/factories folder within a namespace in our composer.json file:
After setting it up, don't forget to run composer dump-autoload.
+
Configuring our Model factory
+
Rerunning our tests lead to the following error:
+
Error: Class 'Database\Factories\JohnDoe\BlogPackage\Models\PostFactory' not found
+
+
The abovementioned error is caused by Laravel, which tries to resolve the Model class for our PostFactory assuming the default namespaces of a usual project (as of version 8.x, App or App\Models).
+To be able to instantiate the right Model from our package with the Post::factory() method, we need to add the following method to our Post Model:
However, the tests will still fail since we haven’t created the posts table in our in-memory SQLite database. We need to tell our tests to first perform all migrations before running the tests.
+
Let’s load the migrations in the getEnvironmentSetUp() method of our TestCase:
+
tests/TestCase.php
<?php
+
+publicfunctiongetEnvironmentSetUp($app)
+{
+// import the CreatePostsTable class from the migration
+include_once__DIR__.'/../database/migrations/create_posts_table.php.stub';
+
+// run the up() method of that migration class
+(new\CreatePostsTable)->up();
+}
+
+
Now, running the tests again will lead to the expected error of no ‘title’ column being present on the ‘posts’ table. Let’s fix that in the create_posts_table.php.stub migration:
After running the test, you should see it passing.
+
Adding Tests for Other Columns
+
Let’s add tests for the “body” and “author_id”:
+
tests/Unit/PostTest.php
<?php
+
+classPostTestextendsTestCase
+{
+useRefreshDatabase;
+
+/** @test */
+functiona_post_has_a_title()
+{
+$post=Post::factory()->create(['title'=>'Fake Title']);
+$this->assertEquals('Fake Title',$post->title);
+}
+
+/** @test */
+functiona_post_has_a_body()
+{
+$post=Post::factory()->create(['body'=>'Fake Body']);
+$this->assertEquals('Fake Body',$post->body);
+}
+
+/** @test */
+functiona_post_has_an_author_id()
+{
+// Note that we are not assuming relations here, just that we have a column to store the 'id' of the author
+$post=Post::factory()->create(['author_id'=>999]);// we choose an off-limits value for the author_id so it is unlikely to collide with another author_id in our tests
+$this->assertEquals(999,$post->author_id);
+}
+}
+
+
You can continue driving this out with TDD on your own, running the tests, exposing the next thing to implement, and testing again.
+
Eventually you’ll end up with a model factory and migration as follows:
Now that we have an “author_id” column on our Post model, let’s create a relationship between a Post and a User. However, we have a problem since we need a User model, but this model also comes out-of-the-box with a fresh installation of the Laravel framework…
+
We can’t just provide our own User model, since you likely want your end-user to be able to hook up the User model from their Laravel app.
+
Below, there are two options to create a relation
+
Approach 1: Fetching the User model from the Auth configuration
+
If you simply want to create a relationship between authenticated users and e.g. a Post model, the easiest option is to reference the Model that is used in the config/auth.php file. By default, this is the App\Models\User Eloquent model.
+
If you just want to target the Eloquent model that is responsible for the authentication, create a belongsToMany relationship on the Post model as follows:
+
<?php
+
+// Post model
+classPostextendsModel
+{
+publicfunctionauthor()
+{
+return$this->belongsTo(config('auth.providers.users.model'));
+}
+}
+
+
However, what if the user of our package has an Admin and a User model and the author of a Post can be an Admin model or a User model ? In such cases, you can opt for a polymorphic relationship.
+
Approach 2: Using a Polymorphic Relationship
+
Instead of opting for a conventional one-to-many relationship (a user can have many posts, and a post belongs to a user), we’ll use a polymorphic one-to-many relationship where a Post morphs to a specific related model (not necessarily a User model).
+
Let’s compare the standard and polymorphic relationships.
+
Definition of a standard one-to-many relationship:
+
<?php
+
+// Post model
+classPostextendsModel
+{
+publicfunctionauthor()
+{
+return$this->belongsTo(User::class);
+}
+}
+
+// User model
+classUserextendsModel
+{
+publicfunctionposts()
+{
+return$this->hasMany(Post::class);
+}
+}
+
+
Definition of a polymorphic one-to-many relationship:
+
<?php
+
+// Post model
+classPostextendsModel
+{
+publicfunctionauthor()
+{
+return$this->morphTo();
+}
+}
+
+// User (or other) model
+useJohnDoe\BlogPackage\Models\Post;
+
+classAdminextendsModel
+{
+publicfunctionposts()
+{
+return$this->morphMany(Post::class,'author');
+}
+}
+
+
After adding this author() method to our Post model, we need to update our create_posts_table_migration.php.stub file to reflect our polymorphic relationship. Since we named the method “author”, Laravel expects an “author_id” and an “author_type” field. The latter contains a string of the namespaced model we refer to (for example, “App\User”).
Now, we need a way to provide our end-user with the option to allow specific models to have a relationship with our Post model. Traits offer an excellent solution for this exact purpose.
+
Providing a Trait
+
Create a Traits folder in the src/ directory and add the following HasPosts trait:
Now the end-user can add a use HasPosts statement to any of their models (likely the User model), which would automatically register the one-to-many relationship with our Post model. This allows creating new posts as follows:
+
<?php
+
+// Given we have a User model, using the HasPosts trait
+$user=User::first();
+
+// We can create a new post from the relationship
+$user->posts()->create([
+'title'=>'Some title',
+'body'=>'Some body',
+]);
+
+
Testing the Polymorphic Relationship
+
Of course, we want to prove that any model using our HasPost trait can create new posts and that those posts are stored correctly.
+
Therefore, we’ll create a new User model, not within the src/Models/ directory, but rather in our tests/ directory.
+
To create users within our tests we'll need to overwrite the UserFactory provided by the Orchestra Testbench package, as shown below.
In the User model we’ll use the same traits available on the User model that ships with a standard Laravel project to stay close to a real-world scenario. Also, we use our own HasPosts trait and UserFactory:
Now that we have a User model, we also need to add a new migration (the standard users table migration that ships with Laravel) to our database/migrations as create_users_table.php.stub:
Also load the migration at the beginning of our tests, by including the migration and performing its up() method in our TestCase:
+
tests/TestCase.php
<?php
+
+publicfunctiongetEnvironmentSetUp($app)
+{
+include_once__DIR__.'/../database/migrations/create_posts_table.php.stub';
+include_once__DIR__.'/../database/migrations/create_users_table.php.stub';
+
+// run the up() method (perform the migration)
+(new\CreatePostsTable)->up();
+(new\CreateUsersTable)->up();
+}
+
+
Updating Our Post Model Factory
+
Now that we can whip up User models with our new factory, let’s create a new User in our PostFactory and then assign it to “author_id” and “author_type”:
Finally, we need to verify that our test User can create a Post and it is stored correctly.
+
Since we are not creating a new post using a call to a specific route in the application, let's store this test in the Post unit test. In the next section on “Routes & Controllers”, we’ll make a POST request to an endpoint to create a new Post model and therefore divert to a Feature test.
+
A Unit test that verifies the desired behavior between a User and a Post could look as follows:
+
tests/Unit/PostTest.php
<?php
+
+classPostTestextendsTestCase
+{
+// other tests...
+
+/** @test */
+functiona_post_belongs_to_an_author()
+{
+// Given we have an author
+$author=User::factory()->create();
+// And this author has a Post
+$author->posts()->create([
+'title'=>'My first fake post',
+'body'=>'The body of this fake post',
+]);
+
+$this->assertCount(1,Post::all());
+$this->assertCount(1,$author->posts);
+
+// Using tap() to alias $author->posts()->first() to $post
+// To provide cleaner and grouped assertions
+tap($author->posts()->first(),function($post)use($author){
+$this->assertEquals('My first fake post',$post->title);
+$this->assertEquals('The body of this fake post',$post->body);
+$this->assertTrue($post->author->is($author));
+});
+}
+}
+
+
At this stage, all of the tests should be passing.
Sometimes you want to expose additional routes to the end-user of your package.
+
Since we're offering a Post model, let's add some RESTful routes. To keep things simple, we're just going to implement 3 of the RESTful routes:
+
+
show all posts ('index')
+
show a single post ('show')
+
store a new post ('store')
+
+
Controllers
+
Creating a Base Controller
+
We want to create a PostController.
+
To make use of some traits the Laravel controllers offer, we'll first create our own base controller containing these traits in a src/Http/Controllers directory (resembling Laravel's folder structure) named Controller.php:
Creating a Controller That Extends Base Controller
+
Now, let's create a PostController in the src/Http/Controllers directory, starting first with the 'store' method:
+
src/Http/Controllers/PostController
<?php
+
+namespaceJohnDoe\BlogPackage\Http\Controllers;
+
+classPostControllerextendsController
+{
+publicfunctionindex()
+{
+//
+}
+
+publicfunctionshow()
+{
+//
+}
+
+publicfunctionstore()
+{
+// Let's assume we need to be authenticated
+// to create a new post
+if(!auth()->check()){
+abort(403,'Only authenticated users can create new posts.');
+}
+
+request()->validate([
+'title'=>'required',
+'body'=>'required',
+]);
+
+// Assume the authenticated user is the post's author
+$author=auth()->user();
+
+$post=$author->posts()->create([
+'title'=>request('title'),
+'body'=>request('body'),
+]);
+
+returnredirect(route('posts.show',$post));
+}
+}
+
+
Routes
+
Defining Routes
+
Now that we have a controller, create a new routes/ directory in our package's root and add a web.php file containing the three RESTful routes we've mentioned above.
Before we can use these routes, we need to register them in the boot() method of our Service Provider:
+
BlogPackageServiceProvider.php
<?php
+
+publicfunctionboot()
+{
+// ... other things
+$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
+}
+
+
Configurable Route Prefix and Middleware
+
You may want to allow users to define a route prefix and middleware for the routes exposed by your package. Instead of registering the routes directly in the boot() method we'll register the routes using Route::group, passing in the dynamic configuration (prefix and middleware). Don't forget to import the corresponding Route facade.
+
The following examples use a namespace of blogpackage. Don't forget to replace this with your package's namespace.
Specify a default route prefix and middleware in the package's config.php file:
+
<?php
+[
+'prefix'=>'blogger',
+'middleware'=>['web'],// you probably want to include 'web' here
+]
+
+
In the above default configuration, all routes defined in routes.web need to be prefixed with /blogger. In this way, collision with potentially existing routes is avoided.
+
Views
+
The 'index' and 'show' methods on the PostController need to render a view.
+
Creating the Blade View Files
+
Create a new resources/ folder at the root of our package. In that folder, create a subfolder named views. In the views folder, we'll create a posts subfolder in which we'll create two (extremely) simple templates.
+
+
resources/views/posts/index.blade.php:
+
+
<h1>Showing all Posts</h1>
+
+@forelse ($posts as $post)
+<li>{{ $post->title }}</li>
+@empty
+<p> 'No posts yet' </p>
+@endforelse
+
Note: these templates would extend a base/master layout file in a real-world scenario.
+
Registering Views in the Service Provider
+
Now that we have some views, we need to register that we want to load any views from our resources/views directory in the boot() method of our Service Provider. Important: provide a "key" as the second argument to loadViewsFrom() as you'll need to specify this key when returning a view from a controller (see next section).
+
BlogPackageServiceProvider.php
<?php
+
+publicfunctionboot()
+{
+// ... other things
+$this->loadViewsFrom(__DIR__.'/../resources/views','blogpackage');
+}
+
+
Returning a View from the Controller
+
We can now return the views we've created from the PostController (don't forget to import our Post model).
+
Note the blogpackage:: prefix, which matches the prefix we registered in our Service Provider.
Chances are that you want to be able to let the users of your package customize the views. Similar to the database migrations, the views can be published if we register them to be exported in the boot() method of our service provider using the 'views' key of the publishes() method:
Since Laravel 8, it is possible to generate Blade components using php artisan make:component MyComponent which generates a base MyComponent class and a Blade my-component.blade.php file, which receives all public properties as defined in the MyComponent class. These components can then be reused and included in any view using the component syntax: <x-my-component> and closing </x-my-component> (or the self-closing form). To learn more about Blade components, make sure to check out the Laravel documentation.
+
In addition to generating Blade components using the artisan command, it is also possible to create a my-component.blade.php component without class. These are called anonymous components and are placed in the views/components directory by convention.
+
This section will cover how to provide these type of Blade components in your package.
+
Class Based Components
+
If you want to offer class based View Components in your package, first create a new View/Components directory in the src folder. Add a new class, for example Alert.php.
Next, create a new views/components directory in the resources folder. Add a new Blade component alert.blade.php:
+
<div>
+<p>This is an Alert</p>
+
+<p>{{ $message }}</p>
+</div>
+
+
Next, register the component in the Service Provider by the class and provide a prefix for the components. In our example, using 'blogpackage', the alert component will become available as <x-blogpackage-alert />.
+
BlogPackageServiceProvider.php
<?php
+
+useJohnDoe\BlogPackage\View\Components\Alert;
+
+publicfunctionboot()
+{
+// ... other things
+$this->loadViewComponentsAs('blogpackage',[
+Alert::class,
+]);
+}
+
+
Anonymous View Components
+
If your package provides anonymous components, it suffices to add the my-component.blade.php Blade component to resources/views/components directory, given that you have specified the loadViewsFrom directory in your Service Provider as "resources/views". If you don't already, add the loadViewsFrom method to your Service Provider:
+
BlogPackageServiceProvider.php
<?php
+
+publicfunctionboot()
+{
+// ... other things
+$this->loadViewsFrom(__DIR__.'/../resources/views','blogpackage');
+}
+
+
Components (in the resources/views/components folder) can now be referenced prefixed by the defined namespace above ("blogpackage"):
+
<x-blogpackage::alert />
+
+
Customizable View Components
+
In order to let the end user of our package modify the provided Blade component(s), we first need to register the publishables into our Service Provider:
Be aware that the end user needs to update the namespaces of the published component class and update the render() method to reference the Blade components of the Laravel application directly, instead of referencing the package namespace. Additionally, the Blade component no longer has to be namespaced since it was published to the Laravel application itself.
+
Testing Routes
+
Let’s verify that we can indeed create a post, show a post and show all posts with our provided routes, views, and controllers.
+
Feature Test
+
Create a new Feature test called CreatePostTest.php in the tests/Feature directory and add the following assertions to verify that authenticated users can indeed create new posts:
+
tests/Feature/CreatePostTest.php
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Feature;
+
+useIlluminate\Foundation\Testing\RefreshDatabase;
+useJohnDoe\BlogPackage\Models\Post;
+useJohnDoe\BlogPackage\Tests\TestCase;
+useJohnDoe\BlogPackage\Tests\User;
+
+classCreatePostTestextendsTestCase
+{
+useRefreshDatabase;
+
+/** @test */
+functionauthenticated_users_can_create_a_post()
+{
+// To make sure we don't start with a Post
+$this->assertCount(0,Post::all());
+
+$author=User::factory()->create();
+
+$response=$this->actingAs($author)->post(route('posts.store'),[
+'title'=>'My first fake title',
+'body'=>'My first fake body',
+]);
+
+$this->assertCount(1,Post::all());
+
+tap(Post::first(),function($post)use($response,$author){
+$this->assertEquals('My first fake title',$post->title);
+$this->assertEquals('My first fake body',$post->body);
+$this->assertTrue($post->author->is($author));
+$response->assertRedirect(route('posts.show',$post));
+});
+}
+}
+
+
Additionally, we could verify that we require both a "title" and a "body" attribute when creating a new post:
Next, let's verify that unauthenticated users (or "guests") can not create new posts:
+
tests/Feature/CreatePostTest.php
<?php
+
+/** @test */
+functionguests_can_not_create_posts()
+{
+// We're starting from an unauthenticated state
+$this->assertFalse(auth()->check());
+
+$this->post(route('posts.store'),[
+'title'=>'A valid title',
+'body'=>'A valid body',
+])->assertForbidden();
+}
+
+
Finally, let's verify the index route shows all posts, and the show route shows a specific post:
+
tests/Feature/CreatePostTest.php
<?php
+
+/** @test */
+functionall_posts_are_shown_via_the_index_route()
+{
+// Given we have a couple of Posts
+Post::factory()->create([
+'title'=>'Post number 1'
+]);
+Post::factory()->create([
+'title'=>'Post number 2'
+]);
+Post::factory()->create([
+'title'=>'Post number 3'
+]);
+
+// We expect them to all show up
+// with their title on the index route
+$this->get(route('posts.index'))
+->assertSee('Post number 1')
+->assertSee('Post number 2')
+->assertSee('Post number 3')
+->assertDontSee('Post number 4');
+}
+
+/** @test */
+functiona_single_post_is_shown_via_the_show_route()
+{
+$post=Post::factory()->create([
+'title'=>'The single post title',
+'body'=>'The single post body',
+]);
+
+$this->get(route('posts.show',$post))
+->assertSee('The single post title')
+->assertSee('The single post body');
+}
+
+
+
Tip: whenever you are getting cryptic error messages from your tests, it might be helpful to disable graceful exception handling to get more insight into the error's origin. You can do so by declaring $this->withoutExceptionHandling(); at the start of your test.
Your package may want to offer support for hooking into Laravel's Events and Listeners.
+
Laravel's events provide a way to hook into a particular activity that took place in your application. They can be emitted/dispatched using the event() helper, which accepts an Event class as a parameter. After an event is dispatched, the handle() method of all registered Listeners will be triggered. The listeners for a certain event are defined in the application's event service provider. An event-driven approach might help to keep the code loosely coupled.
+
It is not uncommon that packages emit events upon performing a particular task. The end-user may or may not register their own listeners for an event you submit within a package. However, sometimes you might also want to listen within your package to your own events. For this, we'll need our package-specific event service provider and that's what we're looking at in this section.
+
Creating a New Event
+
First, let's emit an event whenever a new Post is created via the route we set up earlier.
+
In a new Events folder in the src/ directory, create a new PostWasCreated.php file. In the PostWasCreated event class, we'll accept the created Post in the constructor and save it to a public instance variable $post.
To be sure this event is successfully fired, add a test to our CreatePostTestfeature test. We can easily fake Laravel's Event facade and make assertions (see Laravel documentation on Fakes) that the event was emitted and about the passed Post model.
Now that we know that our event is fired correctly let's hook up our listener.
+
Creating a New Listener
+
After a PostWasCreated event was fired, let's modify our post's title for demonstrative purposes. In the src/ directory, create a new folder Listeners. In this folder, create a new file that describes our action: UpdatePostTitle.php:
Although we've tested correct behavior when the Event is emitted, it is still worthwhile to have a separate test for the event's listener. If something breaks in the future, this test will lead you directly to the root of the problem: the listener.
+
In this test, we'll assert that the listener's handle() method indeed changes the title of a blog post (in our silly example) by instantiating the UpdatePostTitle listener and passing a PostWasCreated event to its handle() method:
Now that we have a passing test for emitting the event, and we know that our listener shows the right behavior handling the event, let's couple the two together and create a custom Event Service Provider.
+
Creating an Event Service Provider
+
Like in Laravel, our package can have multiple service providers as long as we load them in our application service provider (in the next section).
+
First, create a new folder Providers in the src/ directory. Add a file called EventServiceProvider.php and register our Event and Listener:
In our main BlogPackageServiceProvider we need to register our Event Service Provider in the register() method, as follows (don't forget to import it):
Earlier, we faked the Event facade. But in this test, we would like to confirm that an event was fired that led to a handle method on a listener and that eventually changed the title of our Post, exactly like we'd expect. The test assertion is easy: assume that the title was changed after creating a new post. We'll add this method to the CreatePostTest feature test:
This test is green, but what if we run the full suite?
+
Fixing the Failing Test
+
If we run the full suite with composer test, we see we have one failing test:
+
There was 1 failure:
+
+1) JohnDoe\BlogPackage\Tests\Feature\CreatePostTest::authenticated_users_can_create_a_post
+Failed asserting that two strings are equal.
+--- Expected
++++ Actual
+@@ @@
+-'My first fake title'
++'New: My first fake title'
+
+
The failing test is a regression from the Event we've introduced. There are two ways to fix this error:
+
+
change the expected title in the authenticated_users_can_create_a_post test
+
by faking any events before the test runs, which inhibits the actual handlers to be called
+
+
It is very situational what happens to be the best option but let's go with option 2 for now.
+
tests/Feature/CreatePostTest.php
<?php
+
+/** @test */
+functionauthenticated_users_can_create_a_post()
+{
+Event::fake();
+
+$this->assertCount(0,Post::all());
+// the rest of the test...
+
+
All tests are green, so let's move on to the next topic.
If we look at an incoming HTTP request, this request is processed by Laravel's index.php file and sent through a series of pipelines. These include a series of ('before') middleware, where each will act on the incoming request before it eventually reaches the core of the application. A response is prepared from the application core, which is post-modified by all registered 'after' middleware before returning the response.
+
That's why middleware is excellent for authentication, verifying tokens, or applying any other check. Laravel also uses middleware to strip out empty characters from strings and encrypt cookies.
+
Creating Middleware
+
There are two types of middleware: 1) acting on the request before a response is returned ("Before Middleware"); or 2) acting on the response before returning ("After Middleware").
+
Before discussing the two types of middleware, first create a new Middleware folder in the package's src/Http directory.
+
Before Middleware
+
A before middleware performs an action on the request and then calls the next middleware in line. Generally, a Before Middleware takes the following shape:
As an illustration of a before middleware, let's add a middleware that capitalizes a 'title' parameter whenever present in the request (which would be silly in a real-world application).
+
Add a file called CapitalizeTitle.php which provides a handle() method accepting both the current request and a $next action:
Although we haven't registered the middleware yet, and it will not be used in the application, we want to make sure that the handle() method shows the correct behavior.
+
Add a new CapitalizeTitleMiddlewareTest.php unit test in the tests/Unit directory. In this test, we'll assert that a title parameter on a Request() will contain the capitalized string after the middleware ran its handle() method:
+
tests/Unit/CapitalizeMiddlewareTest.php
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Unit;
+
+useIlluminate\Http\Request;
+useJohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle;
+useJohnDoe\BlogPackage\Tests\TestCase;
+
+classCapitalizeTitleMiddlewareTestextendsTestCase
+{
+/** @test */
+functionit_capitalizes_the_request_title()
+{
+// Given we have a request
+$request=newRequest();
+
+// with a non-capitalized 'title' parameter
+$request->merge(['title'=>'some title']);
+
+// when we pass the request to this middleware,
+// it should've capitalized the title
+(newCapitalizeTitle())->handle($request,function($request){
+$this->assertEquals('Some title',$request->title);
+});
+}
+}
+
+
After Middleware
+
The "after middleware" acts on the response returned after passing through all other middleware layers down the chain. Next, it modifies, and returns the response. Generally, it takes the following form:
Similar to before middleware, we can unit test after middleware that operate on the Response for a given request and modify this request before it is passed down to the next layer of middleware. Given that we have an InjectHelloWorld middleware that injects the string 'Hello World' in each response, the following test would assert correct behavior:
+
tests/Unit/InjectHelloWorldMiddlewareTest.php
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Unit;
+
+useIlluminate\Http\Request;
+useJohnDoe\BlogPackage\Http\Middleware\InjectHelloWorld;
+useJohnDoe\BlogPackage\Tests\TestCase;
+
+classInjectHelloWorldMiddlewareTestextendsTestCase
+{
+/** @test */
+functionit_checks_for_a_hello_word_in_response()
+{
+// Given we have a request
+$request=newRequest();
+
+// when we pass the request to this middleware,
+// the response should contain 'Hello World'
+$response=(newInjectHelloWorld())->handle($request,function($request){});
+
+$this->assertStringContainsString('Hello World',$response);
+}
+}
+
+
Now that we know the handle() method does its job correctly, let's look at the two options to register the middleware: globally vs. route specific.
+
Global middleware
+
Global middleware is, as the name implies, globally applied. Each request will pass through these middlewares.
+
If we want our capitalization check example to be applied globally, we can append this middleware to the Http\Kernel from our package's service provider. Make sure to import the Http Kernel contract, not the Console Kernel contract:
+
BlogPackageServiceProvider.php
<?php
+
+useIlluminate\Contracts\Http\Kernel;
+useJohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle;
+
+publicfunctionboot(Kernel$kernel)
+{
+// other things ...
+
+$kernel->pushMiddleware(CapitalizeTitle::class);
+}
+
+
This will push our middleware into the application's array of globally registered middleware.
+
Route middleware
+
In our case, you might argue that we likely don't have a 'title' parameter on each request. Probably even only on requests that are related to creating/updating posts. On top of that, we likely only ever want to apply this middleware to requests related to our blog posts.
+
However, our example middleware will modify all requests which have a title attribute. This is probably not desired. The solution is to make the middleware route-specific.
+
Therefore, we can register an alias to this middleware in the resolved Router class, from within the boot() method of our service provider.
+
Here's how to register the capitalize alias for this middleware:
+
BlogPackageServiceProvider.php
<?php
+
+useIlluminate\Routing\Router;
+useJohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle;
+
+publicfunctionboot()
+{
+// other things ...
+
+$router=$this->app->make(Router::class);
+$router->aliasMiddleware('capitalize',CapitalizeTitle::class);
+}
+
+
We can apply this middleware from within our controller by requiring it from the constructor:
+
src/Http/Controllers/PostController.php
<?php
+
+classPostControllerextendsController
+{
+publicfunction__construct()
+{
+$this->middleware('capitalize');
+}
+
+// other methods... (will use this middleware)
+}
+
+
Middleware Groups
+
Additionally, we can push our middleware to certain groups, like web or api, to make sure our middleware is applied on each route that belongs to these groups.
+
To do so, tell the router to push the middleware to a specific group (in this example, web):
+
BlogPackageServiceProvider.php
<?php
+
+useIlluminate\Contracts\Http\Kernel;
+useJohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle;
+
+publicfunctionboot(Kernel$kernel)
+{
+// other things ...
+
+$kernel->prependMiddlewareToGroup('web',CapitalizeTitle::class);// Add it before all other middlewares
+$kernel->appendMiddlewareToGroup('web',CapitalizeTitle::class);// Add it after all other middlewares
+}
+
+
The route middleware groups of a Laravel application are located in the App\Http\Kernel class. When applying this approach, you need to be sure that this package's users have the specific middleware group defined in their application.
+
Feature Testing Middleware
+
Regardless of whether we registered the middleware globally or route specifically, we can test that the middleware is applied when making a request.
+
Add a new test to the CreatePostTest feature test, in which we'll assume our non-capitalized title will be capitalized after the request has been made.
+
tests/Feature/CreatePostTest.php
<?php
+
+/** @test */
+functioncreating_a_post_will_capitalize_the_title()
+{
+$author=User::factory()->create();
+
+$this->actingAs($author)->post(route('posts.store'),[
+'title'=>'some title that was not capitalized',
+'body'=>'A valid body',
+]);
+
+$post=Post::first();
+
+// 'New: ' was added by our event listener
+$this->assertEquals('New: Some title that was not capitalized',$post->title);
+}
+
+
With the tests returning green, we've covered adding Middleware to your package.
Using e-mails in your package works very much the same as in a normal Laravel application. However, in your package, you need to make sure you are loading a views directory from your package (or the end-user's exported version of it).
+
To start sending e-mails, we need to create 1) a new mailable and 2) an e-mail template.
+
The e-mail template can be in either markdown or blade template format, as you're used to. In this example, we'll focus on writing a Blade template, however if you're using a markdown template replace the $this->view('blogpackage::mails.welcome') with a call to $this->markdown('blogpackage::mails.welcome'). Notice that we're using the namespaced view name, allowing our package users to export the views and update their contents.
+
Creating a Mailable
+
First, add a new Mail folder in the src/ directory, which will contain your mailables. Let's call it WelcomeMail.php mailable. Since we've been working with a Post model in the previous sections, let's accept that model in the constructor and assign it to a public$post property on the mailable.
In the call to the mailable's view() method we've specified the string emails.welcome, which Laravel will translate to searching for a welcome.blade.php file in the emails directory in the package's registered views directory.
+
To specify a view directory, you need to add the $this->loadViews() call to your package's service provider in the boot() method. View files can be referenced by the specified namespace, in this example, 'blogpackage'. Note: if you're following along since the section about Routing, you've already done this.
+
BlogPackageServiceProvider.php
<?php
+
+publicfunctionboot()
+{
+// ... other things
+$this->loadViewsFrom(__DIR__.'/../resources/views','blogpackage');
+}
+
+
This will look for views in the resources/views directory in the root of your package.
+
Creating a Blade Mail Template
+
Create the welcome.blade.php file in the resources/views/emails directory, where the $post variable will be freely available to use in the template.
+
resources/views/emails/welcome.blade.php
<p>
+Dear reader,
+
+Post title: {{ $post->title }}
+
+-- Sent from the blogpackage
+</p>
+
+
Testing Mailing
+
To test that e-mailing works and the mail contains all the right information, Laravel's Mail facade offers a built-in fake() method which makes it easy to swap the real mailer for a mock in our tests.
+
To demonstrate how to test our e-mail, create a new WelcomeMailTest in the tests/unit directory. Next, in the test:
+
+
Switch the Mail implementation for a mock using Mail::fake().
Much like the Mail facade in the previous section, implementing Jobs in your package is very similar to the workflow you'd go through in a Laravel application.
+
Creating a Job
+
First, create a new Jobs directory in the src/ directory of your package and add a PublishPost.php file, responsible for updating the 'published_at' timestamp of a Post. The example below illustrates what the handle() method could look like:
For this example, we have a publish() method on the Post model, which is already under test (a unit test for Post). We can easily test the expected behavior by adding a new PublishPostTest.php unit test in the tests/unit directory.
+
In this test, we can make use of the Bus facade, which offers a fake() helper to swap the real implementation with a mock. After dispatching the Job, we can assert on the Bus facade that our Job was dispatched and contains the correct Post.
Notifications are a powerful tool in Laravel's toolbox. They provide support for sending notifications to an array of different services, including mail, SMS, Slack, or storing them in your database to show on the user's profile page, for example.
+
Creating a Notification
+
First, to start using Notifications in your package, create a Notifications directory in your package's src/ directory.
+
For this example, add a PostWasPublishedNotification.php, which notifies the author of the Post that his submission was approved.
+
<?php
+
+namespaceJohnDoe\BlogPackage\Notifications;
+
+useIlluminate\Notifications\Messages\MailMessage;
+useIlluminate\Notifications\Notification;
+useJohnDoe\BlogPackage\Models\Post;
+
+classPostWasPublishedNotificationextendsNotification
+{
+public$post;
+
+publicfunction__construct(Post$post)
+{
+$this->post=$post;
+}
+
+/**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+publicfunctionvia($notifiable)
+{
+return['mail'];
+}
+
+/**
+ * Get the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+publicfunctiontoMail($notifiable)
+{
+return(newMailMessage)
+->line("Your post '{$this->post->title}' was accepted")
+->action('Notification Action',url("/posts/{$this->post->id}"))
+->line('Thank you for using our application!');
+}
+
+/**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+publicfunctiontoArray($notifiable)
+{
+return[
+//
+];
+}
+}
+
+
Testing Notifications
+
In the test:
+
+
Swap the Notification facade with a mock using the fake() helper.
+
Assert no notifications have been sent before calling the notify() method.
+
Notify the User model via $user->notify() (which needs to use the Notifiable trait).
+
Assert that the notification was sent and contains the correct Post model.
+
+
<?php
+
+namespaceJohnDoe\BlogPackage\Tests\Unit;
+
+useIlluminate\Support\Facades\Notification;
+useJohnDoe\BlogPackage\Models\Post;
+useJohnDoe\BlogPackage\Notifications\PostWasPublishedNotification;
+useJohnDoe\BlogPackage\Tests\TestCase;
+useJohnDoe\BlogPackage\Tests\User;
+
+classNotifyPostWasPublishedTestextendsTestCase
+{
+/** @test */
+publicfunctionit_can_notify_a_user_that_a_post_was_published()
+{
+Notification::fake();
+
+$post=Post::factory()->create();
+
+// the User model has the 'Notifiable' trait
+$user=User::factory()->create();
+
+Notification::assertNothingSent();
+
+$user->notify(newPostWasPublishedNotification($post));
+
+Notification::assertSentTo(
+$user,
+PostWasPublishedNotification::class,
+function($notification)use($post){
+return$notification->post->id===$post->id;
+}
+);
+}
+}
+
+
With the test passing, you can safely use this notification in your package.
+
Custom Notification Channels
+
Additionally, you may configure the channels for the notification to be dependent on your package's configuration file to allow your users to specify which notification channels they want to use.
Once satisfied with its functionality, you might want to share your package with a broader audience. This section will explain how to publish your package to the Packagist repository.
+
If you haven't already pushed your local git repository to a repository host (GitHub / GitLab / BitBucket / etc.), you should do so now. It is advisable to create an online repository with the same (package) name as defined in your composer.json file. Try to match these names, for example, by renaming your package to follow this convention.
+
Given the example below, consumers would be able to require the package using composer require johndoe/blogpackage and find the corresponding repository at (if using GitHub) github.com/johndoe/blogpackage.
The next step is to publish the git repository of the package to Packagist.
+
Publishing on Packagist
+
To submit a package to Packagist, first create an account and then use the Submit link and specify the publicrepository URL to the git repository of your package.
+
Packagist will now host a so-called dev-master version of your package. Although anyone can access this package now through composer require [vendor]/[package-name], the consumer will receive the package in its current state on the master branch. This means that all changes to master immediately take effect when consumers run composer update, which might lead to breaking changes. For projects which define a minimum-stability of "stable", this means that Composer will not install your package at all.
+
To prevent introducing breaking changes, while still free to refactor our package, the convention of semantic versioning is used to discriminate between versions and thus compatibility.
+
Releasing v1.0.0
+
Releases (and thus versions) of your package are tracked through tags on the corresponding git repository. There is currently a dev-master release available through Packagist, which always points to the latest commit on the master branch of the repository. However, ideally, we would like to serve the package in a fixed state to the consumer. This is where tags come in, which point to a specific commit.
+
To release version 1.0.0 of the package, create a new tag in your git repository. If you're using GitHub, you can visit the "releases" tab and "Create a new release". Provide a "Tag version" and "Release title" of 1.0.0 targeted at the current state of the master branch (serving as a pointer to the latest commit). Additionally, you might provide information regarding this release in the description. After clicking "Publish release", Packagist will automatically update and reflect this new version. By default, consumers requiring the package without specifying a version will be served the latest tag/version/release which in this case will be 1.0.0. You'll notice when you require this package in your project, the version constraint in composer.json will be ^1.0, allowing composer update to download versions up to 1.x (allowing minor and patch releases) but not 2.x (major release, containing breaking changes). See the section below on semantic versioning for more information.
+
Releasing a New Version
+
As you make updates to your package, refer to the semantic versioning while drafting new releases. When you create a new tag in the associated git repository, Packagist will automatically be updated.
+
Semantic Versioning
+
This section will provide a short overview of how Semantic Versioning is used and applied by Composer. To get a more in-depth overview, check out semver.org.
+
A version consists of three parts: MAJOR.MINOR.PATCH. Version 1.2.3 of a package could be referred to as a package on major version 1, minor version 2, patchlevel 3.
+
+
+
Major: contains breaking changes, compared to the previous release. Consumers of our package need to make adjustments to their existing code integrating this package.
+
+
+
Minor: contains added functionality (e.g. new methods) which do not break existing functionality. Consumers of our package do not need to make adjustments to their existing code integrating this package.
+
+
+
Patchlevel: contains bug fixes, upgraded dependencies, etc. but does not contain new functionality. Consumers of our package do not need to make adjustments to their existing code integrating this package.
+
+
+
Composer
+
PHP's package manager Composer uses the composer.json file to identify which packages should be installed and additionally which versions of a package are compatible with the project it belongs to. It keeps track of the versions through the package's tags on the corresponding repository.
+
From the Composer documentation:
+
+
Composer first asks the VCS to list all available tags, then creates an internal list of available versions based on these tags [...] When Composer has a complete list of available versions from your VCS, it then finds the highest version that matches all version constraints in your project (it's possible that other packages require more specific versions of the library than you do, so the version it chooses may not always be the highest available version) and it downloads a zip archive of that tag to unpack in the correct location in your vendor directory.
+
+
Version Constraints
+
Composer supports various version constraints, of which the ones using semantic versioning are the most used as most packages implement semantic versioning. There are two distinct ways to define a semantic version range:
+
+
+
Semantic version range (tilde "~"): ~1.2, translates to >=1.2 <2.0.0. All packages of version 1.x are considered valid. A more specific range ~1.2.3 translates to >=1.2.3 <1.3.0. All packages of version 1.2.x are considered valid.
+
+
+
Strict semantic version range (caret "^"): ^1.2.3, translates to >=1.2.3 <2.0.0. All packages of version 1.x are considered valid (since no breaking changes should be introduced while upgrading minor versions) and is, therefore, closer following the semantic versioning system compared to the "tilde" method mentioned above.
+
+
+
Semantic versioning allows us to specify a broad range of compatible libraries, preventing collisions with other dependencies requiring the same library and avoiding breaking changes at the same time.
+
Alternatively, Composer allows for more strict constraints:
+
+
+
Exact version: 1.2.3, will always download 1.2.3. If other packages require a different version of this dependency, Composer will throw an error since this dependency's requirements can not be satisfied.
+
+
+
Defined version range (hyphen "-"): 1.0 - 2.0, translates to >=1.0.0 <2.1. All packages of version 1.x are considered valid. A more specific range could be defined in the form 1.0.0 - 1.3.0, which translates to >=1.0.0 <=1.3.0. All packages of version 1.2.x will be considered valid.
You'll likely want to include a CSS and javascript file when you're adding views to your package.
+
Creating an 'assets' Directory
+
If you want to use a CSS stylesheet or include a javascript file in your views, create an assets directory in the resources/ folder. Since we might include several stylesheets or javascript files, let's create two subfolders: css and js to store these files, respectively. A convention is to name the main javascript file app.js and the main stylesheet app.css.
+
Customizable Assets
+
Just like the views, we can let our users customize the assets if they want. First, we'll determine where we'll export the assets in the boot() method of our service provider under the 'assets' key in a 'blogpackage' directory in the public path of the end user's Laravel app:
Sometimes we want to build the assets using a bundler like Webpack or Vite.
+
The latest versions of Laravel switched from Webpack to Vite, and it would be nice to use the same bundler for the package
+to support all the hot reload and dev features of Vite.
+
To do that we need to add Javascript packages using NPM.
+
+
If you don't have a package.json file already, run the npm init -y command to create one.
+
Install Vite and the laravel plugin npm install -D vite laravel-vite-plugin.
+
Create the same structure for the resources as a Laravel website.
+
Then create a vite.config.js
+
+
vite.config.js file content
+
import{defineConfig}from'vite';
+importlaravelfrom'laravel-vite-plugin';
+
+exportdefaultdefineConfig({
+plugins:[
+laravel({
+hotFile:'public/vendor/blogpackage/blogpackage.hot',// Most important lines
+buildDirectory:'vendor/blogpackage',// Most important lines
+input:['resources/css/app.css','resources/js/app.js'],
+refresh:true,
+}),
+],
+});
+