Laravel - atabegruslan/Notes GitHub Wiki

Install Laravel

composer global require laravel/installer
laravel new app

or

composer create-project --prefer-dist laravel/laravel app
# composer install
php artisan key:generate

Versions

To check Laravel version:

Major changes from Laravel 5 to 10:

Good tutorials on new (2021) Laravel stuff

Notable articles

Service Provider

https://www.youtube.com/watch?v=VYPfncvYW-Y

Advantages

Better dependency management

More articles

Lifecycle

Notification

Event

Inbuilt events which you can listen to: https://laravel.com/api/11.x/Illuminate/Auth/Events/Login.html

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class LoginListener
{
    public function handle(Login $event) { ... }

Usage: eg: broadcast events thru a websocket server: https://github.com/Ruslan-Aliyev/laravel-websockets

Observers

Under the hood observers also listen to events

php artisan make:observer UserObserver --model=UserName

Remember to register it first in App\Providers\AppServiceProvider

public function boot()
{
    User::observe(UserObserver::class);
}

Job

Usage: eg: dispatching a job into a queue: https://github.com/Ruslan-Aliyev/laravel-queue

Scheduling tasks

  1. Add a command: php artisan make:command minutely
  2. Do app/Console/Commands/minutely.php
  3. Do app/Console/Kernel.php
  4. When testing in console: php artisan minutely:demonotice. When put on server: php artisan schedule:run. schedule:run actually calls minutely:demonotice (and whatever other tasks are there). If you run them from console, they will run immediately. But on the server, the latter will run to schedule.
  5. Do ONE minutely cronjob on the server for Laravel, and let Laravel's Scheduler handle the rest of its jobs. On linux, do the cron like this: * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1. On Windows, use Task Scheduler like this: https://quantizd.com/how-to-use-laravel-task-scheduler-on-windows-10/

Cron

app/Console/Kernel.php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\TransactionChecker;

class Kernel extends ConsoleKernel
{
    protected $commands = [
        TransactionChecker::class,
    ];

    protected function schedule(Schedule $schedule)
    {
        $schedule->command('transaction:check')->dailyAt('01:00');
    }

    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

app/Console/Commands/TransactionChecker.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\TransactionCompletenessService;

class TransactionChecker extends Command
{
    protected $signature = 'transaction:check';

    protected $description = 'Check the completeness of transactions';

    private $transactionCompletenessService;

    public function __construct(TransactionCompletenessService $transactionCompletenessService)
    {
        $this->transactionCompletenessService = $transactionCompletenessService;

        parent::__construct();
    }

    public function handle()
    {
        $result   = '';
        $finished = false;

        while (!$finished)
        {
            $response = $this->transactionCompletenessService->check();

            $result  .= $response['message'];
            $finished = $response['finished'];
        }

        $this->info("RESULTS: \n" . $result);

        return 0;
    }
}

Now you can invoke from terminal: php artisan transaction:check

In server:

  1. SSH in
  2. crontab -e
  3. * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

In Clevercloud server:

clevercloud/cron.json

[
  "0 1 * * * $ROOT/clevercloud/cron.sh"
]

clevercloud/cron.sh

#!/bin/bash -l
set -euo pipefail

pushd "$APP_HOME"
/path/to/php /path/to/artisan transaction:check >> /dev/null 2>&1

Get Server's info from within Laravel

In Controller

public function showInfo(Request $request)
{
    dd($request);
}

The server sub-section shows the equivalent info of $_SERVER

In Template, you can render

@php
    phpinfo();
@endphp

Find where the PHP executable is located

public function findPhpExec()
{
    $phpFinder = new \Symfony\Component\Process\PhpExecutableFinder;

    if (!$phpPath = $phpFinder->find()) 
    {
        throw new \Exception('The php executable could not be found, add it to your PATH environment variable and try again');
    }

    return $phpPath;
}

See also for plain PHP: https://github.com/atabegruslan/Notes/wiki/PHP#get-various-info

Facades

Helper

Method 1

  1. Make app/XxxHelper.php
use ...\...;

if (! function_exists('helperFunction')) {
    function helperFunction() 
    {

    }
}
  1. composer.json
"autoload": {
    "files": [
        "app/XxxHelper.php"
    ],
  1. composer dump-autoload

Method 2

  1. Make app/Helpers/XxxHelper.php
namespace App\Helpers;

use ...\...;

class XxxHelper
{
    public static function helperFunction()
    {

    }
}
  1. config/app.php
'aliases' => [
    'XxxHelper' => App\Helpers\XxxHelper::class,
]
  1. composer dump-autoload

Making Service

In services, use:

  • Static methods: If no need for objects. Eg yearly report service for year 2020, 2021, ...
  • Non-static methods: If you want to chain methods
  • DI service: slightly less code

Different ways of writing things

In Blade

@if (!in_array($modLabel, ['xxx', 'yyy']))

@endif

is same as

@php {{ $skips = ['xxx','yyy','deleted_at']; }} @endphp
@if (!in_array($initLabel, $skips))

@endif

In PHP

$thisAndPrevious = ActionLog::where([
        [ 'time',            '<=', $log['time']            ],
        [ 'record_key_name', '=',  $log['record_key_name'] ],
        [ 'record_id',       '=',  $log['record_id']       ],
        [ 'model',           '=',  $log['model']           ],
    ])
    ->where(function ($query) {
        $query->where('method', '=', 'create')
              ->orWhere('method', '=', 'update');
    })
    ->orderBy('id', 'DESC')
    ->take(2)
    ->get();

is same as

$thisAndPrevious = CrudLog::where('time', '<=', $log['time'])
    ->where('record_key_name', '=',  $log['record_key_name'])
    ->where('record_id', '=',  $log['record_id'])
    ->where('model', '=',  $log['model'])
    ->whereIn('method', ['create', 'update'])
    ->orderBy('id', 'DESC')
    ->take(2)
    ->get();

Patterns

Shorter code

Redirect

Route

View

View Overriding

Filter in view:

In Blade

  1. Make provider: php artisan make:provider BladeFiltersServiceProvider , https://github.com/atabegruslan/Laravel_CRUD_API/blob/master/app/Providers/BladeFiltersServiceProvider.php
  2. Make service: https://github.com/atabegruslan/Laravel_CRUD_API/blob/master/app/Services/BladeFiltersCompiler.php
  3. Make custom provider: php artisan make:provider TranslateServiceProvider , https://github.com/atabegruslan/Laravel_CRUD_API/blob/master/app/Providers/TranslateServiceProvider.php
  4. Register in config/app.php : https://github.com/atabegruslan/Laravel_CRUD_API/blob/master/config/app.php#L187-188
  5. Use it in Blade: {{ $blablah | translate:'vn' }}

In Vue

  1. In resources\js\app.js write your filter: https://github.com/atabegruslan/Laravel_CRUD_API/blob/master/resources/js/app.js
  2. In Vue, use it like: {{ blahblah | to_3dp }}
  3. run npm run dev

Pagination (with and without Vue)

Blade

In controller

    public function index()
    {
        //$data = WhateverModel::orderBy('updated_at', 'ASC')->get()->all();
        $data = WhateverModel::orderBy('updated_at', 'ASC')->paginate(10);

        return view('whatever.index', ['data' => $data]);

In view whatever/index.blade.php

<div class="row" >
    <div id="paginate">
        {{ $data->links() }}
    </div>
</div>

Vue

For the view

  1. Write your custom pagination component, like: https://github.com/atabegruslan/Laravel_CRUD_API/tree/master/resources/js/components/common/Pagination.vue

  2. In app.js

import VuePagination from './components/common/Pagination';
Vue.component('vue-pagination', VuePagination);
  1. Use it <vue-pagination :pagination="pagination" @paginate="getItems()" /> where pagination is
{
    "current_page" :1,
    "from"         :1,
    "last_page"    :1,
    "per_page"     :20,
    "to"           :2,
    "total"        :2
}

In API controller

Pass the pagination object into the view.

Or you can use other's libraries

Blade Component

Request & Response

Model casts

File generating/manipulating libraries

Documentation

Here are a few libraries:

Swagger/OpenAPI

Nowadays, Swagger and OpenAPI can be loosely spoken about interchangably. But to be more exact, OpenAPI refers more to the standard, while Swagger refers more to the software tool for making documentation.

Document APIs using Swagger:

All the documentation libraries involves annotating the controller functions

An actual working example of Swagger in Laravel: https://github.com/Ruslan-Aliyev/laravel-swagger

Date and time

Validation

Client Side Vallidation

Logging

Laravel uses Monolog under the hood.
More about PHP logs and Monolog, see: https://github.com/Ruslan-Aliyev/log

  1. Default log's path: storage/logs/laravel.log

  2. In .env, you can see LOG_CHANNEL=stack

  3. Then you can see config/logging.php

  4. With the default "stack" channel, you can specify multiple channels, eg:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'custom'],
    ],

    'single' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel.log'), // You can see that the default log path is indeed at 'storage/logs/laravel.log'
        ...
    ],

    'custom' => [ // You can specify your custom logs
        'driver' => 'single',
        'path' => storage_path('logs/custom.log'),
        ...
    ],
],
  1. Then just test if it works by making a route
Route::get('/log', function () {
    Log::channel('custom')->info('msg', ['data' => 'value']);
});

Multi Tenancy

Integrations

Email

Recaptcha

SMS SignIn

Zalo Integration

https://www.youtube.com/watch?v=rXI03h4jbBg

Signup verification email

{domain}/verify-email/13/{sha1($email)}?expires=1723625264&signature=8f9e4716b90267802346b45c8a4bc3a52c42b5a977e3493e8675767bc0d55bd4

Web Scraping

https://gist.github.com/atabegruslan/b603476fbfa5a928ce92c0f9b49f29d7

Package Development

Pipeline

In controller

namespace App\Http\Controllers;

use Illuminate\Pipeline\Pipeline;
use App\Models\Post;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $select   = ['title', 'contents'];
        $filters  = ['title' => 'Wanted Title', 'another_filter_name' => 'another_filter_value'];
        $paginate = true;
        $perPage  = 10;

        /** @var \Illuminate\Database\Query\Builder $query */
        $query = app(Pipeline::class)
            ->send( Post::with('comment') /* Of type Illuminate\Database\Eloquent\Builder */ )
            ->through( Post::PIPES )
            ->thenReturn();

        if (count($select)) 
        {
            $query->select(DB::raw(join(',', $select)));
        }
        if (!empty($filters))
        {
            $query->where($filters);
        }

        $data = null;

        if ($paginate) 
        {
            $data = $query->paginate($perPage);
        }
        else
        {
            $data = $query->get();
        }

        $entries           = $data->items();
        $numberOfPages     = ceil($data->total() / $data->perPage());
        $currentPageNumber = $data->currentPage();
    }
}

In model

namespace App\Models;

use App\Models\Comment;
use App\Filters\Title;
use App\Filters\Contents;

class Post extends Model
{
    const PIPES = [
        Title::class,
        Contents::class,
    ];

    public function comment()
    {
        return $this->hasMany(Comment::class);
    }

Filters:

namespace App\Filters;

use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

abstract class Filter
{
    protected $request;

    function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function handle(Builder $builder, Closure $next)
    {
        if (
            !$this->request->has($this->filterName()) 
            || 
            $this->request->input($this->filterName(), '') === ''
        ) 
        {
            return $next($builder);
        }

        return $this->applyFilters($next($builder));
    }

    protected abstract function applyFilters(Builder $builder): Builder;

    protected function filterName()
    {
        return Str::snake(class_basename($this));
    }
}
namespace App\Filters;

use Illuminate\Database\Eloquent\Builder;

class Title extends Filter
{
    protected function applyFilters(Builder $builder): Builder
    {
        return $builder->where('title', 'ILIKE', '%' . $this->request->input($this->filterName()) . '%');
        // ILIKE only works in PostgreSql
    }
}

https://dev.to/abrardev99/pipeline-pattern-in-laravel-278p

API returning file downloads

Download PDF on server:

return response()->download('/absolute/path/to/file.pdf', 'filename.pdf', ['Content-Type: application/pdf']);

Download PDF directly from base64:

return response()->make( base64_decode('base64_binary_string_blah_blah') , 200, [
    'Content-Type' => 'application/pdf',
    'Content-Disposition' => 'inline; filename="filename.pdf"'
]);

https://www.codegrepper.com/code-examples/php/response%28%29-%3Emake+laravel+pdf

Download CSV directly from stream: https://www.laravelcode.com/post/how-to-export-csv-file-in-laravel-example

Nested controllers

Nested Models

Accessors / Mutators

Eg: Get timestamps in a formatted way, get name with first letter capitalized, get a currency number in the currency format ...

https://laravel.com/docs/9.x/eloquent-mutators

https://github.com/academico-sis/academico/blob/pro/app/Traits/PriceTrait.php#L7

Fonts

Google fonts

Eg: https://fonts.google.com/specimen/Inter?query=inter

https://stackoverflow.com/questions/56783985/how-to-use-custom-fonts-in-laravels-blade

Laravel

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">

.inter-normal {
    font-family: "Inter", sans-serif;
    font-optical-sizing: auto;
    font-weight: normal;
    font-style: normal;
}

.whatever {
    font-family: inter;
}

International

https://github.com/atabegruslan/Notes/wiki/Encoding#load-multi-lingual-fonts-for-barryvdhlaravel-dompdf

Permissions terminology

In Laravel, there is the regular User - Role - Permission

Then there are "weirder" terms like:

  • Permission = gate
  • Group of permissions = policy

Security

Middleware

Auth

Passport

https://github.com/atabegruslan/Laravel_CRUD_API?tab=readme-ov-file#passport

Past usage

composer require laravel/passport
php artisan migrate
php artisan passport:install

After this, oauth_clients table will automatically have 2 clients

Setup up API guards in config/auth.php

Protect routes using auth:api middleware

Update in Laravel 11

composer require laravel/passport
php artisan migrate

Install passport this way: php artisan install:api --passport

After this, oauth_clients table still remain empty. So you need to run these:

php artisan passport:client --personal

 What should we name the personal access client? [Laravel Personal Access Client]:
 > 

   INFO  Personal access client created successfully.  

  Client ID .................................................................................................................... 1  
  Client secret ......................................................................... SeB3Z3cSjGDI0wpWqwlRjKAHaG0u1y8dOjbTgga5  

php artisan passport:client --password

 What should we name the password grant client? [Laravel Password Grant Client]:
 > 

 Which user provider should this client use to retrieve users? [users]:
  [0] users
 > 

   INFO  Password grant client created successfully.  

  Client ID .................................................................................................................... 2  
  Client secret ......................................................................... DKf3NLRHwkneYV7fovCSLjmybsNwZJNzuLbGiRkA

If you use Password grant type, remember this in App\Providers\AppServiceProvider.php

public function boot(): void
{
    Passport::enablePasswordGrant();
}

https://laravel.com/docs/11.x/passport#password-grant-tokens

JWT

Note: Passport's bearer token is also a JWT. (There is no rule saying that a JWT can't be used as a bearer token). To test this:

composer create-project laravel/laravel test-passport-jwt
cd test-passport-jwt
php artisan key:generate

composer require laravel/passport
php artisan migrate
php artisan passport:install

By now, php artisan passport:keys should already been ran under the hood. If not, then please run this. This will generate /storage/oauth-public.key & /storage/oauth-private.key

Use Tinker to add a user:

php artisan tinker
> User::create(["name"=> "laravel", "email"=>"[email protected]", "password"=>bcrypt("secret")]);

Run server: php artisan serve

Get token: curl --location 'http://127.0.0.1:8000/oauth/token' \ --form 'client_id="2"' \ --form 'client_secret="yXnMzsj6nnE0awGKpZ5l8elXpd0KFI19fd5KYEPe"' \ --form 'grant_type="password"' \ --form 'username="[email protected]"' \ --form 'password="secret"'

Use Password type client (2) info from oauth_clients table. (This is treated as a 1st-party client)

Go to https://jwt.io/ to check

JWT in Laravel using php-jwt

https://adevait.com/laravel/implementing-jwt-authentication-in-laravel

JWT in Laravel using tymon/jwt-auth

https://github.com/Ruslan-Aliyev/laravel-jwt

Laravel Sanctum

It generates an opaque token, like a Personal Access Token.
This token can be passed in the API call, in the Authorization header, just like how you would for a bearer token.
Furthermore, for SPAs, Sanctum can be used to provide session-based authentications.
Sanctum's session-based authentications involves a HTTP-only cookie, which is safe for frontend SPAs (browser storage or regular cookie is unsafe because a bit of JS can acquire the sensitive credentials)

Full tutorial: https://www.youtube.com/watch?v=TzAJfjCn7Ks

Sanctum for session

For first party SPA frontends

https://github.com/Ruslan-Aliyev/laravel-sanctum-session-seperate-vue

https://github.com/Ruslan-Aliyev/laravel-sanctum-session-inlaravel-vue

The seperate front end way is more appropriate

Sanctum for token

For third party apps

https://github.com/Ruslan-Aliyev/laravel-sanctum-token

Sanctum for token (in SPA)

PS: This way isn't proper

Passport vs Sanctum

  • Sanctum offers both session-based and token-based authentication and is good for single-page application (SPA) authentications.

  • Passport uses JWT authentication as standard but also implements full OAuth2 authorization

    • This means that the bearer token that Passport generates is actually a JWT. Since OAuth2 makes no specifications on its bearer token's format, a JWT can be use.
      • The private and public keys for the signing of the generated JWT can be found in /storage folder
  • Sanctum always requires that you're 'logging in' as a user. If there's ever a use-case for authentication without a user present you can look into laravel passport's client credential grant.

  • https://www.youtube.com/watch?v=edcTejycirk

  • https://www.youtube.com/watch?v=8myQdPL8I1s

In relation to Breeze

Session that uses DB (Suitable for multi server environment)

SESSION_DRIVER=database

php artisan session:table php artisan migrate

Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->foreignId('user_id')->nullable()->index();
    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->longText('payload');
    $table->integer('last_activity')->index();
});

SSO

Homestead

https://github.com/atabegruslan/Notes/wiki/Laravel-Homestead

Storage of public images

Storage to AWS S3

  1. composer require league/flysystem-aws-s3-v3

  2. .env

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_ENDPOINT=https://cellar-c2.services.clever-cloud.com
  1. config/filesystems.php
'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],
    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],
    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
        'endpoint' => env('AWS_ENDPOINT'),
    ],
],
  1. Usage
use Illuminate\Support\Facades\Storage;

$fileName = 'path/to/file/filename.txt';
$content = 'bla bla';

if (Storage::disk('s3')->exists($fileName))
{
  Storage::disk('s3')->append($fileName, $content);
  Storage::disk('s3')->setVisibility($fileName, 'public');
}
else
{
  Storage::disk('s3')->put($fileName, $heading . $content, 'public');
}

Critical Sections

Laravel Ecosystem

https://ecosystem.laravel.io

TALL

Alpine

Alternative to Vue

Livewire

Unifying BE and FE from the back

InertiaJS

Unifying BE and FE from the front

Filament

Admin Dashboard

composer require tcg/voyager
# Update .env database connection and APP_URL
php artisan voyager:install --with-dummy
{domain}/admin : [email protected]/password

Nova

Admin dashboard

LaravelDaily/Larastarters library*

Statamic

Admin dashboard

Gitamic

Kinetic

Shift

For update

Shift Blueprint

Scaffold

Orion

Building REST APIs

Pint

PHP code style fixer

Herd

Laravel and PHP development environment for macOS

Takeout

Docker related

Valet

Docker related

Laradock

Docker related

Sail

Docker related

Sail

Setup in existing project

composer require laravel/sail --dev
php artisan sail:install # After here, a docker-compose.yaml will be created

Basset

Load CSS & JS

Bun

JS package manager

Smart Ads

Make ad banners

TelemetryHub

Monitoring

Benchmark

Test performance

Ray

Test performance

Sentry

Monitor performance

Laravel 11's health-check endpoint*

Health-check

barryvdh/laravel-debugbar library*

Debugging and Monitoring

Folio

Simplify routing

Telescope

Logging

Tinkerwell tool*

Code runner for PHP. Better than Tinker

Forge

Server management

Vapor

Serverless deployment platform

Precognition

Deployment

Methods:

  1. Upload public folder into server's public_html folder. Upload the rest to another folder outside of the server's public_html folder. In public/index.php rectify all relevant paths. Import .sql to server's database. Refactor database-name, username & password in the .env file.
  2. Load the entire folder as it is. To rid the /public/ segment of the URL, put the following into the root folder's .htaccess: https://infyom.com/blog/how-to-remove-public-path-from-url-in-laravel-application
  3. To rid the /public/ by: https://www.devopsschool.com/blog/laravel-remove-public-from-url-using-htaccess/

Clevercloud

clevercloud/php.json

 {
   "deploy": {
     "webroot": "/public"
   }
 }

HTTPS

Clear cache

File Upload

EC

AI

Laravel's system wide try-catch

https://github.com/jimrubenstein/laravel-framework/blob/master/src/Illuminate/Foundation/Http/Kernel.php#L111-L130

Misc

Masking sensitive info:


Places for info and help

⚠️ **GitHub.com Fallback** ⚠️