├── .prettierrc.json ├── docs ├── .vuepress │ ├── public │ │ ├── robots.txt │ │ ├── laravel-package-logo.png │ │ ├── assets │ │ │ ├── favicons │ │ │ │ ├── favicon.ico │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── mstile-150x150.png │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-384x384.png │ │ │ │ ├── browserconfig.xml │ │ │ │ └── safari-pinned-tab.svg │ │ │ └── pages │ │ │ │ └── laravelpackage.jpeg │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ └── android-chrome-384x384.png │ │ └── manifest.webmanifest │ ├── styles │ │ └── index.scss │ └── config.js ├── 01-the-basics.md ├── 07-configuration-files.md ├── 13-jobs.md ├── 14-notifications.md ├── 03-service-providers.md ├── 05-facades.md ├── 12-mail.md ├── README.md ├── 04-testing.md ├── 15-publishing.md ├── 02-development-environment.md ├── 10-events-and-listeners.md ├── 11-middleware.md ├── 06-artisan-commands.md ├── 09-routing.md └── 08-models-and-migrations.md ├── .github └── FUNDING.yml ├── .prettierignore ├── .gitignore ├── CONTRIBUTING.md ├── package.json ├── README.md ├── LICENSE └── CODE_OF_CONDUCT.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /docs/.vuepress/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Jhnbrn90] 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs/.vuepress/dist 3 | docs/.vuepress/config.js 4 | docs/.vuepress/public 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | docs/.vuepress/dist 4 | docs/.vuepress/.cache 5 | docs/.vuepress/.temp 6 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-brand: #4299e1; 3 | } 4 | 5 | html.dark { 6 | --c-brand: #4299e1; 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vuepress/public/laravel-package-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/laravel-package-logo.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/pages/laravelpackage.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/pages/laravelpackage.jpeg -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/icons/android-chrome-384x384.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkGhostHunter/LaravelPackage.com/master/docs/.vuepress/public/assets/favicons/android-chrome-384x384.png -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are highly welcomed. 4 | 5 | To start contributing, follow these steps: 6 | 7 | 1. Fork this repository and `git clone` your version 8 | 1. Install the dependencies (including Vuepress) with `npm install` (or use yarn) 9 | 1. Edit the documentation and view the output with Vuepress using `npm run dev` 10 | 1. When satisfied, format your code using `npx prettier --write .` 11 | 1. Commit your changes to the branch on your fork and submit a new PR to this master branch 12 | -------------------------------------------------------------------------------- /docs/.vuepress/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Laravel Package", 3 | "short_name": "Laravel Package", 4 | "icons": [ 5 | { 6 | "src": "/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/icons/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-package", 3 | "version": "1.0.0", 4 | "description": "A central place to learn how to create packages from scratch.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vuepress dev docs", 8 | "build": "vuepress build docs", 9 | "serve": "npm run dev" 10 | }, 11 | "dependencies": { 12 | "global": "^4.3.2", 13 | "vuepress": "2.0.0-beta.35" 14 | }, 15 | "devDependencies": { 16 | "@vuepress/plugin-pwa": "2.0.0-beta.35", 17 | "@vuepress/plugin-docsearch": "2.0.0-beta.35", 18 | "prettier": "2.1.2", 19 | "vuepress-plugin-seo": "^0.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Developing Laravel Packages 2 | 3 | This is the repo for the [Laravel Package](https://laravelpackage.com) project. A central place to learn how to create a package from scratch. 4 | 5 | All provided examples are available as an accompanying example package named "BlogPackage", which you can find and clone here: [https://github.com/Jhnbrn90/BlogPackage](https://github.com/Jhnbrn90/BlogPackage). 6 | 7 | ## Contributing 8 | 9 | Contributions are highly welcomed. 10 | 11 | To start contributing, follow these steps: 12 | 13 | 1. Fork this repository and `git clone` your version 14 | 1. Install the dependencies (including **VuePress**) with `npm install` (or use yarn) 15 | 1. Edit the documentation and view the output with VuePress using `npm run dev` 16 | 1. When satisfied, format your code using `npx prettier --write .` 17 | 1. Commit your changes and submit the PR to the master branch 18 | 19 | ## Credits 20 | 21 | - [John Braun][link-author] 22 | - [All Contributors][link-contributors] 23 | 24 | [link-author]: https://github.com/Jhnbrn90 25 | [link-contributors]: ../../contributors 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 John Braun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/01-the-basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Basics of Package Development" 3 | description: "Explore the basics of a PHP package, including the general directory structure, composer.json and autoloading." 4 | tags: ["package basics", "directory structure", "autoloading", "composer"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # The Basics 10 | 11 | ## Autoloading 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | Laravel's `public/index.php` file: 18 | 19 | ```php 20 | 'posts', 21 | // other options... 22 | ]; 23 | ``` 24 | 25 | ## Merging Into the Existing Configuration 26 | 27 | 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')`. 28 | 29 | ```php 30 | // 'BlogPackageServiceProvider.php' 31 | public function register() 32 | { 33 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'blogpackage'); 34 | } 35 | ``` 36 | 37 | ## Exporting 38 | 39 | 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). 40 | 41 | ```php 42 | // 'BlogPackageServiceProvider.php' 43 | public function boot() 44 | { 45 | if ($this->app->runningInConsole()) { 46 | 47 | $this->publishes([ 48 | __DIR__.'/../config/config.php' => config_path('blogpackage.php'), 49 | ], 'config'); 50 | 51 | } 52 | } 53 | ``` 54 | 55 | 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. 56 | 57 | ```bash 58 | php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="config" 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/13-jobs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Jobs" 3 | description: "Implementing Jobs in a package is essentially very similar to the workflow within a regular Laravel application. This section also covers testing the Job using the Bus facade." 4 | tags: ["Jobs", "Dispatching Jobs", "Testing Jobs", "Bus Facade"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Jobs 10 | 11 | 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. 12 | 13 | ## Creating a Job 14 | 15 | 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: 16 | 17 | ```php 18 | post = $post; 38 | } 39 | 40 | public function handle() 41 | { 42 | $this->post->publish(); 43 | } 44 | } 45 | ``` 46 | 47 | ## Testing Dispatching a Job 48 | 49 | 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. 50 | 51 | In this test, we can make use of the [`Bus` facade](https://laravel.com/docs/mocking#bus-fake), 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`. 52 | 53 | ```php 54 | create(); 71 | 72 | $this->assertNull($post->published_at); 73 | 74 | PublishPost::dispatch($post); 75 | 76 | Bus::assertDispatched(PublishPost::class, function ($job) use ($post) { 77 | return $job->post->id === $post->id; 78 | }); 79 | } 80 | } 81 | ``` 82 | 83 | As the test passes, you can safely make use of this Job in the package. 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@laravelpackage.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Laravel Package Development', 3 | description: 'Learn to create Laravel specific PHP packages from scratch, following this open documentation. Contributions are welcomed.', 4 | head: [ 5 | ['link', { rel: "apple-touch-icon", sizes: "180x180", href: "/assets/favicons/apple-touch-icon.png"}], 6 | ['link', { rel: "icon", href: "/assets/laravel-package-logo.png"}], 7 | ['link', { rel: "icon", type: "image/png", sizes: "32x32", href: "/assets/favicons/favicon-32x32.png"}], 8 | ['link', { rel: "icon", type: "image/png", sizes: "16x16", href: "/assets/favicons/favicon-16x16.png"}], 9 | ['link', { rel: "manifest", href: "/manifest.webmanifest"}], 10 | ['link', { rel: "mask-icon", href: "/assets/favicons/safari-pinned-tab.svg", color: "#3a0839"}], 11 | ['link', { rel: "shortcut icon", href: "/assets/favicons/favicon.ico"}], 12 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 13 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 14 | ['meta', { name: "msapplication-TileColor", content: "#3a0839"}], 15 | ['meta', { name: "msapplication-config", content: "/assets/favicons/browserconfig.xml"}], 16 | ['meta', { name: "theme-color", content: "#ffffff"}], 17 | ['meta', { name: "viewport", content: "width=device-width"}], 18 | ['script', { src: "https://jackal.laravelpackage.com/script.js", spa: "auto", site: "HUVUTEUR", defer:true}] 19 | ], 20 | themeConfig: { 21 | logo: '/laravel-package-logo.png', 22 | repo: 'Jhnbrn90/LaravelPackage.com', 23 | docsBranch: 'master', 24 | author: { 25 | 'name': 'John Braun', 26 | 'twitter': '@jhnbrn90' 27 | }, 28 | docsDir: 'docs', 29 | editLinks: true, 30 | editLinkText: 'Improve this page (submit a PR)', 31 | contributors: false, 32 | lastUpdated: false, 33 | domain: 'https://www.laravelpackage.com', 34 | docsearch: { 35 | container: '#docsearch', 36 | appId: process.env.DOCSEARCH_APP_ID, 37 | apiKey: process.env.DOCSEARCH_KEY, 38 | indexName: 'laravelpackage' 39 | }, 40 | navbar: [ 41 | { 42 | text: 'Laravel 8.x', 43 | ariaLabel: 'Version Menu', 44 | children: [ 45 | { text: 'Laravel 6.x - 7.x', link: 'https://v6-v7.laravelpackage.com', target:'_self', rel: false} 46 | ] 47 | }, 48 | { 49 | text: 'John Braun', 50 | link: 'https://johnbraun.blog/' 51 | }, 52 | ], 53 | // displayAllHeaders: true, 54 | sidebar: [ 55 | '/', 56 | '/01-the-basics', 57 | '/02-development-environment', 58 | '/03-service-providers', 59 | '/04-testing', 60 | '/05-facades', 61 | '/06-artisan-commands', 62 | '/07-configuration-files', 63 | '/08-models-and-migrations', 64 | '/09-routing', 65 | '/10-events-and-listeners', 66 | '/11-middleware', 67 | '/12-mail', 68 | '/13-jobs', 69 | '/14-notifications', 70 | '/15-publishing', 71 | ] 72 | }, 73 | plugins: [ 74 | ['seo', { 75 | siteTitle: (_, $site) => $site.title, 76 | title: $page => $page.title, 77 | description: $page => $page.frontmatter.description, 78 | author: (_, $site) => $site.themeConfig.author, 79 | tags: $page => $page.frontmatter.tags, 80 | twitterCard: _ => 'summary_large_image', 81 | type: $page => 'article', 82 | url: (_, $site, path) => ($site.themeConfig.domain || '') + path, 83 | image: ($page, $site) => $page.frontmatter.image, 84 | publishedAt: $page => $page.frontmatter.date && new Date($page.frontmatter.date), 85 | modifiedAt: $page => $page.lastUpdated && new Date($page.lastUpdated), 86 | }], 87 | [ 88 | '@vuepress/pwa', 89 | { 90 | skipWaiting: true, 91 | } 92 | ], 93 | [ 94 | '@vuepress/docsearch', 95 | { 96 | container: '#docsearch', 97 | appId: process.env.DOCSEARCH_APP_ID, 98 | apiKey: process.env.DOCSEARCH_KEY, 99 | indexName: 'laravelpackage' 100 | } 101 | ], 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /docs/14-notifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Notifications" 3 | description: "Discover how to send Notifications within a package, to an array of different services including mail, SMS, Slack, or storing them in your database. Additionally, the section covers testing of the Notification facade." 4 | tags: ["Notifications", "Testing Notifications", "Custom Notification Channels"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Notifications 10 | 11 | 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. 12 | 13 | ## Creating a Notification 14 | 15 | First, to start using Notifications in your package, create a `Notifications` directory in your package's `src/` directory. 16 | 17 | For this example, add a `PostWasPublishedNotification.php`, which notifies the author of the `Post` that his submission was approved. 18 | 19 | ```php 20 | post = $post; 35 | } 36 | 37 | /** 38 | * Get the notification's delivery channels. 39 | * 40 | * @param mixed $notifiable 41 | * @return array 42 | */ 43 | public function via($notifiable) 44 | { 45 | return ['mail']; 46 | } 47 | 48 | /** 49 | * Get the mail representation of the notification. 50 | * 51 | * @param mixed $notifiable 52 | * @return \Illuminate\Notifications\Messages\MailMessage 53 | */ 54 | public function toMail($notifiable) 55 | { 56 | return (new MailMessage) 57 | ->line("Your post '{$this->post->title}' was accepted") 58 | ->action('Notification Action', url("/posts/{$this->post->id}")) 59 | ->line('Thank you for using our application!'); 60 | } 61 | 62 | /** 63 | * Get the array representation of the notification. 64 | * 65 | * @param mixed $notifiable 66 | * @return array 67 | */ 68 | public function toArray($notifiable) 69 | { 70 | return [ 71 | // 72 | ]; 73 | } 74 | } 75 | ``` 76 | 77 | ## Testing Notifications 78 | 79 | In the test: 80 | 81 | - Swap the `Notification` facade with a mock using the `fake()` helper. 82 | - Assert no notifications have been sent before calling the `notify()` method. 83 | - Notify the `User` model via `$user->notify()` (which needs to use the `Notifiable` trait). 84 | - Assert that the notification was sent and contains the correct `Post` model. 85 | 86 | ```php 87 | create(); 105 | 106 | // the User model has the 'Notifiable' trait 107 | $user = User::factory()->create(); 108 | 109 | Notification::assertNothingSent(); 110 | 111 | $user->notify(new PostWasPublishedNotification($post)); 112 | 113 | Notification::assertSentTo( 114 | $user, 115 | PostWasPublishedNotification::class, 116 | function ($notification) use ($post) { 117 | return $notification->post->id === $post->id; 118 | } 119 | ); 120 | } 121 | } 122 | ``` 123 | 124 | With the test passing, you can safely use this notification in your package. 125 | 126 | ## Custom Notification Channels 127 | 128 | 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. 129 | 130 | ```php 131 | public function via($notifiable) 132 | { 133 | return config('blogpackage.notifications.channels'); 134 | } 135 | ``` 136 | 137 | Finally, add the `notifications.channels` sub-array entries to your configuration stub file (see the [Package Configuration](https://laravelpackage.com/07-configuration-files.html) section). 138 | -------------------------------------------------------------------------------- /docs/03-service-providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Service Providers" 3 | description: "The Service Provider of a package is essential to register package-specific functionality. This section will cover the role and basics of a Service Provider and explains how to create and use a Service Provider for your package." 4 | tags: ["Service Provider"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Service Providers 10 | 11 | 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 the service providers, please continue to the next section. 12 | 13 | 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. 14 | 15 | Every service provider extends the `Illuminate\Support\ServiceProvider` and implements a `register()` and a `boot()` method. 16 | 17 | 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. 18 | 19 | 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()` can be used. 20 | 21 | Here is an example of how a service provider may look and which things you might implement in a `register()` and `boot()` method. 22 | 23 | ```php 24 | use App\Calculator; 25 | use Illuminate\Support\Collection; 26 | use Illuminate\Support\Facades\Gate; 27 | use Illuminate\Support\ServiceProvider; 28 | 29 | class AppServiceProvider extends ServiceProvider 30 | { 31 | public function register() 32 | { 33 | // Register a class in the service container 34 | $this->app->bind('calculator', function ($app) { 35 | return new Calculator(); 36 | }); 37 | } 38 | 39 | public function boot() 40 | { 41 | // Register a macro, extending the Illuminate\Collection class 42 | Collection::macro('rejectEmptyFields', function () { 43 | return $this->reject(function ($entry) { 44 | return $entry === null; 45 | }); 46 | }); 47 | 48 | // Register an authorization policy 49 | Gate::define('delete-post', function ($user, $post) { 50 | return $user->is($post->author); 51 | }); 52 | } 53 | } 54 | ``` 55 | 56 | ## Creating a Service Provider 57 | 58 | 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. 59 | 60 | 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): 61 | 62 | ```php 63 | // 'src/BlogPackageServiceProvider.php' 64 | "laravel"> "providers" key in our package's `composer.json`: 87 | 88 | ```json 89 | { 90 | ..., 91 | 92 | "autoload": { ... }, 93 | 94 | "extra": { 95 | "laravel": { 96 | "providers": [ 97 | "JohnDoe\\BlogPackage\\BlogPackageServiceProvider" 98 | ] 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | 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. 105 | 106 | **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. 107 | 108 | ```php 109 | // 'config/app.php' 110 | [ 113 | // Other Service Providers 114 | 115 | App\Providers\ComposerServiceProvider::class, 116 | ], 117 | ``` 118 | -------------------------------------------------------------------------------- /docs/05-facades.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Facades" 3 | description: "Facades can provide end-users of your package with an easy-to-use (and understand) API for interaction with the functions (features) within your package. This section explains how to create your facades for your package." 4 | tags: ["Facades", "API"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Facades 10 | 11 | The word 'facade' refers to a "superficial appearance or illusion of something," according to [Dictionary.com](https://www.dictionary.com/browse/facade). In architecture, the term refers to the front of a building. 12 | 13 | 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. 14 | 15 | An example of a fluent API using a facade: 16 | 17 | ```php 18 | MessageFactory::sentBy($user) 19 | ->withTopic('Example message') 20 | ->withMessage($body) 21 | ->withReply($replyByFrank) 22 | ->create(); 23 | ``` 24 | 25 | ## How a Facade Works 26 | 27 | To learn more about facades and how they work, refer to the excellent [Laravel documentation](https://laravel.com/docs/facades#how-facades-work). 28 | 29 | 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. 30 | 31 | ## Creating a Facade 32 | 33 | Let’s assume that we provide a `Calculator` class as part of our package and want to make this class available as a facade. 34 | 35 | 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()->result()`). 36 | 37 | ```php 38 | // 'src/Calculator.php' 39 | result = 0; 50 | } 51 | 52 | public function add(int $value) 53 | { 54 | $this->result += $value; 55 | 56 | return $this; 57 | } 58 | 59 | public function subtract(int $value) 60 | { 61 | $this->result -= $value; 62 | 63 | return $this; 64 | } 65 | 66 | public function clear() 67 | { 68 | $this->result = 0; 69 | 70 | return $this; 71 | } 72 | 73 | public function getResult() 74 | { 75 | return $this->result; 76 | } 77 | } 78 | ``` 79 | 80 | In addition to this class, we’ll create the facade in a new `src/Facades` folder: 81 | 82 | ```php 83 | // 'src/Facades/Calculator.php' 84 | app->bind('calculator', function($app) { 108 | return new Calculator(); 109 | }); 110 | } 111 | ``` 112 | 113 | 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: 114 | 115 | ```json 116 | "extra": { 117 | "laravel": { 118 | "providers": [ 119 | "JohnDoe\\BlogPackage\\BlogPackageServiceProvider" 120 | ], 121 | "aliases": { 122 | "Calculator": "JohnDoe\\BlogPackage\\Facades\\Calculator" 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | **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. 129 | 130 | 131 | You can also load an alias from a Service Provider (or anywhere else) by using the `AliasLoader` singleton class: 132 | 133 | ``` 134 | $loader = \Illuminate\Foundation\AliasLoader::getInstance(); 135 | $loader->alias('Calculator', "JohnDoe\\BlogPackage\\Facades\\Calculator"); 136 | ``` 137 | 138 | Our facade now no longer requires an import and can be used in projects from the root namespace: 139 | 140 | ```php 141 | // Usage of the example Calculator facade 142 | Calculator::add(5)->subtract(3)->getResult(); // 2 143 | ``` 144 | -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/12-mail.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mail" 3 | description: "Send e-mail from your package by creating a custom Mailable class and template, utilizing the views provided by the package. Additionally, This chapter will cover testing of the Mail facade." 4 | tags: ["Mail", "Mail template", "Views", "Mailables", "Testing Mail"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Mail 10 | 11 | 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). 12 | 13 | To start sending e-mails, we need to create 1) a new **mailable** and 2) an e-mail **template**. 14 | 15 | 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. 16 | 17 | ## Creating a Mailable 18 | 19 | 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. 20 | 21 | ```php 22 | post = $post; 41 | } 42 | 43 | public function build() 44 | { 45 | return $this->view('blogpackage::emails.welcome'); 46 | } 47 | } 48 | ``` 49 | 50 | ## Registering the Views Directory 51 | 52 | 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. 53 | 54 | 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.** 55 | 56 | ```php 57 | // 'BlogPackageServiceProvider.php' 58 | public function boot() 59 | { 60 | // ... other things 61 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'blogpackage'); 62 | } 63 | ``` 64 | 65 | This will look for views in the `resources/views` directory in the root of your package. 66 | 67 | ## Creating a Blade Mail Template 68 | 69 | 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. 70 | 71 | ``` 72 | // 'resources/views/emails/welcome.blade.php' 73 |

74 | Dear reader, 75 | 76 | Post title: {{ $post->title }} 77 | 78 | -- Sent from the blogpackage 79 |

80 | ``` 81 | 82 | ## Testing Mailing 83 | 84 | To test that e-mailing works and the mail contains all the right information, [Laravel's Mail facade](https://laravel.com/docs/mocking#mail-fake) offers a built-in `fake()` method which makes it easy to swap the _real_ mailer for a mock in our tests. 85 | 86 | To demonstrate how to test our e-mail, create a new `WelcomeMailTest` in the `tests/unit` directory. Next, in the test: 87 | 88 | - Switch the Mail implementation for a mock using `Mail::fake()`. 89 | - Create a `Post` using our factory (see section [Models and Migrations](#08-models-and-migrations)). 90 | - Assert that at this stage, no e-mails are sent using `assertNothingSent()`. 91 | - Send a new `WelcomeMail` mailable, passing in the `Post` model. 92 | - Assert that the e-mail was sent and contains the correct `Post` model using `assertSent()`. 93 | 94 | ```php 95 | create(['title' => 'Fake Title']); 112 | 113 | Mail::assertNothingSent(); 114 | 115 | Mail::to('test@example.com')->send(new WelcomeMail($post)); 116 | 117 | Mail::assertSent(WelcomeMail::class, function ($mail) use ($post) { 118 | return $mail->post->id === $post->id 119 | && $mail->post->title === 'Fake Title'; 120 | }); 121 | } 122 | } 123 | ``` 124 | 125 | With this passing test, you can be sure that your package can now send e-mails. 126 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction" 3 | description: "Learn to create Laravel specific PHP packages from scratch, following this open documentation. Contributions are welcomed." 4 | tags: ["Laravel", "PHP Package", "Package Development"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Introduction to Package Development 10 | 11 | In my experience, learning to develop a package for Laravel can be quite challenging, which is why I previously wrote [a blog series about that](https://johnbraun.blog/posts/creating-a-laravel-package-1). 12 | 13 | Over time, I began thinking this topic deserves proper documentation, rather than a couple of posts that only cover _my_ insights. That's where I feel this open-source documentation on Laravel Package Development will come in. I've bundled up my blog posts and expanded on a couple of more topics in separate chapters. Contributions (in the form of pull requests) are highly welcomed and appreciated. I hope this website can become a place to share knowledge on Laravel package development to help developers get a head start. 14 | 15 | You are highly encouraged to participate and [contribute to this project](https://github.com/Jhnbrn90/LaravelPackage.com). Please feel free to submit a PR, even only for a typo. 16 | 17 | First of all, I want to thank Marcel Pociot. His clear and structured [video course](https://phppackagedevelopment.com/) encouraged me to create PHP packages myself. I can highly recommend his video course if you want to learn how to make (framework agnostic) PHP packages. 18 | 19 | --- 20 | 21 | 💡 Would you rather watch than read? The famous and reputable package builders from **Spatie** launched a full video course on [Laravel Package Development](https://laravelpackage.training). 22 | 23 | ## Reasons to Develop a Package 24 | 25 | You might encounter a scenario where you want to reuse some feature(s) of your application in other applications, open-source a specific functionality or keep related code together but separate it from your main application. In those cases, it makes sense to extract parts to a package. Packages or "libraries" provide an easy way to add additional functionality to existing applications and focus on a single feature. 26 | 27 | ## Companion Package 28 | 29 | In this documentation, we'll build a demo package along the way (called "BlogPackage") by introducing the listed functionalities one-by-one. Make sure to check out the finished version of this [companion package](https://github.com/Jhnbrn90/BlogPackage) to have a handy reference, for example, when something doesn't work as expected. The demo package contains a test suite comprising unit and feature tests for the covered topics. 30 | 31 | ## Composer & Packagist 32 | 33 | There are nearly 240,000 packages available on [Packagist](https://packagist.org/), the primary repository for PHP packages at the time of writing. 34 | 35 | Packages are downloaded and installed using [Composer](https://getcomposer.org/) - PHP's package management system - which manages dependencies within a project. 36 | 37 | To install a package in your existing Laravel project, the `composer require /` command will download all necessary files into a `/vendor` directory of your project where all your third party packages live, separated by vendor name. Consequently, the content from these packages is separated from your application code, which means this particular code is maintained by someone else, most often by the creator of that package. Whenever the package needs an update, run `composer update` to get the latest (compatible) version of your packages. 38 | 39 | ## Tools and Helpers 40 | 41 | The first chapter will address the basic structure of a package. While it is good to understand the general structure of a package, check out one of the following helpful tools to instantly set-up the basic skeleton. 42 | 43 | - [Package Skeleton by Spatie](https://github.com/spatie/package-skeleton-laravel) 44 | This package skeleton by Spatie offers a great starting point for setting up a Laravel package from scratch. Besides the essential components of a Laravel Package, the skeleton comes with a GitHub specific configuration including a set of (CI) workflows for GitHub actions. They also offer a skeleton for [generic PHP packages](https://github.com/spatie/package-skeleton-php). 45 | 46 | - [Laravel Package Boilerplate](https://laravelpackageboilerplate.com/) 47 | This tool by Marcel Pociot allows you to generate a basic template for Laravel specific and generic PHP packages that can be downloaded as a `.zip` file. 48 | 49 | - [Laravel Packager](https://github.com/Jeroen-G/laravel-packager) 50 | This package by Jeroen-G provides a CLI tool to quickly scaffold packages from within an existing Laravel application. The package was featured on [Laracasts](https://laracasts.com/series/building-laracasts/episodes/3) in the _Building Laracasts series_. 51 | 52 | - [Laravel Packager Hermes](https://github.com/DelveFore/laravel-packager-hermes) 53 | This package by DelveFore is an extension of the Laravel Packager package, enabling usage of Artisan commands within that package to quickly generate Laravel specific classes. Currently, it only supports the scaffolding of `Controllers`. 54 | 55 | - [Orchestral Canvas](https://github.com/orchestral/canvas) 56 | The Orchestral Canvas package offers code generators and replicates all of the `make` artisan commands available in your basic Laravel application. 57 | 58 | - [Yeoman Laravel Package Scaffolder](https://github.com/verschuur/generator-laravel-package-scaffolder) 59 | This package provides a standalone generator to quickly scaffold a Laravel package. It will generate a skeleton structure, a ready-to-go composer.json file, and a fully configured service provider. Just uncomment what you need and start developing. 60 | 61 | - [Laravel Packer](https://github.com/bitfumes/laravel-packer) 62 | A PHP package offering a command-line tool to scaffold a basic package directory structure and `composer.json` file and provides the `make` artisan commands within your package. 63 | 64 | - [Laravel Package Maker](https://github.com/naoray/laravel-package-maker) 65 | A PHP package that provides all the Laravel `make` commands for package development. It uses Composer's repositories feature to symlink your test app with your package to make testing as easy as possible. 66 | -------------------------------------------------------------------------------- /docs/04-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Testing" 3 | description: "Testing is an essential part of every package to ensure proper behavior and allow refactoring with confidence. This section explains how to set up a testing environment using PHPUnit to create robust packages." 4 | tags: ["Testing", "PHPUnit", "Directory Structure"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Testing 10 | 11 | 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. 12 | 13 | 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. 14 | 15 | ## Installing PHPUnit 16 | 17 | There are many options to test behavior in PHP. However, we'll stay close to Laravel's defaults, which uses the excellent tool PHPUnit. 18 | 19 | Install PHPUnit as a dev-dependency in our package: 20 | 21 | ```bash 22 | composer require --dev phpunit/phpunit 23 | ``` 24 | 25 | **Note:** you might need to install a specific version if you're developing a package for an older version of Laravel. 26 | 27 | To configure PHPUnit, create a `phpunit.xml` file in the root directory of the package. 28 | Then, copy the following template to use an in-memory sqlite database and enable colorful reporting. 29 | 30 | `phpunit.xml`: 31 | 32 | ```xml 33 | 34 | 48 | 49 | 50 | src/ 51 | 52 | 53 | 54 | 55 | ./tests/Unit 56 | 57 | 58 | ./tests/Feature 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ``` 67 | 68 | Note the dummy `APP_KEY` in the example above. This environment variable is consumed by [Laravel's encrypter](https://laravel.com/docs/8.x/encryption#using-the-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. 69 | 70 | ## Directory Structure 71 | 72 | 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: 73 | 74 | ```json 75 | - tests 76 | - Feature 77 | - Unit 78 | TestCase.php 79 | ``` 80 | 81 | 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: 82 | 83 | - `getPackageProviders()` 84 | - `getEnvironmentSetUp()` 85 | - `setUp()` 86 | 87 | Let's look at these methods one by one. 88 | 89 | `setUp()` 90 | 91 | 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). 92 | 93 | --- 94 | 95 | `getEnvironmentSetUp()` 96 | 97 | 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). 98 | 99 | --- 100 | 101 | `getPackageProviders()` 102 | 103 | 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. 104 | 105 | --- 106 | 107 | In a package, `TestCase` will inherit from the Orchestra Testbench TestCase: 108 | 109 | ```php 110 | // 'tests/TestCase.php' 111 | 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. 64 | 65 | ### Version Constraints 66 | 67 | Composer supports various [version constraints](https://getcomposer.org/doc/articles/versions.md#writing-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: 68 | 69 | - 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. 70 | 71 | - 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. 72 | 73 | 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. 74 | 75 | Alternatively, Composer allows for more strict constraints: 76 | 77 | - 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. 78 | 79 | - 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. 80 | -------------------------------------------------------------------------------- /docs/02-development-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Development Environment" 3 | description: "Set up a stable environment for package development. Starting with installing Composer, configuring package details and PSR autoloading in composer.json to pulling in the package locally and testing with Orchestra Testbench." 4 | tags: 5 | [ 6 | "development setup", 7 | "composer", 8 | "package skeleton", 9 | "PSR", 10 | "namespacing", 11 | "testing", 12 | "testbench", 13 | ] 14 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 15 | date: 2019-09-17 16 | --- 17 | 18 | # Development Environment 19 | 20 | ## Installing Composer 21 | 22 | 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](https://getcomposer.org/download/). 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`. 23 | 24 | ## Package Skeleton 25 | 26 | 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. 27 | 28 | For example, I store all packages in `~/packages/` and my Laravel apps live in `~/websites/`. 29 | 30 | ## Composer.json 31 | 32 | 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. 33 | 34 | 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. 35 | 36 | An example `composer.json` is highlighted below. 37 | 38 | ```json 39 | { 40 | "name": "johndoe/blogpackage", 41 | "description": "A demo package", 42 | "type": "library", 43 | "license": "MIT", 44 | "authors": [ 45 | { 46 | "name": "John Doe", 47 | "email": "john@doe.com" 48 | } 49 | ], 50 | "require": {} 51 | } 52 | ``` 53 | 54 | Alternatively, you can create your `composer.json` file by running `composer init` in your empty package directory. 55 | 56 | If you're planning to **publish** the package, it is important to choose an appropriate package [type](https://getcomposer.org/doc/04-schema.md#type) (in our case, a "library") and license (e.g., "MIT"). Learn more about open source licenses at [ChooseALicense.com](https://choosealicense.com/). 57 | 58 | ## Namespacing 59 | 60 | 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`). 61 | 62 | We can register our namespace under the "psr-4" autoload key in the `composer.json` file as follows (replace the namespace with your own): 63 | 64 | ```json 65 | { 66 | ..., 67 | 68 | "require": {}, 69 | 70 | "autoload": { 71 | "psr-4": { 72 | "JohnDoe\\BlogPackage\\": "src" 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ## PSR-4 Autoloading 79 | 80 | Now, you might wonder why we needed a "psr-4" key. PSR stands for PHP Standards Recommendations devised by the [PHP Framework Interoperability Group](https://www.php-fig.org/) (PHP-FIG). This group of 20 members, representing a cross-section of the PHP community, proposed a [series of PSR's](https://www.php-fig.org/psr/). 81 | 82 | In the list, PSR-4 represents a recommendation regarding autoloading classes from file paths, replacing the until then prevailing [PSR-0 autoloading standard](https://www.php-fig.org/psr/psr-0/). 83 | 84 | 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](https://stackoverflow.com/questions/24868586/what-are-the-differences-between-psr-0-and-psr-4/50226226#50226226) on StackOverflow has a clear description of how PSR-0 and PSR-4 work. 85 | 86 | PSR-0 87 | 88 | ```json 89 | "autoload": { 90 | "psr-0": { 91 | "Book\\": "src/", 92 | "Vehicle\\": "src/" 93 | } 94 | } 95 | ``` 96 | 97 | - Looking for `Book\History\UnitedStates` in `src/Book/History/UnitedStates.php` 98 | 99 | - Looking for `Vehicle\Air\Wings\Airplane` in `src/Vehicle/Air/Wings/Airplane.php` 100 | 101 | PSR-4 102 | 103 | ```json 104 | "autoload": { 105 | "psr-4": { 106 | "Book\\": "src/", 107 | "Vehicle\\": "src/" 108 | } 109 | } 110 | ``` 111 | 112 | - Looking for `Book\History\UnitedStates` in `src/History/UnitedStates.php` 113 | 114 | - Looking for `Vehicle\Air\Wings\Airplane` in `src/Air/Wings/Airplane.php` 115 | 116 | ## Importing the Package Locally 117 | 118 | To help with development, you can require a local package in a local Laravel project. 119 | 120 | 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**. 121 | 122 | 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): 123 | 124 | ```json 125 | { 126 | "scripts": { ... }, 127 | 128 | "repositories": [ 129 | { 130 | "type": "path", 131 | "url": "../../packages/blogpackage" 132 | } 133 | ] 134 | } 135 | ``` 136 | 137 | You can now require your local package in the Laravel application using your chosen namespace of the package. Following our example, this would be: 138 | 139 | ```bash 140 | composer require johndoe/blogpackage 141 | ``` 142 | 143 | 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: 144 | 145 | ```json 146 | { 147 | "scripts": { ... }, 148 | 149 | "repositories": [ 150 | { 151 | "type": "path", 152 | "url": "../../packages/blogpackage", 153 | "options": { 154 | "symlink": false 155 | } 156 | } 157 | ] 158 | } 159 | ``` 160 | 161 | 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: 162 | 163 | ```json 164 | { 165 | "scripts": { ... }, 166 | 167 | "repositories": [ 168 | { 169 | "type": "path", 170 | "url": "../../packages/*" 171 | } 172 | ] 173 | } 174 | ``` 175 | 176 | **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. 177 | 178 | 179 | 180 | ## Orchestra Testbench 181 | 182 | 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. 183 | 184 | To use these components in our package, we'll require the [Orchestra Testbench](https://github.com/orchestral/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. 185 | 186 | ```bash 187 | composer require --dev "orchestra/testbench=^6.0" 188 | ``` 189 | 190 | The full compatibility table of the Orchestra Testbench is shown below, taken from [the original documentation](https://github.com/orchestral/testbench). 191 | 192 | | Laravel | Testbench | 193 | | :------ | :-------- | 194 | | 9.x | 7.x | 195 | | 8.x | 6.x | 196 | | 7.x | 5.x | 197 | | 6.x | 4.x | 198 | | 5.8.x | 3.8.x | 199 | | 5.7.x | 3.7.x | 200 | | 5.6.x | 3.6.x | 201 | | 5.5.x | 3.5.x | 202 | | 5.4.x | 3.4.x | 203 | | 5.3.x | 3.3.x | 204 | | 5.2.x | 3.2.x | 205 | | 5.1.x | 3.1.x | 206 | | 5.0.x | 3.0.x | 207 | 208 | 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). 209 | 210 | 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. 211 | 212 | ```php 213 | // 'vendor/orchestra/testbench-core/src/Concerns/CreatesApplication.php' 214 | /** 215 | * Get base path. 216 | * 217 | * @return string 218 | */ 219 | protected function getBasePath() 220 | { 221 | return \realpath(__DIR__.'/../../laravel'); 222 | } 223 | ``` 224 | -------------------------------------------------------------------------------- /docs/10-events-and-listeners.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Events and Listeners" 3 | description: "Creating and testing custom Events and Listeners in your package." 4 | tags: ["Events", "Listeners", "Testing Events", "Testing Listeners"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Events & Listeners 10 | 11 | Your package may want to offer support for hooking into Laravel's Events and Listeners. 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | ## Creating a New Event 18 | 19 | First, let's emit an event whenever a new `Post` is created via the route we set up earlier. 20 | 21 | 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`. 22 | 23 | ```php 24 | // 'src/Events/PostWasCreated.php' 25 | post = $post; 42 | } 43 | } 44 | ``` 45 | 46 | When creating a new `Post` in the `PostController`, we can now emit this event (don't forget to import it): 47 | 48 | ```php 49 | // 'src/Http/Controllers/PostController.php' 50 | posts()->create([...]); 61 | 62 | event(new PostWasCreated($post)); 63 | 64 | return redirect(...); 65 | } 66 | } 67 | ``` 68 | 69 | ### Testing the Event was Emitted 70 | 71 | To be sure this event is successfully fired, add a test to our `CreatePostTest` _feature_ test. We can easily fake Laravel's `Event` facade and make assertions (see [Laravel documentation on Fakes](https://laravel.com/docs/mocking#event-fake)) that the event was emitted **and** about the passed `Post` model. 72 | 73 | ```php 74 | // 'tests/Feature/CreatePostTest.php' 75 | use Illuminate\Support\Facades\Event; 76 | use JohnDoe\BlogPackage\Events\PostWasCreated; 77 | use JohnDoe\BlogPackage\Models\Post; 78 | 79 | class CreatePostTest extends TestCase 80 | { 81 | use RefreshDatabase; 82 | 83 | // other tests 84 | 85 | /** @test */ 86 | function an_event_is_emitted_when_a_new_post_is_created() 87 | { 88 | Event::fake(); 89 | 90 | $author = User::factory()->create(); 91 | 92 | $this->actingAs($author)->post(route('posts.store'), [ 93 | 'title' => 'A valid title', 94 | 'body' => 'A valid body', 95 | ]); 96 | 97 | $post = Post::first(); 98 | 99 | Event::assertDispatched(PostWasCreated::class, function ($event) use ($post) { 100 | return $event->post->id === $post->id; 101 | }); 102 | } 103 | } 104 | ``` 105 | 106 | Now that we know that our event is fired correctly let's hook up our listener. 107 | 108 | ## Creating a New Listener 109 | 110 | 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`: 111 | 112 | ```php 113 | // 'src/Listeners/UpdatePostTitle.php' 114 | post->update([ 125 | 'title' => 'New: ' . $event->post->title 126 | ]); 127 | } 128 | } 129 | ``` 130 | 131 | ## Testing the Listener 132 | 133 | 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. 134 | 135 | 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: 136 | 137 | ```php 138 | // 'tests/Feature/CreatePostTest.php' 139 | /** @test */ 140 | function a_newly_created_posts_title_will_be_changed() 141 | { 142 | $post = Post::factory()->create([ 143 | 'title' => 'Initial title', 144 | ]); 145 | 146 | $this->assertEquals('Initial title', $post->title); 147 | 148 | (new UpdatePostTitle())->handle( 149 | new PostWasCreated($post) 150 | ); 151 | 152 | $this->assertEquals('New: ' . 'Initial title', $post->fresh()->title); 153 | } 154 | ``` 155 | 156 | 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. 157 | 158 | ## Creating an Event Service Provider 159 | 160 | 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). 161 | 162 | First, create a new folder `Providers` in the `src/` directory. Add a file called `EventServiceProvider.php` and register our Event and Listener: 163 | 164 | ```php 165 | // 'src/Providers/EventServiceProvider.php' 166 | [ 178 | UpdatePostTitle::class, 179 | ] 180 | ]; 181 | 182 | /** 183 | * Register any events for your application. 184 | * 185 | * @return void 186 | */ 187 | public function boot() 188 | { 189 | parent::boot(); 190 | } 191 | } 192 | ``` 193 | 194 | ## Registering the Event Service Provider 195 | 196 | In our main `BlogPackageServiceProvider` we need to register our Event Service Provider in the `register()` method, as follows (don't forget to import it): 197 | 198 | ```php 199 | // 'BlogPackageServiceProvider.php' 200 | use JohnDoe\BlogPackage\Providers\EventServiceProvider; 201 | 202 | public function register() 203 | { 204 | $this->app->register(EventServiceProvider::class); 205 | } 206 | ``` 207 | 208 | ## Testing the Event/Listener Cascade 209 | 210 | 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: 211 | 212 | ```php 213 | // 'tests/Feature/CreatePostTest.php' 214 | /** @test */ 215 | function the_title_of_a_post_is_updated_whenever_a_post_is_created() 216 | { 217 | $author = factory(User::class)->create(); 218 | 219 | $this->actingAs($author)->post(route('posts.store'), [ 220 | 'title' => 'A valid title', 221 | 'body' => 'A valid body', 222 | ]); 223 | 224 | $post = Post::first(); 225 | 226 | $this->assertEquals('New: ' . 'A valid title', $post->title); 227 | } 228 | ``` 229 | 230 | This test is green, but what if we run the full suite? 231 | 232 | ## Fixing the Failing Test 233 | 234 | If we run the full suite with `composer test`, we see we have one failing test: 235 | 236 | ```php 237 | There was 1 failure: 238 | 239 | 1) JohnDoe\BlogPackage\Tests\Feature\CreatePostTest::authenticated_users_can_create_a_post 240 | Failed asserting that two strings are equal. 241 | --- Expected 242 | +++ Actual 243 | @@ @@ 244 | -'My first fake title' 245 | +'New: My first fake title' 246 | ``` 247 | 248 | The failing test is a regression from the Event we've introduced. There are two ways to fix this error: 249 | 250 | 1. change the expected title in the authenticated_users_can_create_a_post test 251 | 2. by faking any events before the test runs, which inhibits the actual handlers to be called 252 | 253 | It is very situational what happens to be the best option but let's go with **option 2** for now. 254 | 255 | ```php 256 | // 'tests/Feature/CreatePostTest.php' 257 | /** @test */ 258 | function authenticated_users_can_create_a_post() 259 | { 260 | Event::fake(); 261 | 262 | $this->assertCount(0, Post::all()); 263 | // the rest of the test... 264 | ``` 265 | 266 | All tests are green, so let's move on to the next topic. 267 | -------------------------------------------------------------------------------- /docs/11-middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Middleware" 3 | description: "Explore the different types of Middleware and how to make use of them within a Laravel package. Additionally, writing tests for the Middleware will be explained." 4 | tags: 5 | [ 6 | "Middleware", 7 | "Before Middlware", 8 | "After Middleware", 9 | "Route Middleware", 10 | "Middleware Groups", 11 | "Global Middleware", 12 | "Testing Middleware", 13 | ] 14 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 15 | date: 2019-09-17 16 | --- 17 | 18 | # Middleware 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | ## Creating Middleware 25 | 26 | 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"). 27 | 28 | Before discussing the two types of middleware, first create a new `Middleware` folder in the package's `src/Http` directory. 29 | 30 | ## Before Middleware 31 | 32 | 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: 33 | 34 | ```php 35 | has('title')) { 69 | $request->merge([ 70 | 'title' => ucfirst($request->title) 71 | ]); 72 | } 73 | 74 | return $next($request); 75 | } 76 | } 77 | ``` 78 | 79 | ## Testing Before Middleware 80 | 81 | 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. 82 | 83 | 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: 84 | 85 | ```php 86 | // 'tests/Unit/CapitalizeMiddlewareTest.php' 87 | merge(['title' => 'some title']); 105 | 106 | // when we pass the request to this middleware, 107 | // it should've capitalized the title 108 | (new CapitalizeTitle())->handle($request, function ($request) { 109 | $this->assertEquals('Some title', $request->title); 110 | }); 111 | } 112 | } 113 | ``` 114 | 115 | ## After Middleware 116 | 117 | 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: 118 | 119 | ```php 120 | handle($request, function ($request) { }); 164 | 165 | $this->assertStringContainsString('Hello World', $response); 166 | } 167 | } 168 | ``` 169 | 170 | 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**. 171 | 172 | ## Global middleware 173 | 174 | Global middleware is, as the name implies, globally applied. Each request will pass through these middlewares. 175 | 176 | 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: 177 | 178 | ```php 179 | // 'BlogPackageServiceProvider.php' 180 | use Illuminate\Foundation\Http\Kernel; 181 | use JohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle; 182 | 183 | public function boot(Kernel $kernel) 184 | { 185 | // other things ... 186 | 187 | $kernel->pushMiddleware(CapitalizeTitle::class); 188 | } 189 | ``` 190 | 191 | This will push our middleware into the application's array of globally registered middleware. 192 | 193 | ## Route middleware 194 | 195 | 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. 196 | 197 | 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. 198 | 199 | Therefore, we can register an alias to this middleware in the resolved Router class, from within the `boot()` method of our service provider. 200 | 201 | Here's how to register the `capitalize` alias for this middleware: 202 | 203 | ```php 204 | // 'BlogPackageServiceProvider.php' 205 | use Illuminate\Routing\Router; 206 | use JohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle; 207 | 208 | public function boot() 209 | { 210 | // other things ... 211 | 212 | $router = $this->app->make(Router::class); 213 | $router->aliasMiddleware('capitalize', CapitalizeTitle::class); 214 | } 215 | ``` 216 | 217 | We can apply this middleware from within our controller by requiring it from the constructor: 218 | 219 | ```php 220 | // 'src/Http/Controllers/PostController.php' 221 | class PostController extends Controller 222 | { 223 | public function __construct() 224 | { 225 | $this->middleware('capitalize'); 226 | } 227 | 228 | // other methods... (will use this middleware) 229 | } 230 | ``` 231 | 232 | ### Middleware Groups 233 | 234 | 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. 235 | 236 | To do so, tell the router to _push_ the middleware to a specific group (in this example, `web`): 237 | 238 | ```php 239 | // 'BlogPackageServiceProvider.php' 240 | use Illuminate\Routing\Router; 241 | use JohnDoe\BlogPackage\Http\Middleware\CapitalizeTitle; 242 | 243 | public function boot() 244 | { 245 | // other things ... 246 | 247 | $router = $this->app->make(Router::class); 248 | $router->pushMiddlewareToGroup('web', CapitalizeTitle::class); 249 | } 250 | ``` 251 | 252 | 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. 253 | 254 | ## Feature Testing Middleware 255 | 256 | Regardless of whether we registered the middleware globally or route specifically, we can test that the middleware is applied when making a request. 257 | 258 | 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. 259 | 260 | ```php 261 | // 'tests/Feature/CreatePostTest.php' 262 | /** @test */ 263 | function creating_a_post_will_capitalize_the_title() 264 | { 265 | $author = User::factory()->create(); 266 | 267 | $this->actingAs($author)->post(route('posts.store'), [ 268 | 'title' => 'some title that was not capitalized', 269 | 'body' => 'A valid body', 270 | ]); 271 | 272 | $post = Post::first(); 273 | 274 | // 'New: ' was added by our event listener 275 | $this->assertEquals('New: Some title that was not capitalized', $post->title); 276 | } 277 | ``` 278 | 279 | With the tests returning green, we've covered adding Middleware to your package. 280 | -------------------------------------------------------------------------------- /docs/06-artisan-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Commands" 3 | description: "Creating and testing custom Artisan Commands in your package. Additionally, this section covers testing a command, without publishing it within your package for testing purposes." 4 | tags: ["Artisan", "Commands", "Testing Commands", "Test-Only Commands"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Artisan Commands 10 | 11 | Laravel ships with an executable `artisan` file, which offers a number of helpful commands through a command-line interface (CLI). 12 | 13 | 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](https://laravel.com/docs/artisan). 14 | 15 | 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`. 16 | 17 | ## Creating a New Command 18 | 19 | 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. 20 | 21 | ```php 22 | // 'src/Console/InstallBlogPackage.php' 23 | info('Installing BlogPackage...'); 39 | 40 | $this->info('Publishing configuration...'); 41 | 42 | if (! $this->configExists('blogpackage.php')) { 43 | $this->publishConfiguration(); 44 | $this->info('Published configuration'); 45 | } else { 46 | if ($this->shouldOverwriteConfig()) { 47 | $this->info('Overwriting configuration file...'); 48 | $this->publishConfiguration($force = true); 49 | } else { 50 | $this->info('Existing configuration was not overwritten'); 51 | } 52 | } 53 | 54 | $this->info('Installed BlogPackage'); 55 | } 56 | 57 | private function configExists($fileName) 58 | { 59 | return File::exists(config_path($fileName)); 60 | } 61 | 62 | private function shouldOverwriteConfig() 63 | { 64 | return $this->confirm( 65 | 'Config file already exists. Do you want to overwrite it?', 66 | false 67 | ); 68 | } 69 | 70 | private function publishConfiguration($forcePublish = false) 71 | { 72 | $params = [ 73 | '--provider' => "JohnDoe\BlogPackage\BlogPackageServiceProvider", 74 | '--tag' => "config" 75 | ]; 76 | 77 | if ($forcePublish === true) { 78 | $params['--force'] = true; 79 | } 80 | 81 | $this->call('vendor:publish', $params); 82 | } 83 | } 84 | ``` 85 | 86 | ## Registering a Command in the Service Provider 87 | 88 | We need to present this package functionality to the end-user, thus registering it in the package's service provider. 89 | 90 | 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: 91 | 92 | ```php 93 | // 'BlogPackageServiceProvider.php' 94 | 95 | use JohnDoe\BlogPackage\Console\InstallBlogPackage; 96 | 97 | public function boot() 98 | { 99 | // Register the command if we are using the application via the CLI 100 | if ($this->app->runningInConsole()) { 101 | $this->commands([ 102 | InstallBlogPackage::class, 103 | ]); 104 | } 105 | } 106 | ``` 107 | 108 | ## Scheduling a Command in the Service Provider 109 | 110 | 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: 111 | 112 | ```php 113 | // 'BlogPackageServiceProvider.php' 114 | 115 | use Illuminate\Console\Scheduling\Schedule; 116 | 117 | public function boot() 118 | { 119 | // Schedule the command if we are using the application via the CLI 120 | if ($this->app->runningInConsole()) { 121 | $this->app->booted(function () { 122 | $schedule = $this->app->make(Schedule::class); 123 | $schedule->command('some:command')->everyMinute(); 124 | }); 125 | } 126 | } 127 | ``` 128 | 129 | ## Testing a Command 130 | 131 | To test that our Command class works, let's create a new unit test called `InstallBlogPackageTest.php` in the Unit test folder. 132 | 133 | 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. 134 | 135 | ```php 136 | // 'tests/Unit/InstallBlogPackageTest.php' 137 | assertFalse(File::exists(config_path('blogpackage.php'))); 156 | 157 | Artisan::call('blogpackage:install'); 158 | 159 | $this->assertTrue(File::exists(config_path('blogpackage.php'))); 160 | } 161 | } 162 | ``` 163 | 164 | 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`. 165 | 166 | ```php 167 | // 'tests/Unit/InstallBlogPackageTest.php' 168 | /** @test */ 169 | public function when_a_config_file_is_present_users_can_choose_to_not_overwrite_it() 170 | { 171 | // Given we have already have an existing config file 172 | File::put(config_path('blogpackage.php'), 'test contents'); 173 | $this->assertTrue(File::exists(config_path('blogpackage.php'))); 174 | 175 | // When we run the install command 176 | $command = $this->artisan('blogpackage:install'); 177 | 178 | // We expect a warning that our configuration file exists 179 | $command->expectsConfirmation( 180 | 'Config file already exists. Do you want to overwrite it?', 181 | // When answered with "no" 182 | 'no' 183 | ); 184 | 185 | // We should see a message that our file was not overwritten 186 | $command->expectsOutput('Existing configuration was not overwritten'); 187 | 188 | // Assert that the original contents of the config file remain 189 | $this->assertEquals('test contents', file_get_contents(config_path('blogpackage.php'))); 190 | 191 | // Clean up 192 | unlink(config_path('blogpackage.php')); 193 | } 194 | 195 | /** @test */ 196 | public function when_a_config_file_is_present_users_can_choose_to_do_overwrite_it() 197 | { 198 | // Given we have already have an existing config file 199 | File::put(config_path('blogpackage.php'), 'test contents'); 200 | $this->assertTrue(File::exists(config_path('blogpackage.php'))); 201 | 202 | // When we run the install command 203 | $command = $this->artisan('blogpackage:install'); 204 | 205 | // We expect a warning that our configuration file exists 206 | $command->expectsConfirmation( 207 | 'Config file already exists. Do you want to overwrite it?', 208 | // When answered with "yes" 209 | 'yes' 210 | ); 211 | 212 | // execute the command to force override 213 | $command->execute(); 214 | 215 | $command->expectsOutput('Overwriting configuration file...'); 216 | 217 | // Assert that the original contents are overwritten 218 | $this->assertEquals( 219 | file_get_contents(__DIR__.'/../config/config.php'), 220 | file_get_contents(config_path('blogpackage.php')) 221 | ); 222 | 223 | // Clean up 224 | unlink(config_path('blogpackage.php')); 225 | } 226 | ``` 227 | 228 | ## Hiding a Command 229 | 230 | 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. 231 | 232 | ```php 233 | class InstallBlogPackage extends Command 234 | { 235 | protected $hidden = true; 236 | 237 | protected $signature = 'blogpackage:install'; 238 | 239 | protected $description = 'Install the BlogPackage'; 240 | 241 | public function handle() 242 | { 243 | // ... 244 | } 245 | } 246 | ``` 247 | 248 | ## Creating a Generator Command 249 | 250 | 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. 251 | 252 | To create a Generator Command, you have to extend the `Illuminate\Console\GeneratorCommand` class, and override the following properties and methods: 253 | 254 | - `protected $name`: name of the command 255 | - `protected $description`: description of the command 256 | - `protected $type`: the type of class the command generates 257 | - `protected function getStub()`: method returning the path of the stub template file 258 | - `protected function getDefaultNamespace($rootNamespace)`: the default namespace of the generated class 259 | - `public function handle()`: the body of the command 260 | 261 | The `GeneratorCommand` base class provides some helper methods: 262 | 263 | - `getNameInput()`: returns the name passed from command line execution 264 | - `qualifyClass(string $name)`: returns the qualified class name for a given class name 265 | - `getPath(string $name)`: returns the file path for a given name 266 | 267 | Consider the following example for the `php artisan make:foo MyFoo` command: 268 | 269 | ```php 270 | doOtherOperations(); 299 | } 300 | 301 | protected function doOtherOperations() 302 | { 303 | // Get the fully qualified class name (FQN) 304 | $class = $this->qualifyClass($this->getNameInput()); 305 | 306 | // get the destination path, based on the default namespace 307 | $path = $this->getPath($class); 308 | 309 | $content = file_get_contents($path); 310 | 311 | // Update the file content with additional data (regular expressions) 312 | 313 | file_put_contents($path, $content); 314 | } 315 | } 316 | ``` 317 | 318 | Note that the Generator Command will export the class to a directory **based on the namespace** specified in the `getDefaultNamespace()` method. 319 | 320 | As with the `InstallBlogPackage` command, we have to register this new command in the `BlogPackageServiceProvider`: 321 | 322 | ```php 323 | // 'BlogPackageServiceProvider.php' 324 | use JohnDoe\BlogPackage\Console\{InstallBlogPackage, MakeFooCommand}; 325 | 326 | public function boot() 327 | { 328 | if ($this->app->runningInConsole()) { 329 | // publish config file 330 | 331 | $this->commands([ 332 | InstallBlogPackage::class, 333 | MakeFooCommand::class, // registering the new command 334 | ]); 335 | } 336 | } 337 | ``` 338 | 339 | ### Creating a stub 340 | 341 | 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: 342 | 343 | ```php 344 | // 'stubs/foo.php.stub' 345 | assertFalse(File::exists($fooClass)); 389 | 390 | // Run the make command 391 | Artisan::call('make:foo MyFooClass'); 392 | 393 | // Assert a new file is created 394 | $this->assertTrue(File::exists($fooClass)); 395 | 396 | // Assert the file contains the right contents 397 | $expectedContents = <<assertEquals($expectedContents, file_get_contents($fooClass)); 414 | } 415 | } 416 | ``` 417 | 418 | ## Creating a Test-Only Command 419 | 420 | 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. 421 | 422 | 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](https://twitter.com/marcelpociot/status/1219274939565514754): 423 | 424 | ```php 425 | add(app(TestCommand::class)); 441 | }); 442 | 443 | // Running the command 444 | Artisan::call('test-command:run'); 445 | 446 | // Assertions... 447 | } 448 | } 449 | ``` 450 | -------------------------------------------------------------------------------- /docs/09-routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Routing, Views and Controllers" 3 | description: "Expose (custom) routes in your package, which call a controller action and render views provided by the package. This chapter will additionally cover testing of routes, controllers, and views." 4 | tags: 5 | [ 6 | "Routing", 7 | "Controllers", 8 | "Views", 9 | "RESTful", 10 | "Testing Routing", 11 | "Testing Controllers", 12 | "Testing Views", 13 | ] 14 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 15 | date: 2019-09-17 16 | --- 17 | 18 | # Routing 19 | 20 | Sometimes you want to expose additional routes to the end-user of your package. 21 | 22 | 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: 23 | 24 | - show all posts ('index') 25 | - show a single post ('show') 26 | - store a new post ('store') 27 | 28 | ## Controllers 29 | 30 | ### Creating a Base Controller 31 | 32 | We want to create a `PostController`. 33 | 34 | 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`: 35 | 36 | ```php 37 | // 'src/Http/Controllers/Controller.php' 38 | check()) { 80 | abort (403, 'Only authenticated users can create new posts.'); 81 | } 82 | 83 | request()->validate([ 84 | 'title' => 'required', 85 | 'body' => 'required', 86 | ]); 87 | 88 | // Assume the authenticated user is the post's author 89 | $author = auth()->user(); 90 | 91 | $post = $author->posts()->create([ 92 | 'title' => request('title'), 93 | 'body' => request('body'), 94 | ]); 95 | 96 | return redirect(route('posts.show', $post)); 97 | } 98 | } 99 | ``` 100 | 101 | ## Routes 102 | 103 | ### Defining Routes 104 | 105 | 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. 106 | 107 | ```php 108 | // 'routes/web.php' 109 | name('posts.index'); 115 | Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show'); 116 | Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); 117 | ``` 118 | 119 | ### Registering Routes in the Service Provider 120 | 121 | Before we can use these routes, we need to register them in the `boot()` method of our Service Provider: 122 | 123 | ```php 124 | // 'BlogPackageServiceProvider.php' 125 | public function boot() 126 | { 127 | // ... other things 128 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 129 | } 130 | ``` 131 | 132 | ### Configurable Route Prefix and Middleware 133 | 134 | 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. 135 | 136 | The following examples use a namespace of `blogpackage`. Don't forget to replace this with your package's namespace. 137 | 138 | ```php 139 | // 'BlogPackageServiceProvider.php' 140 | use Illuminate\Support\Facades\Route; 141 | 142 | public function boot() 143 | { 144 | // ... other things 145 | $this->registerRoutes(); 146 | } 147 | 148 | protected function registerRoutes() 149 | { 150 | Route::group($this->routeConfiguration(), function () { 151 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 152 | }); 153 | } 154 | 155 | protected function routeConfiguration() 156 | { 157 | return [ 158 | 'prefix' => config('blogpackage.prefix'), 159 | 'middleware' => config('blogpackage.middleware'), 160 | ]; 161 | } 162 | ``` 163 | 164 | Specify a default route prefix and middleware in the package's `config.php` file: 165 | 166 | ```php 167 | 'prefix' => 'blogger', 168 | 'middleware' => ['web'], // you probably want to include 'web' here 169 | ``` 170 | 171 | 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. 172 | 173 | ## Views 174 | 175 | The 'index' and 'show' methods on the `PostController` need to render a view. 176 | 177 | ### Creating the Blade View Files 178 | 179 | 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. 180 | 181 | 1. `resources/views/posts/index.blade.php`: 182 | 183 | ``` 184 |

Showing all Posts

185 | 186 | @forelse ($posts as $post) 187 |
  • {{ $post->title }}
  • 188 | @empty 189 |

    'No posts yet'

    190 | @endforelse 191 | ``` 192 | 193 | 2. `resources/views/posts/show.blade.php`: 194 | 195 | ``` 196 |

    {{ $post->title }}

    197 | 198 |

    {{ $post->body }}

    199 | ``` 200 | 201 | Note: these templates would extend a base/master layout file in a real-world scenario. 202 | 203 | ### Registering Views in the Service Provider 204 | 205 | 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). 206 | 207 | ```php 208 | // 'BlogPackageServiceProvider.php' 209 | public function boot() 210 | { 211 | // ... other things 212 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'blogpackage'); 213 | } 214 | ``` 215 | 216 | ### Returning a View from the Controller 217 | 218 | We can now return the views we've created from the `PostController` (don't forget to import our `Post` model). 219 | 220 | Note the `blogpackage::` prefix, which matches the prefix we registered in our Service Provider. 221 | 222 | ```php 223 | // 'src/Http/Controllers/PostController.php' 224 | use JohnDoe\BlogPackage\Models\Post; 225 | 226 | public function index() 227 | { 228 | $posts = Post::all(); 229 | 230 | return view('blogpackage::posts.index', compact('posts')); 231 | } 232 | 233 | public function show() 234 | { 235 | $post = Post::findOrFail(request('post')); 236 | 237 | return view('blogpackage::posts.show', compact('post')); 238 | } 239 | ``` 240 | 241 | ### Customizable Views 242 | 243 | 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: 244 | 245 | ```php 246 | // 'BlogPackageServiceProvider.php' 247 | if ($this->app->runningInConsole()) { 248 | // Publish views 249 | $this->publishes([ 250 | __DIR__.'/../resources/views' => resource_path('views/vendor/blogpackage'), 251 | ], 'views'); 252 | 253 | } 254 | ``` 255 | 256 | The views can then be exported by users of our package using: 257 | 258 | ``` 259 | php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="views" 260 | ``` 261 | 262 | ## View Components 263 | 264 | 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: `` and closing `` (or the self-closing form). To learn more about Blade components, make sure to check out the Laravel documentation. 265 | 266 | 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. 267 | 268 | This section will cover how to provide these type of Blade components in your package. 269 | 270 | ### Class Based Components 271 | 272 | 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`. 273 | 274 | ```php 275 | // 'src/View/Components/Alert.php' 276 | message = $message; 289 | } 290 | 291 | public function render() 292 | { 293 | return view('blogpackage::components.alert'); 294 | } 295 | } 296 | ``` 297 | 298 | Next, create a new `views/components` directory in the `resources` folder. Add a new Blade component `alert.blade.php`: 299 | 300 | ```html 301 |
    302 |

    This is an Alert

    303 | 304 |

    {{ $message }}

    305 |
    306 | ``` 307 | 308 | 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 ``. 309 | 310 | ```php 311 | // 'BlogPackageServiceProvider.php' 312 | loadViewComponentsAs('blogpackage', [ 320 | Alert::class, 321 | ]); 322 | } 323 | ``` 324 | 325 | ### Anonymous View Components 326 | 327 | 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: 328 | 329 | ```php 330 | // 'BlogPackageServiceProvider.php' 331 | public function boot() 332 | { 333 | // ... other things 334 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'blogpackage'); 335 | } 336 | ``` 337 | 338 | Components (in the `resources/views/components` folder) can now be referenced prefixed by the defined namespace above ("blogpackage"): 339 | 340 | ``` 341 | 342 | ``` 343 | 344 | ### Customizable View Components 345 | 346 | 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: 347 | 348 | ```php 349 | // 'BlogPackageServiceProvider.php' 350 | if ($this->app->runningInConsole()) { 351 | // Publish view components 352 | $this->publishes([ 353 | __DIR__.'/../src/View/Components/' => app_path('View/Components'), 354 | __DIR__.'/../resources/views/components/' => resource_path('views/components'), 355 | ], 'view-components'); 356 | } 357 | ``` 358 | 359 | Now, it is possible to publish both files (class and Blade component) using: 360 | 361 | ``` 362 | php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="view-components" 363 | ``` 364 | 365 | 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. 366 | 367 | ## Assets 368 | 369 | You'll likely want to include a CSS and javascript file when you're adding views to your package. 370 | 371 | ### Creating an 'assets' Directory 372 | 373 | 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`. 374 | 375 | ### Customizable Assets 376 | 377 | 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: 378 | 379 | ```php 380 | // 'BlogPackageServiceProvider.php' 381 | if ($this->app->runningInConsole()) { 382 | // Publish assets 383 | $this->publishes([ 384 | __DIR__.'/../resources/assets' => public_path('blogpackage'), 385 | ], 'assets'); 386 | 387 | } 388 | ``` 389 | 390 | The assets can then be exported by users of our package using: 391 | 392 | ``` 393 | php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="assets" 394 | ``` 395 | 396 | ### Referencing Assets 397 | 398 | We can reference the stylesheet and javascript file in our views as follows: 399 | 400 | ```html 401 | 402 | 403 | ``` 404 | 405 | ## Testing Routes 406 | 407 | Let’s verify that we can indeed create a post, show a post and show all posts with our provided routes, views, and controllers. 408 | 409 | ### Feature Test 410 | 411 | 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: 412 | 413 | ```php 414 | // 'tests/Feature/CreatePostTest.php' 415 | assertCount(0, Post::all()); 433 | 434 | $author = User::factory()->create(); 435 | 436 | $response = $this->actingAs($author)->post(route('posts.store'), [ 437 | 'title' => 'My first fake title', 438 | 'body' => 'My first fake body', 439 | ]); 440 | 441 | $this->assertCount(1, Post::all()); 442 | 443 | tap(Post::first(), function ($post) use ($response, $author) { 444 | $this->assertEquals('My first fake title', $post->title); 445 | $this->assertEquals('My first fake body', $post->body); 446 | $this->assertTrue($post->author->is($author)); 447 | $response->assertRedirect(route('posts.show', $post)); 448 | }); 449 | } 450 | } 451 | ``` 452 | 453 | Additionally, we could verify that we require both a "title" and a "body" attribute when creating a new post: 454 | 455 | ```php 456 | // 'tests/Feature/CreatePostTest.php' 457 | /** @test */ 458 | function a_post_requires_a_title_and_a_body() 459 | { 460 | $author = User::factory()->create(); 461 | 462 | $this->actingAs($author)->post(route('posts.store'), [ 463 | 'title' => '', 464 | 'body' => 'Some valid body', 465 | ])->assertSessionHasErrors('title'); 466 | 467 | $this->actingAs($author)->post(route('posts.store'), [ 468 | 'title' => 'Some valid title', 469 | 'body' => '', 470 | ])->assertSessionHasErrors('body'); 471 | } 472 | ``` 473 | 474 | Next, let's verify that unauthenticated users (or "guests") can not create new posts: 475 | 476 | ```php 477 | // 'tests/Feature/CreatePostTest.php' 478 | /** @test */ 479 | function guests_can_not_create_posts() 480 | { 481 | // We're starting from an unauthenticated state 482 | $this->assertFalse(auth()->check()); 483 | 484 | $this->post(route('posts.store'), [ 485 | 'title' => 'A valid title', 486 | 'body' => 'A valid body', 487 | ])->assertForbidden(); 488 | } 489 | ``` 490 | 491 | Finally, let's verify the index route shows all posts, and the show route shows a specific post: 492 | 493 | ```php 494 | // 'tests/Feature/CreatePostTest.php' 495 | /** @test */ 496 | function all_posts_are_shown_via_the_index_route() 497 | { 498 | // Given we have a couple of Posts 499 | Post::factory()->create([ 500 | 'title' => 'Post number 1' 501 | ]); 502 | Post::factory()->create([ 503 | 'title' => 'Post number 2' 504 | ]); 505 | Post::factory()->create([ 506 | 'title' => 'Post number 3' 507 | ]); 508 | 509 | // We expect them to all show up 510 | // with their title on the index route 511 | $this->get(route('posts.index')) 512 | ->assertSee('Post number 1') 513 | ->assertSee('Post number 2') 514 | ->assertSee('Post number 3') 515 | ->assertDontSee('Post number 4'); 516 | } 517 | 518 | /** @test */ 519 | function a_single_post_is_shown_via_the_show_route() 520 | { 521 | $post = Post::factory()->create([ 522 | 'title' => 'The single post title', 523 | 'body' => 'The single post body', 524 | ]); 525 | 526 | $this->get(route('posts.show', $post)) 527 | ->assertSee('The single post title') 528 | ->assertSee('The single post body'); 529 | } 530 | ``` 531 | 532 | > 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. 533 | -------------------------------------------------------------------------------- /docs/08-models-and-migrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Models and Migrations" 3 | description: "Some packages need to offer a Laravel Model. This section explains how to allow for this and include your own database migrations. Additionally, the section will cover testing the models and migrations." 4 | tags: ["Models", "Migrations", "Testing Models", "Unit Test"] 5 | image: "https://www.laravelpackage.com/assets/pages/laravelpackage.jpeg" 6 | date: 2019-09-17 7 | --- 8 | 9 | # Models & Migrations 10 | 11 | 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. 12 | 13 | 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. 14 | 15 | ## Models 16 | 17 | 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: 18 | 19 | ```php 20 | // 'src/Models/Post.php' 21 | id(); 70 | $table->timestamps(); 71 | }); 72 | } 73 | 74 | /** 75 | * Reverse the migrations. 76 | * 77 | * @return void 78 | */ 79 | public function down() 80 | { 81 | Schema::dropIfExists('posts'); 82 | } 83 | } 84 | ``` 85 | 86 | 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). 87 | 88 | ### Publishing Migrations (method 1) 89 | 90 | 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: 91 | 92 | 1. an array of file paths ("source path" => "destination path") 93 | 94 | 2. the name (“tag”) we assign to this group of related publishable assets. 95 | 96 | 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`. 97 | 98 | Next, we can implement exporting the migration(s) as follows: 99 | 100 | ```php 101 | class BlogPackageServiceProvider extends ServiceProvider 102 | { 103 | public function boot() 104 | { 105 | if ($this->app->runningInConsole()) { 106 | // Export the migration 107 | if (! class_exists('CreatePostsTable')) { 108 | $this->publishes([ 109 | __DIR__ . '/../database/migrations/create_posts_table.php.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_posts_table.php'), 110 | // you can add any number of migrations here 111 | ], 'migrations'); 112 | } 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | 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. 119 | 120 | The migrations of this package are now publishable under the “migrations” tag via: 121 | 122 | ```bash 123 | php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="migrations" 124 | ``` 125 | 126 | ### Loading Migrations Automatically (method 2) 127 | 128 | 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](https://laravel.com/docs/packages#migrations)). 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. 129 | 130 | ```php 131 | class BlogPackageServiceProvider extends ServiceProvider 132 | { 133 | public function boot() 134 | { 135 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 136 | } 137 | } 138 | ``` 139 | 140 | 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. 141 | 142 | ## Testing Models and Migrations 143 | 144 | 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: 145 | 146 | ### Writing a Unit Test 147 | 148 | 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: 149 | 150 | ```php 151 | // 'tests/Unit/PostTest.php' 152 | create(['title' => 'Fake Title']); 168 | $this->assertEquals('Fake Title', $post->title); 169 | } 170 | } 171 | ``` 172 | 173 | Note: we're using the `RefreshDatabase` trait to be sure that we start with a clean database state before every test. 174 | 175 | ### Running the Tests 176 | 177 | 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”: 178 | 179 | ```json 180 | { 181 | ..., 182 | 183 | "autoload-dev": {}, 184 | 185 | "scripts": { 186 | "test": "vendor/bin/phpunit", 187 | "test-f": "vendor/bin/phpunit --filter" 188 | } 189 | } 190 | ``` 191 | 192 | We can now run `composer test` to run all of our tests and `composer test-f` followed by a test method/class's name to run that test solely. 193 | 194 | When we run `composer test-f a_post_has_a_title`, it leads us to the following error: 195 | 196 | ``` 197 | Error: Class 'Database\Factories\JohnDoe\BlogPackage\Models\PostFactory' not found 198 | ``` 199 | 200 | The abovementioned error tells us that we need to create a model factory for the `Post` model. 201 | 202 | ### Creating a Model Factory 203 | 204 | Let’s create a `PostFactory` in the `database/factories` folder: 205 | 206 | ```php 207 | // 'database/factories/PostFactory.php' 208 | up(); 280 | } 281 | ``` 282 | 283 | 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: 284 | 285 | ```php 286 | // 'database/migrations/create_posts_table.php.stub' 287 | Schema::create('posts', function (Blueprint $table) { 288 | $table->id(); 289 | $table->string('title'); 290 | $table->timestamps(); 291 | }); 292 | 293 | ``` 294 | 295 | After running the test, you should see it passing. 296 | 297 | ### Adding Tests for Other Columns 298 | 299 | Let’s add tests for the “body” and “author_id”: 300 | 301 | ```php 302 | // 'tests/Unit/PostTest.php' 303 | class PostTest extends TestCase 304 | { 305 | use RefreshDatabase; 306 | 307 | /** @test */ 308 | function a_post_has_a_title() 309 | { 310 | $post = Post::factory()->create(['title' => 'Fake Title']); 311 | $this->assertEquals('Fake Title', $post->title); 312 | } 313 | 314 | /** @test */ 315 | function a_post_has_a_body() 316 | { 317 | $post = Post::factory()->create(['body' => 'Fake Body']); 318 | $this->assertEquals('Fake Body', $post->body); 319 | } 320 | 321 | /** @test */ 322 | function a_post_has_an_author_id() 323 | { 324 | // Note that we are not assuming relations here, just that we have a column to store the 'id' of the author 325 | $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 326 | $this->assertEquals(999, $post->author_id); 327 | } 328 | } 329 | ``` 330 | 331 | You can continue driving this out with TDD on your own, running the tests, exposing the next thing to implement, and testing again. 332 | 333 | Eventually you’ll end up with a model factory and migration as follows: 334 | 335 | ```php 336 | // 'database/factories/PostFactory.php' 337 | $this->faker->words(3, true), 352 | 'body' => $this->faker->paragraph, 353 | 'author_id' => 999, 354 | ]; 355 | } 356 | } 357 | 358 | ``` 359 | 360 | For now, we hard-coded the ‘author_id’. In the next section, we'll see how we could whip up a relationship with a `User` model. 361 | 362 | ```php 363 | // 'database/migrations/create_posts_table.php.stub' 364 | 365 | Schema::create('posts', function (Blueprint $table) { 366 | $table->id(); 367 | $table->string('title'); 368 | $table->text('body'); 369 | $table->unsignedBigInteger('author_id'); 370 | $table->timestamps(); 371 | }); 372 | ``` 373 | 374 | ## Models related to App\User 375 | 376 | 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… 377 | 378 | 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. 379 | 380 | Below, there are two options to create a relation 381 | 382 | ### Approach 1: Fetching the User model from the Auth configuration 383 | 384 | 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. 385 | 386 | 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: 387 | 388 | ```php 389 | // Post model 390 | class Post extends Model 391 | { 392 | public function author() 393 | { 394 | return $this->belongsTo(config('auth.providers.users.model')); 395 | } 396 | } 397 | ``` 398 | 399 | 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. 400 | 401 | ### Approach 2: Using a Polymorphic Relationship 402 | 403 | 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). 404 | 405 | Let’s compare the standard and polymorphic relationships. 406 | 407 | Definition of a standard one-to-many relationship: 408 | 409 | ```php 410 | // Post model 411 | class Post extends Model 412 | { 413 | public function author() 414 | { 415 | return $this->belongsTo(User::class); 416 | } 417 | } 418 | 419 | // User model 420 | class User extends Model 421 | { 422 | public function posts() 423 | { 424 | return $this->hasMany(Post::class); 425 | } 426 | } 427 | ``` 428 | 429 | Definition of a polymorphic one-to-many relationship: 430 | 431 | ```php 432 | // Post model 433 | class Post extends Model 434 | { 435 | public function author() 436 | { 437 | return $this->morphTo(); 438 | } 439 | } 440 | 441 | // User (or other) model 442 | use JohnDoe\BlogPackage\Models\Post; 443 | 444 | class Admin extends Model 445 | { 446 | public function posts() 447 | { 448 | return $this->morphMany(Post::class, 'author'); 449 | } 450 | } 451 | ``` 452 | 453 | 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”). 454 | 455 | ```php 456 | Schema::create('posts', function (Blueprint $table) { 457 | $table->id(); 458 | $table->string('title'); 459 | $table->text('body'); 460 | $table->unsignedBigInteger('author_id'); 461 | $table->string('author_type'); 462 | $table->timestamps(); 463 | }); 464 | ``` 465 | 466 | 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. 467 | 468 | ### Providing a Trait 469 | 470 | Create a `Traits` folder in the `src/` directory and add the following `HasPosts` trait: 471 | 472 | ```php 473 | // 'src/Traits/HasPosts.php' 474 | morphMany(Post::class, 'author'); 485 | } 486 | } 487 | ``` 488 | 489 | 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: 490 | 491 | ```php 492 | // Given we have a User model, using the HasPosts trait 493 | $user = User::first(); 494 | 495 | // We can create a new post from the relationship 496 | $user->posts()->create([ 497 | 'title' => 'Some title', 498 | 'body' => 'Some body', 499 | ]); 500 | ``` 501 | 502 | ### Testing the Polymorphic Relationship 503 | 504 | Of course, we want to prove that any model using our `HasPost` trait can create new posts and that those posts are stored correctly. 505 | 506 | Therefore, we’ll create a new `User` model, not within the `src/Models/` directory, but rather in our `tests/` directory. 507 | 508 | To create users within our tests we'll need to overwrite the `UserFactory` provided by the Orchestra Testbench package, as shown below. 509 | 510 | ```php 511 | // 'tests/UserFactory.php' 512 | $this->faker->name, 531 | 'email' => $this->faker->unique()->safeEmail, 532 | 'email_verified_at' => now(), 533 | 'password' => bcrypt('password'), 534 | 'remember_token' => \Illuminate\Support\Str::random(10), 535 | ]; 536 | } 537 | } 538 | ``` 539 | 540 | 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`: 541 | 542 | ```php 543 | // 'tests/User.php' 544 | id(); 592 | $table->string('name'); 593 | $table->string('email')->unique(); 594 | $table->timestamp('email_verified_at')->nullable(); 595 | $table->string('password'); 596 | $table->rememberToken(); 597 | $table->timestamps(); 598 | }); 599 | } 600 | 601 | /** 602 | * Reverse the migrations. 603 | * 604 | * @return void 605 | */ 606 | public function down() 607 | { 608 | Schema::dropIfExists('users'); 609 | } 610 | } 611 | ``` 612 | 613 | Also load the migration at the beginning of our tests, by including the migration and performing its `up()` method in our `TestCase`: 614 | 615 | ```php 616 | // 'tests/TestCase.php' 617 | public function getEnvironmentSetUp($app) 618 | { 619 | include_once __DIR__ . '/../database/migrations/create_posts_table.php.stub'; 620 | include_once __DIR__ . '/../database/migrations/create_users_table.php.stub'; 621 | 622 | // run the up() method (perform the migration) 623 | (new \CreatePostsTable)->up(); 624 | (new \CreateUsersTable)->up(); 625 | } 626 | ``` 627 | 628 | ### Updating Our Post Model Factory 629 | 630 | 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”: 631 | 632 | ```php 633 | // 'database/factories/PostFactory.php' 634 | create(); 659 | 660 | return [ 661 | 'title' => $this->faker->words(3, true), 662 | 'body' => $this->faker->paragraph, 663 | 'author_id' => $author->id, 664 | 'author_type' => get_class($author) 665 | ]; 666 | } 667 | } 668 | ``` 669 | 670 | Next, we update the `Post` unit test to verify an ‘author_type’ can be specified. 671 | 672 | ```php 673 | // 'tests/Unit/PostTest.php' 674 | class PostTest extends TestCase 675 | { 676 | // other tests... 677 | 678 | /** @test */ 679 | function a_post_has_an_author_type() 680 | { 681 | $post = Post::factory()->create(['author_type' => 'Fake\User']); 682 | $this->assertEquals('Fake\User', $post->author_type); 683 | } 684 | } 685 | ``` 686 | 687 | Finally, we need to verify that our test `User` can create a `Post` and it is stored correctly. 688 | 689 | 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. 690 | 691 | A Unit test that verifies the desired behavior between a `User` and a `Post` could look as follows: 692 | 693 | ```php 694 | // 'tests/Unit/PostTest.php' 695 | class PostTest extends TestCase 696 | { 697 | // other tests... 698 | 699 | /** @test */ 700 | function a_post_belongs_to_an_author() 701 | { 702 | // Given we have an author 703 | $author = User::factory()->create(); 704 | // And this author has a Post 705 | $author->posts()->create([ 706 | 'title' => 'My first fake post', 707 | 'body' => 'The body of this fake post', 708 | ]); 709 | 710 | $this->assertCount(1, Post::all()); 711 | $this->assertCount(1, $author->posts); 712 | 713 | // Using tap() to alias $author->posts()->first() to $post 714 | // To provide cleaner and grouped assertions 715 | tap($author->posts()->first(), function ($post) use ($author) { 716 | $this->assertEquals('My first fake post', $post->title); 717 | $this->assertEquals('The body of this fake post', $post->body); 718 | $this->assertTrue($post->author->is($author)); 719 | }); 720 | } 721 | } 722 | ``` 723 | 724 | At this stage, all of the tests should be passing. 725 | --------------------------------------------------------------------------------