Back to campField Journalby Zin
All entries

php · laravel · typescript

PHP 8.5 & Laravel 13: Zero to Hero, for JavaScript & TypeScript Developers

Modern PHP and Laravel through a JS/TS lens: from your first script to a tested JSON API with Eloquent, validation, policies, queues and Pest, every example explained line by line.

33 min read0 likes

PHP 8.5 နဲ့ Laravel 13: သုညကနေ Hero ဆီ၊ JavaScript နဲ့ TypeScript Developer တွေအတွက်

JS/TS မျက်လုံးနဲ့ ကြည့်တဲ့ ခေတ်မီ PHP နဲ့ Laravel — ပထမဆုံး script ကနေ Eloquent၊ validation၊ policy၊ queue နဲ့ Pest test တွေပါတဲ့ JSON API အထိ၊ ဥပမာတိုင်းကို တစ်ကြောင်းချင်း ရှင်းပြထားပါတယ်။

ဖတ်ချိန် 31 မိနစ်0 likes

If you write JavaScript or TypeScript for a living, you have probably heard two things about PHP: that it runs a huge part of the web, and that it is old and messy. The first is true. The second describes PHP 5 from 2010. Modern PHP is a typed, fast, object-oriented language with enums, readonly classes, property hooks and, since PHP 8.5, a pipe operator. Laravel 13, its most popular framework, gives you routing, an ORM, validation, queues, auth and testing in one coherent package.

This guide goes from zero to the point where you can read real Laravel code and build a small JSON API. Each section puts a PHP idea beside the JS/TS one you already know, and every example has line-by-line notes under it. Tap or hover a highlighted line to read what it does. The plain-PHP examples are live: edit one and press Run (or ⌘/Ctrl + Enter) to run it on PHP 8.5, right here on the page.

Why PHP, for a JS/TS developer

PHP was created by Rasmus Lerdorf in 1994 as a few tools for his home page, and grew into a general-purpose language. Today it runs WordPress, Wikipedia and around three quarters of all websites whose server language is known. Since PHP 7 (2015) it has roughly doubled in speed, and since PHP 8 it has gained a JIT compiler, union types, enums, fibers and much stricter, saner behaviour.

Here is the mental shift, side by side:

  • Shared-nothing requests. A Node server is one long-running process that keeps state in memory between requests. Classic PHP starts every request with a clean slate: load, run, respond, throw everything away. A crash or memory leak only ever hurts one request.
  • Types are checked at run time. TypeScript checks types when you build and then erases them. PHP keeps type declarations in the running program and checks them on every call. Static analysers (PHPStan, Psalm) add the build-time checking you are used to.
  • One array type does everything. A PHP array is an ordered map: it is your JS array, your object literal and your Map at once.
  • Blocking is normal. There is no event loop in your code and no async/await colouring. A database call simply waits; concurrency comes from running many PHP workers side by side.
  • Composer is npm. composer.json is your package.json, and Packagist is the registry.
  • Laravel is the batteries-included framework that Node never settled on: think Express, Prisma, Zod, BullMQ, NextAuth and Vitest, designed together and documented in one place.

Installing PHP and Laravel

On macOS and Windows the easiest route is Laravel Herd, a free app that installs PHP, Composer and the Laravel installer and serves your projects locally. On any OS, the one-line installers at php.new do the same in a terminal. Check what you have:

bash
php -v                 # PHP 8.5.x (cli)composer --version     # Composer 2.xphp -a                 # an interactive shell, like running node with no filephp hello.php          # run a script, like node hello.jsphp -S localhost:8000  # a tiny dev web server for the current folder

Line by line

  1. php -v prints the version. Anything below 8.3 is too old for Laravel 13; this guide uses 8.5.
  2. Composer is PHP's package manager, the npm of this world. It installs into a vendor/ folder (your node_modules).
  3. The interactive shell evaluates PHP as you type it. Inside a Laravel project you will use php artisan tinker instead, which also loads your app.
  4. Runs a file from top to bottom and exits. This is how to try every PHP-only example below: save it as a .php file and run it.
  5. PHP ships with a development server, so you can serve a folder of .php files with no Apache or Nginx. Never use it in production.

Hello, PHP

A PHP file starts in "HTML mode" and switches to code at <?php. A file that is only code starts with that tag and leaves out the closing ?>.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. The opening tag. Everything after it is PHP. A file that holds only code never closes the tag, so no stray whitespace after it can leak into the response.
  2. Turns on strict typing for this file: passing a string where an int is declared becomes an error instead of a silent conversion. Put it at the top of every file you write; it is the PHP version of "strict": true.
  3. Every variable starts with $. There is no let or const: the first assignment creates it. Statements end with a semicolon, and it is not optional.
  4. Double quotes interpolate variables, like a JS template literal; the braces make the boundary clear. Single quotes never interpolate.
  5. echo prints. . is the string concatenation operator (+ only adds numbers). PHP_EOL is the newline for the current OS.
  6. echo can take several values separated by commas. Built-in functions live in one global namespace, so it is strtoupper($s), not s.toUpperCase().
  7. printf formats like C: %s for a string, %d for an integer. strlen counts bytes; use mb_strlen for characters in non-ASCII text such as Burmese.

Types, strict mode and comparisons

PHP types look a lot like TypeScript annotations, but they are enforced while the program runs. Parameters, return values and class properties can all be typed.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Parameter types go before the name, and the return type after the colon. The body sits in braces on its own line, by the PSR-12 style convention.
  2. int|string is a union type, exactly like TS. ?string means string|null, and = null gives a default value.
  3. ?? is the null coalescing operator, the same as in JS. It also works on missing array keys and undefined variables without a warning.
  4. var_dump prints values with their types: bool(false) bool(true) bool(false). Since PHP 8, 0 == 'a' is false. Loose == still converts '1' == '01' to numbers, so use === and !== by habit, just as in JS.
  5. Because of strict_types, the string '2' is refused for an int parameter. Without it PHP would quietly convert it.
  6. A failed type check throws a TypeError. $e::class gives the class name of any object.
  7. ?-> is the nullsafe operator, PHP's optional chaining. Here $user is null, so the whole chain is null and ?? supplies guest.

The scalar types are int, float, string and bool. On top of them PHP has array, object, callable, iterable, mixed (like unknown), void, never (a function that always throws or exits), null, true and false, plus class and interface names.

Arrays: one type for lists and objects

This is the biggest everyday difference. A PHP array is an ordered map of keys to values. With keys 0, 1, 2… it behaves like a JS array; with string keys it behaves like an object literal. And it is a value, not a reference.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Square brackets make an array, and with no keys PHP numbers them 0, 1, 2. This is a list.
  2. $array[] = value appends, like push.
  3. => pairs a key with a value. This array plays the role of { name: 'Aye', age: 29 }. Keys are read with brackets: $user['name'].
  4. Reading a missing key on its own would raise a warning and give null. With ?? it is safe, like user.email ?? 'no email'.
  5. Destructuring works on both kinds: by position (skip one with an empty slot) and by key. It is the same idea as const [first, , third] = langs.
  6. The array functions take the array as an argument: array_map(callback, array) but array_filter(array, callback). The argument order is famously inconsistent, so lean on your editor. fn (…) => is an arrow function.
  7. A gotcha: array_filter keeps the original keys, so $short is [1 => 'ts', 2 => 'go'] and turns into a JSON object {"1":"ts","2":"go"}. array_values renumbers it into a list: ["ts","go"].
  8. Assigning an array copies it (lazily, so it is cheap until you write). Changing $copy does not touch $langs. In JS both names would point at the same array.
  9. The spread operator works with string keys too, and later keys win. This is { ...user, age: 30, city: 'Yangon' }.
  10. array_find (PHP 8.4) returns the first value that matches, like Array.prototype.find. There are also array_find_key, array_any and array_all (some and every).
  11. array_first and array_last (PHP 8.5) read the first and last values without moving any internal pointer. They return null on an empty array.

Functions, closures and callables

Functions feel familiar, with two features JS lacks: named arguments and explicit control over what a closure captures.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Named arguments let you skip to the parameter you need. JS fakes this with an options object; in PHP any function can be called this way, so parameter names are part of a function's public API.
  2. ...$numbers collects the remaining arguments into an array: the rest parameter ...numbers: number[].
  3. The same ... spreads an array into arguments, like total(...[4, 5]) in JS. This prints 6 9.
  4. An anonymous function sees nothing from the outer scope unless you list it in use. &$count captures it by reference, so the closure can change it. Without the & it would get a copy.
  5. An arrow function fn captures outer variables automatically, by value, and its body is a single expression. This is the closest thing to a JS arrow function. Floats print without a trailing .0, so this shows 110.
  6. The first-class callable syntax name(...) turns any function or method into a Closure object you can pass around, like passing String.prototype.toUpperCase but bound and type-safe.

The pipe operator (new in PHP 8.5)

Because PHP's standard library is functions rather than methods, chaining used to read inside-out. PHP 8.5 adds |>, which passes the value on its left into the callable on its right. JS has had a pipe proposal for years; PHP shipped one.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. The old way: four nested calls that run in the opposite order to how you read them.
  2. The pipeline starts with a value…
  3. …and each |> calls the next callable with it. trim(...) is a first-class callable, so this is trim($title).
  4. A callable can take only one argument here. When you need more, wrap the call in an arrow function. The parentheses around the fn are required; without them PHP refuses to parse it.
  5. The last step's result becomes the value of the whole expression: php-8-5-the-pipe-operator.
  6. Both versions produce the same string. Pipes pair well with Laravel's Str helpers and with your own small, single-purpose functions.

Classes, enums and interfaces

PHP's object model is closer to TypeScript's than to JavaScript's prototypes: real classes, interfaces, abstract classes, visibility that is enforced at run time, and native enums.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. A backed enum: each case has a string value. Unlike a TS string union, it is a real type you can declare, and unlike a TS enum it exists at run time with methods.
  2. Enums can have methods and implement interfaces. Inside, $this is the current case.
  3. match is a strict (===) switch that returns a value and has no fall-through. If no arm matches it throws an UnhandledMatchError, so a missing case is a loud bug, not a silent undefined.
  4. An interface works like in TS, with one difference: a class must declare implements HasSlug. PHP checks types by name, not by shape.
  5. final forbids subclasses. A readonly class makes every property assign-once: an immutable value object, like a frozen TS class.
  6. Constructor promotion: a parameter with a visibility keyword is also declared and assigned as a property. These four lines replace a property list plus $this->title = $title; lines, just like TS constructor(public title: string). The trailing comma is allowed.
  7. Properties and methods are reached with ->, not . (the dot is taken by string concatenation). $this is always explicit.
  8. new creates the object. Named arguments let you skip status and set only tags. This prints hello-laravel · Not live yet.
  9. from turns a stored value back into a case and throws on an unknown value; tryFrom returns null instead. ->name is the case name, Published.
  10. Writing to a readonly property throws an Error: "Cannot modify readonly property Post::$title". To "change" an immutable object, make a modified copy, as shown two sections below.

Two more pieces you will meet constantly in Laravel. Traits are reusable bundles of methods that a class pulls in with use SomeTrait; (Laravel models use HasFactory, for example), a form of mix-in. Static members belong to the class, not an instance, and are reached with ::, as in Post::query() or Status::from().

Property hooks and asymmetric visibility (PHP 8.4)

In TypeScript you would write get and set accessors, or a private field with a public getter. PHP 8.4 puts both ideas directly on the property.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Asymmetric visibility: anyone can read $logins, but only the class can write it. No more private field plus a getter.
  2. A property hook. Code that writes $user->email = … runs this set block, but from the outside it still looks like a plain property.
  3. Inside its own hook, assigning to the property stores the value. Here every email is validated and lower-cased on the way in.
  4. A virtual property with only a get hook. It has no storage of its own and is computed on every read, like a TS getter.
  5. The constructor's assignment goes through the set hook too, so even the first value is validated.
  6. Prints aye@example.com example.com 1.
  7. Writing from outside fails: "Cannot modify private(set) property User::$logins from global scope".
  8. Since PHP 8.4 you can call a method or read a property straight off new without wrapping it in parentheses, as in JS. This prints shop.dev.

Immutable updates with clone-with (PHP 8.5)

In TS you write { ...price, amount: 1250 } to get a changed copy. Readonly PHP objects used to need a lot of boilerplate for the same thing. PHP 8.5 lets clone take the properties to change, and adds #[\NoDiscard] so nobody calls an immutable method and throws the result away.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Store money as an integer number of cents. Floats cannot hold 0.1 exactly, in PHP or in JS.
  2. An attribute (PHP's decorators, written #[…]). #[\NoDiscard] asks PHP to warn whenever the return value is ignored. The leading backslash means "the global namespace".
  3. clone($object, [...]) copies the object and sets the listed properties on the copy. It respects visibility: readonly properties can only be set this way from inside the class, which is exactly what a with… method is for.
  4. $price is untouched. This prints 1000 -> 1250 EUR.
  5. A (void) cast says "I meant to ignore this", and silences the warning.
  6. This line forgets to use the result, so PHP prints a warning that includes the attribute's message: the return value "should either be used or intentionally ignored by casting it as (void), Money is immutable; use the returned copy".

Errors and exceptions

Unlike Go, PHP uses exceptions, so this part will feel like home: throw, try, catch, finally. Two differences: you catch by class, and since PHP 8 almost every engine failure (a type error, a call on null, a division by zero) is an exception you can catch, rather than a fatal error.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. Custom errors are classes that extend Exception or one of its children, like class NotFound extends Error in JS. An empty body is enough.
  2. throw is an expression, so it can sit on the right of ??: "the user, or throw".
  3. This call throws, so the rest of the try block is skipped.
  4. A catch names the class it handles, and | catches several. Anything not listed keeps travelling up the stack.
  5. finally runs whether or not something was thrown, exactly as in JS.
  6. Many older functions signal failure by returning false or null. Pass JSON_THROW_ON_ERROR and json_decode throws instead, the way JSON.parse does. The message here is "Syntax error".

Namespaces, autoloading and Composer

PHP has no import that loads a file. Instead, classes live in namespaces, and Composer generates an autoloader that finds a class's file the first time you use it. The convention that maps names to paths is called PSR-4: App\Money\Money lives in src/Money/Money.php.

json
{    "name": "aye/shop",    "require": {        "php": "^8.5",        "nesbot/carbon": "^3.0"    },    "require-dev": {        "pestphp/pest": "^5.0"    },    "autoload": {        "psr-4": { "App\\": "src/" }    },    "scripts": {        "test": "pest"    }}

Line by line

  1. The package name is always vendor/name, like a scoped npm package @aye/shop.
  2. require is dependencies. You can even require a PHP version: Composer refuses to install on anything older. Version ranges use the same ^ caret rules as npm.
  3. require-dev is devDependencies. Pest is a testing framework with a Jest-style API.
  4. The PSR-4 rule: any class whose name starts with App\ is loaded from src/. (In JSON the backslash is escaped, so it is written twice.)
  5. scripts work like npm scripts: composer test runs Pest.
php
<?php // src/Money/Money.phpdeclare(strict_types=1); namespace App\Money; final readonly class Money{    public function __construct(public int $amount) {}} // public/index.phprequire __DIR__ . '/../vendor/autoload.php'; use App\Money\Money;use function App\Support\format_money; echo format_money(new Money(1250));

Line by line

  1. A namespace declaration names the folder this file's classes belong to. Backslashes separate the parts, like / in an import path.
  2. Your entry file includes Composer's autoloader once. After that, every class loads itself on first use; there is no import graph and no bundler.
  3. use only aliases a name, so you can write Money instead of \App\Money\Money. It loads nothing by itself.
  4. Functions are imported with use function. Plain functions are not autoloaded by PSR-4; list their files under autoload.files in composer.json.
  5. In a real project these two files are separate; they are shown together here to fit one block. Laravel wires all of this up for you.
bash
composer init                          # npm initcomposer require guzzlehttp/guzzle     # npm install axioscomposer require --dev pestphp/pest    # npm install -D vitestcomposer install                       # npm ci: install exactly what composer.lock sayscomposer update                        # npm updatecomposer dump-autoload                 # rebuild the class map after moving filescomposer audit                         # npm audit

Line by line

  1. Answers a few questions and writes composer.json.
  2. Adds a package to require and installs it into vendor/. Guzzle is the HTTP client most PHP code uses (Laravel wraps it as Http::get()).
  3. composer.lock is your package-lock.json. Commit it, and run composer install in CI and on servers.
  4. Rarely needed with PSR-4, but useful after renaming namespaces or adding entries to autoload.files.

How PHP runs your code

Knowing what happens under a request explains most of PHP's design, and why Laravel feels the way it does.

1. The engine: source to opcodes

The Zend Engine reads a .php file, parses it into an abstract syntax tree and compiles that into opcodes, a compact bytecode, which a virtual machine then executes. It is the same overall shape as V8 turning JS into bytecode for Ignition.

2. OPcache and the JIT

Compiling every file on every request would be wasteful, so OPcache keeps the compiled opcodes in shared memory, and the next request skips straight to running them. PHP 8 added a JIT compiler on top, which turns hot code into machine code. It helps CPU-heavy work such as image processing most; typical web requests spend their time waiting on the database instead.

3. PHP-FPM: one worker per request

In production a web server such as Nginx hands each request to PHP-FPM, a pool of worker processes. Each worker handles one request at a time from start to finish, and then everything that request created is freed. Concurrency comes from the pool size, not from an event loop, and a slow request never blocks others as long as workers are free.

You can see the shared-nothing model in the runtime on this page: every Run is a new request. Press Run twice and the counter starts from zero again each time.

phpEditable: change it and press Run

Tab indents · ⌘/Ctrl + Enter runs · Esc leaves the editor

Line by line

  1. A plain function. In a Node server, a counter at module level would keep growing from one request to the next; watch what PHP does instead.
  2. A static variable keeps its value between calls within one request, like a variable captured by a closure. It is the nearest thing PHP has to module state.
  3. Prints 3: three calls in this request. Run it again and you still get 3, because the next request starts from a blank slate. Every request under PHP-FPM in production behaves the same way.
  4. $GLOBALS holds every global variable, and ??= assigns only if the key is missing, as in JS. A global "cache" like this lasts a single request, which is why real caches live in Redis, APCu or the database.
  5. Each request also gets its own memory, released when it ends. memory_get_peak_usage() reports the most this script used, a few hundred kilobytes, so a leak in one request can't pile up in the next.

4. Long-running workers: Octane and FrankenPHP

Booting a framework on every request costs a few milliseconds. Laravel Octane (with FrankenPHP, Swoole or RoadRunner) boots your app once and keeps it in memory between requests, which is much closer to a Node server, and much faster. The trade-off is Node's trade-off: static state now lives across requests, so you must not leak it.

5. Memory

PHP frees most values the moment the last reference to them disappears (reference counting), and a cycle collector cleans up objects that point at each other. Because classic workers are thrown away after each request, memory leaks are rarely a problem in PHP. In Octane, queue workers and other long-running scripts they are, just as in Node.

Laravel 13 in one picture

Laravel was created by Taylor Otwell in 2011 and ships a major version every year. Laravel 13 (March 2026) needs PHP 8.3 or newer, gets bug fixes until the third quarter of 2027 and security fixes until March 2028. It was a deliberately gentle upgrade with few breaking changes, adding more PHP attributes for configuration, JSON:API resources, a first-party AI SDK, semantic (vector) search and many smaller improvements.

The fastest way in is to map it onto tools you already use:

  • Routing and controllers → Express or Next.js route handlers.
  • Eloquent ORM and migrations → Prisma or Drizzle, but in the Active Record style: a model object saves itself.
  • Form Requests and validation rules → Zod schemas at the edge of your API.
  • API Resources → the DTO or select you use to shape JSON responses.
  • Queues and jobs → BullMQ, with workers you start using php artisan queue:work.
  • Sanctum, Fortify and policies → NextAuth or Lucia for sign-in, plus your own permission checks.
  • Blade, Livewire or Inertia → server-rendered templates, or a React, Vue or Svelte front end with no separate API.
  • Pest → Vitest or Jest. Artisan → your package.json scripts and code generators. Reverb → Socket.IO.

Create a project

bash
composer global require laravel/installerlaravel new blogcd blogphp artisan install:apicomposer run devphp artisan make:model Post -mfphp artisan route:list

Line by line

  1. Installs the laravel command globally, like npm i -g create-next-app. Herd already includes it.
  2. Creates the app and asks a few questions: a starter kit (none, React, Vue, Svelte or Livewire, with sign-in screens already built), the database (SQLite by default, so there is nothing to set up) and the test framework (choose Pest).
  3. Adds routes/api.php and Sanctum for API tokens. Every route in that file is served under /api.
  4. Starts the development server, a queue worker, a live log viewer and Vite together in one terminal: the npm run dev of Laravel. The app is on http://localhost:8000.
  5. Artisan is Laravel's CLI. make:* commands generate files: here a model plus its migration (-m) and factory (-f).
  6. Prints every route with its method, URL, name and handler. Handy when a URL does not match what you expected.

The folders you will touch most, and their JS equivalents:

  • routes/: web.php for pages, api.php for JSON, console.php for commands and scheduled tasks.
  • app/Models/: Eloquent models, one class per table.
  • app/Http/Controllers/, Requests/, Resources/: request handling, validation and response shaping.
  • database/migrations/, factories/, seeders/: schema changes, fake data for tests, starting data.
  • resources/views/ and resources/js/: Blade templates, or your React/Vue code when you use Inertia.
  • config/ and .env: settings. Code reads config('app.name'); only config files read env().
  • bootstrap/app.php: one place to register routes, middleware and exception handling.
  • tests/: Pest tests, Feature/ for HTTP-level tests and Unit/ for plain classes.

Every request takes the same path: public/index.php boots the app, the router matches the URL, the request passes through middleware (auth, CSRF, rate limits), the controller runs, and whatever it returns becomes the response. Return an array or a model and Laravel sends JSON. If that sounds like Express middleware plus a handler, it is.

For the rest of this guide we will build a small blog API: public endpoints that list published posts, and authenticated endpoints to write, edit and delete your own.

Migrations: the schema in code

php
<?php use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema; return new class extends Migration{    public function up(): void    {        Schema::create('posts', function (Blueprint $table) {            $table->id();            $table->foreignId('user_id')->constrained()->cascadeOnDelete();            $table->string('title');            $table->string('slug')->unique();            $table->text('body');            $table->string('status')->default('draft');            $table->json('tags')->nullable();            $table->timestamp('published_at')->nullable();            $table->timestamps();        });    }     public function down(): void    {        Schema::dropIfExists('posts');    }};

Line by line

  1. A migration file returns an anonymous class. Its name on disk carries a timestamp (2026_10_13_000000_create_posts_table.php), which sets the order migrations run in.
  2. up applies the change; php artisan migrate runs every migration that has not run yet, and records it in a migrations table.
  3. An auto-incrementing big-integer primary key called id.
  4. A user_id column with a foreign key to users.id, guessed from the column name. Deleting a user deletes their posts.
  5. A unique index, so the database itself refuses two posts with the same slug.
  6. The status is stored as plain text; the model will turn it into a PHP enum.
  7. Adds created_at and updated_at, which Eloquent fills in for you, like Prisma's @default(now()) and @updatedAt.
  8. down undoes up, so php artisan migrate:rollback can step back. migrate:fresh drops everything and starts over, which is handy in development.

Eloquent models

A model is a class mapped to a table. Laravel 13 lets you configure much of it with attributes on the class instead of protected properties.

php
<?php namespace App\Enums; enum PostStatus: string{    case Draft = 'draft';    case Published = 'published';}

Line by line

  1. The same backed enum idea as earlier, in its own file under app/Enums. The model will store draft or published and hand you the enum case.
php
<?php namespace App\Models; use App\Enums\PostStatus;use Illuminate\Database\Eloquent\Attributes\Fillable;use Illuminate\Database\Eloquent\Attributes\Scope;use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;use Illuminate\Support\Str; #[Fillable(['title', 'body', 'status', 'tags', 'published_at'])]class Post extends Model{    use HasFactory;     protected $attributes = ['status' => 'draft'];     protected function casts(): array    {        return [            'status' => PostStatus::class,            'tags' => 'array',            'published_at' => 'datetime',        ];    }     protected static function booted(): void    {        static::creating(function (Post $post): void {            $post->slug ??= Str::slug($post->title).'-'.Str::lower(Str::random(6));        });         static::saving(function (Post $post): void {            if ($post->status === PostStatus::Published) {                $post->published_at ??= now();            }        });    }     public function user(): BelongsTo    {        return $this->belongsTo(User::class);    }     #[Scope]    protected function published(Builder $query): void    {        $query->where('status', PostStatus::Published)->whereNotNull('published_at');    }}

Line by line

  1. Mass assignment protection. Only these columns can be filled from an array, as in Post::create($data). A request that sneaks in user_id or id cannot overwrite them. Laravel 13 lets you declare this with the #[Fillable] attribute (before, it was a $fillable property).
  2. The class name gives the table: Post reads and writes posts. There is no schema file to keep in sync; columns come from the database.
  3. A trait that gives the model Post::factory() for tests and seeders.
  4. Default attribute values for a new model, so a post is a draft even before the database fills in its own default.
  5. Casts convert columns as they are read and written: status becomes a PostStatus enum, the JSON tags column becomes a PHP array, and published_at becomes a Carbon date object (think a friendlier Date).
  6. Model events. Before a post is first inserted, if no slug was given (??= assigns only when null), one is made from the title, such as hello-world-k3x9qa.
  7. saving runs before every insert and update. The first time a post is saved as published, it is stamped with the time it went live.
  8. A relationship: $post->user loads the author. The foreign key user_id is inferred from the method name.
  9. A local scope, marked with #[Scope], adds a reusable filter to every query: Post::published()->get(). It works like a named Prisma where you can chain.
php
<?php namespace App\Models; use Database\Factories\UserFactory;use Illuminate\Database\Eloquent\Attributes\Fillable;use Illuminate\Database\Eloquent\Attributes\Hidden;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Relations\HasMany;use Illuminate\Foundation\Auth\User as Authenticatable;use Illuminate\Notifications\Notifiable;use Laravel\Sanctum\HasApiTokens; #[Fillable(['name', 'email', 'password'])]#[Hidden(['password', 'remember_token'])]class User extends Authenticatable{    /** @use HasFactory<UserFactory> */    use HasApiTokens, HasFactory, Notifiable;     protected function casts(): array    {        return [            'email_verified_at' => 'datetime',            'password' => 'hashed',        ];    }     public function posts(): HasMany    {        return $this->hasMany(Post::class);    }}

Line by line

  1. #[Hidden] keeps these columns out of every array and JSON version of the model, so a password hash can never leak into an API response by accident.
  2. A class can use several traits. HasApiTokens comes from Sanctum and lets a user create API tokens.
  3. The hashed cast hashes a plain password the moment it is assigned, with bcrypt by default.
  4. The other side of belongsTo: $user->posts is a collection of that user's posts, and $user->posts() is a query you can keep building on.

Querying with Eloquent

Try these in php artisan tinker, Laravel's REPL with your whole app loaded. Each line notes the SQL it runs.

php
$user = User::factory()->create(['name' => 'Aye']); $post = $user->posts()->create([    'title' => 'Hello Laravel',    'body' => 'My first post.',    'tags' => ['php', 'laravel'],]); $post->update(['status' => PostStatus::Published]); $latest = Post::published()    ->with('user')    ->whereJsonContains('tags', 'php')    ->latest('published_at')    ->paginate(10); $titles = $user->posts()->pluck('title');$counts = User::withCount('posts')->orderByDesc('posts_count')->first(); $found = Post::where('slug', $post->slug)->firstOrFail();$found->delete();

Line by line

  1. A factory creates a row with fake data, overriding just the fields you pass. INSERT INTO users ….
  2. Creating through the relationship fills in user_id for you. Only the #[Fillable] columns are accepted, the slug comes from the creating event, and tags is stored as JSON.
  3. update changes the attributes and saves in one step. The model's saving event stamps published_at. UPDATE posts SET status = …, published_at = ….
  4. The scope from the model: WHERE status = 'published' AND published_at IS NOT NULL.
  5. Eager loading. Without with, reading $post->user inside a loop runs one query per post (the "N+1" problem). With it, Laravel loads all the authors in one extra query.
  6. Queries inside a JSON column, translated to the right SQL for MySQL, PostgreSQL or SQLite.
  7. paginate runs the query with LIMIT/OFFSET, counts the total, and reads ?page= from the request. Returned from a controller, it becomes JSON with data, links and meta.
  8. pluck returns one column as a Collection, Laravel's array wrapper with chainable map, filter, groupBy and about a hundred more, much like lodash.
  9. withCount adds a posts_count column using a subquery, without loading the posts.
  10. firstOrFail throws ModelNotFoundException when nothing matches, and Laravel turns that into a 404 response.

Validation with Form Requests

Validation lives in its own class, so controllers stay short. If the input fails, Laravel stops the request before your controller runs and answers with a 422 and a JSON list of errors, much like a Zod safeParse that throws.

php
<?php namespace App\Http\Requests; use App\Enums\PostStatus;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rule; class StorePostRequest extends FormRequest{    public function rules(): array    {        return [            'title' => ['required', 'string', 'max:120'],            'body' => ['required', 'string'],            'status' => ['sometimes', Rule::enum(PostStatus::class)],            'tags' => ['sometimes', 'array', 'max:5'],            'tags.*' => ['string', 'distinct', 'max:20'],        ];    }}

Line by line

  1. Generated with php artisan make:request StorePostRequest. With no authorize() method, every caller is allowed; permissions are handled by the policy below.
  2. Rules read left to right: present and not empty, a string, at most 120 characters. Each failure has a ready-made, translatable message.
  3. sometimes validates the field only when it is sent. Rule::enum accepts only the enum's values, draft or published.
  4. tags.* applies to every item in the array: strings, no duplicates, short. In the controller, $request->validated() returns only the fields named here, never anything else the client sent.

API Resources: shaping the JSON

php
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource{    public function toArray(Request $request): array    {        return [            'id' => $this->id,            'title' => $this->title,            'slug' => $this->slug,            'body' => $this->body,            'status' => $this->status,            'tags' => $this->tags ?? [],            'author' => $this->whenLoaded('user', fn () => $this->user->name),            'published_at' => $this->published_at?->toIso8601String(),        ];    }}

Line by line

  1. A resource is a typed view of a model for the outside world. Your database columns can change while the API stays the same. Generated with php artisan make:resource PostResource.
  2. $this stands in for the wrapped model, so $this->id reads $post->id.
  3. An enum is written to JSON as its value, so clients see "draft" or "published".
  4. whenLoaded includes the author only if the controller eager-loaded user, so a resource never triggers surprise queries.
  5. Dates become ISO 8601 strings, ready for new Date() in JS. The nullsafe ?-> handles drafts, which have no date.

Laravel 13 also ships JSON:API resources, which output the full JSON:API format (type, id, attributes, relationships, sparse fieldsets and includes) if your clients expect that standard. A plain JsonResource like this one is the simpler default.

Controllers, policies and routes

A policy holds the rules for who may do what with a model. Laravel finds PostPolicy for Post by name, so there is nothing to register.

php
<?php namespace App\Policies; use App\Models\Post;use App\Models\User; class PostPolicy{    public function update(User $user, Post $post): bool    {        return $user->id === $post->user_id;    }     public function delete(User $user, Post $post): bool    {        return $user->id === $post->user_id;    }}

Line by line

  1. Each method answers one question: may this signed-in user update this post? Only the author may. Returning false produces a 403.
php
<?php namespace App\Http\Controllers; use App\Enums\PostStatus;use App\Http\Requests\StorePostRequest;use App\Http\Resources\PostResource;use App\Jobs\NotifyFollowers;use App\Models\Post;use Illuminate\Http\JsonResponse;use Illuminate\Http\Resources\Json\AnonymousResourceCollection;use Illuminate\Http\Response;use Illuminate\Routing\Attributes\Controllers\Authorize; class PostController extends Controller{    public function index(): AnonymousResourceCollection    {        $posts = Post::published()->with('user')->latest('published_at')->paginate(10);         return PostResource::collection($posts);    }     public function show(Post $post): PostResource    {        abort_unless($post->status === PostStatus::Published, 404);         return new PostResource($post->load('user'));    }     public function store(StorePostRequest $request): JsonResponse    {        $post = $request->user()->posts()->create($request->validated());         if ($post->status === PostStatus::Published) {            NotifyFollowers::dispatch($post);        }         return (new PostResource($post))->response()->setStatusCode(201);    }     #[Authorize('update', 'post')]    public function update(StorePostRequest $request, Post $post): PostResource    {        $post->update($request->validated());         return new PostResource($post);    }     #[Authorize('delete', 'post')]    public function destroy(Post $post): Response    {        $post->delete();         return response()->noContent();    }}

Line by line

  1. Lists published posts, ten per page. A resource collection keeps the paginator's links and meta in the JSON.
  2. Route model binding. The route has {post}, the parameter is typed Post $post, so Laravel loads the post by id, or answers 404 if there is none. No findOrFail boilerplate.
  3. A draft is invisible to the public: abort_unless throws an HTTP exception that becomes a 404.
  4. Laravel sees the StorePostRequest type and validates before this method runs. Inside, the data is already known to be valid.
  5. $request->user() is the signed-in user from the Sanctum token. Creating through their posts() relationship sets the author, so a client can never post as someone else.
  6. Puts a job on the queue and returns at once. A worker picks it up in the background.
  7. A resource normally answers 200; a newly created post should be 201 Created.
  8. Laravel 13's controller attributes: before update runs, call the update method of the policy with the post route parameter. A stranger gets a 403 and this method never runs.
  9. A 204 No Content response, the usual answer to a successful DELETE.
php
<?php use App\Http\Controllers\PostController;use Illuminate\Http\Request;use Illuminate\Support\Facades\Route; Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum'); Route::apiResource('posts', PostController::class)->only(['index', 'show']); Route::middleware('auth:sanctum')->group(function () {    Route::apiResource('posts', PostController::class)->only(['store', 'update', 'destroy']);});

Line by line

  1. The simplest route: a URL and a closure. Whatever it returns becomes the response, and a model becomes JSON (minus its hidden columns).
  2. apiResource registers the standard REST routes and points each at a controller method: GET /api/posts → index, GET /api/posts/{post} → show. These two are public.
  3. Routes inside the group need a valid Sanctum token (Authorization: Bearer …): POST /api/posts → store, PUT/PATCH /api/posts/{post} → update, DELETE → destroy. Anyone else gets a 401.

Queues and jobs

Anything slow, such as sending email, resizing images or calling another API, should not make the user wait. A job is a class that describes the work; the queue stores it (in the database, Redis or SQS); a worker process runs it.

php
<?php namespace App\Jobs; use App\Models\Post;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Queue\Attributes\Backoff;use Illuminate\Queue\Attributes\Tries;use Illuminate\Support\Facades\Log; #[Tries(3)]#[Backoff([10, 60])]class NotifyFollowers implements ShouldQueue{    use Queueable;     public function __construct(public Post $post) {}     public function handle(): void    {        Log::info('Notifying followers', ['post' => $this->post->id]);    }}

Line by line

  1. Laravel 13 configures jobs with attributes: try at most three times before marking the job as failed.
  2. Wait 10 seconds before the second attempt and 60 before the third.
  3. ShouldQueue is what sends the job to the queue instead of running it right away. Generated with php artisan make:job NotifyFollowers.
  4. Only the model's id is stored on the queue. The worker reloads a fresh copy from the database when it runs the job.
  5. handle does the work in the worker process. Its parameters are resolved from the service container, so you can ask for a mailer or an HTTP client here.

Run php artisan queue:work to process jobs (composer run dev already does). In production you keep the worker alive with a process manager, or use Laravel Horizon for Redis queues with a dashboard. Scheduled tasks, your cron jobs, are declared in routes/console.php, as in Schedule::job(new SendDigest)->dailyAt('08:00').

Tokens with Sanctum

Sanctum gives each user simple API tokens, stored hashed in the personal_access_tokens table. A login route checks the password and hands one back:

php
Route::post('/tokens', function (Request $request) {    $request->validate([        'email' => ['required', 'email'],        'password' => ['required'],    ]);     $user = User::where('email', $request->email)->first();     if (! $user || ! Hash::check($request->password, $user->password)) {        throw ValidationException::withMessages(['email' => 'Wrong email or password.']);    }     $token = $user->createToken('cli', ['posts:write']);     return ['token' => $token->plainTextToken];});

Line by line

  1. $request->validate() is the inline version of a Form Request, fine for small routes. (The use lines for User, Hash and ValidationException are left out here.)
  2. Hash::check compares the password with the stored hash. Throwing a ValidationException answers 422 with the message under email, the same shape as any other validation error.
  3. Creates a token with a name and optional abilities (scopes), which you can check later with $request->user()->tokenCan('posts:write').
  4. The plain token is shown exactly once; only its hash is stored. The client sends it as Authorization: Bearer <token>.

For a browser front end on the same domain, such as a React SPA, Sanctum can use secure cookie sessions instead of tokens, so there is nothing to store in localStorage. The starter kits come with sign-in, registration, password reset and email verification already built.

Testing with Pest

Pest reads like Jest or Vitest. Feature tests send fake HTTP requests through the whole app, with a fresh in-memory SQLite database for each test.

php
<?php use App\Enums\PostStatus;use App\Jobs\NotifyFollowers;use App\Models\Post;use App\Models\User;use Illuminate\Support\Facades\Queue;use Laravel\Sanctum\Sanctum; it('lists only published posts', function () {    Post::factory()->published()->count(2)->create();    Post::factory()->create();     $this->getJson('/api/posts')        ->assertOk()        ->assertJsonCount(2, 'data');}); it('creates a post and queues the notification', function () {    Queue::fake();    Sanctum::actingAs($user = User::factory()->create());     $this->postJson('/api/posts', [        'title' => 'Hello Laravel',        'body' => 'My first post.',        'status' => 'published',    ])        ->assertCreated()        ->assertJsonPath('data.status', 'published');     expect($user->posts()->first())        ->status->toBe(PostStatus::Published)        ->published_at->not->toBeNull();     Queue::assertPushed(NotifyFollowers::class);}); it('rejects invalid input', function () {    Sanctum::actingAs(User::factory()->create());     $this->postJson('/api/posts', ['title' => ''])        ->assertUnprocessable()        ->assertJsonValidationErrors(['title', 'body']);}); it('does not let strangers edit a post', function () {    $post = Post::factory()->create();    Sanctum::actingAs(User::factory()->create());     $this->patchJson("/api/posts/{$post->id}", ['title' => 'Mine now', 'body' => '…'])        ->assertForbidden();});

Line by line

  1. it('…', fn) is a test, exactly like Vitest. Run them all with php artisan test or ./vendor/bin/pest.
  2. Factories with states: two published posts and one draft. The published() state is defined in the factory below.
  3. getJson sends a request through the app without a real server and returns a response you can assert on.
  4. Queue::fake() records jobs instead of running them, like vi.mock for the queue.
  5. Signs in as a fresh user for the rest of the test, with no token needed.
  6. Pest's expect chains: each ->status and ->published_at reads that property of the post and checks it.
  7. A 422 with errors for both fields: the Form Request did its job before the controller ran.
  8. The #[Authorize] attribute and the policy refuse the request with a 403.
php
<?php namespace Database\Factories; use App\Enums\PostStatus;use App\Models\User;use Illuminate\Database\Eloquent\Factories\Factory; class PostFactory extends Factory{    public function definition(): array    {        return [            'user_id' => User::factory(),            'title' => fake()->sentence(4),            'body' => fake()->paragraphs(3, true),            'tags' => fake()->randomElements(['php', 'laravel', 'js', 'ts'], 2),        ];    }     public function published(): static    {        return $this->state(fn () => [            'status' => PostStatus::Published,            'published_at' => now()->subDay(),        ]);    }}

Line by line

  1. The default attributes for a fake post. fake() is the Faker library: names, sentences, emails and more.
  2. Passing a factory as a value creates the related user first and uses their id.
  3. A state is a named variation. Tests call Post::factory()->published().

Front ends: Blade, Livewire or Inertia with React

Laravel can render HTML itself with Blade templates, add interactivity without writing JS using Livewire, or hand pages to a React, Vue or Svelte front end through Inertia. Inertia is the one JS developers usually reach for: the controller returns props, and a React component renders them. There is no separate API, no client-side router to configure and no fetch calls for the first load.

php
// routes/web.phpuse App\Http\Resources\PostResource;use App\Models\Post;use Inertia\Inertia; Route::get('/', function () {    return Inertia::render('posts/index', [        'posts' => PostResource::collection(Post::published()->latest()->take(10)->get()),    ]);});

Line by line

  1. Renders resources/js/pages/posts/index.tsx and passes it posts as props. On the first visit Laravel sends HTML with the props inlined; later clicks fetch only the JSON.
tsx
// resources/js/pages/posts/index.tsximport { Link } from '@inertiajs/react'; type Post = { id: number; title: string; slug: string; author?: string }; export default function Index({ posts }: { posts: { data: Post[] } }) {  return (    <ul>      {posts.data.map((post) => (        <li key={post.id}>          <Link href={'/posts/' + post.id}>{post.title}</Link>        </li>      ))}    </ul>  );}

Line by line

  1. A normal React component in TypeScript. Its props are exactly what the controller passed; a resource collection arrives under data.
  2. Inertia's Link swaps pages without a full reload, like Next.js Link. The starter kits add typed route helpers so you do not build URLs by hand.
blade
{{-- resources/views/posts/index.blade.php --}}<x-layout title="Journal">    @foreach ($posts as $post)        <article>            <h2>{{ $post->title }}</h2>            <p>{{ $post->user->name }} · {{ $post->published_at->diffForHumans() }}</p>        </article>    @endforeach</x-layout>

Line by line

  1. The same list in Blade, Laravel's template language, if you would rather render HTML on the server. {{-- --}} is a comment.
  2. <x-layout> is a component, a file in resources/views/components, used like a JSX component with props and children.
  3. @foreach, @if and friends are directives that compile to plain PHP and are cached.
  4. {{ }} prints a value HTML-escaped, like JSX does, so user input cannot inject markup. diffForHumans() prints "2 days ago".

How major companies use TypeScript/JavaScript, Go and PHP together

Large products rarely run on one language. A pattern you will see again and again is TypeScript or JavaScript for the interface, PHP for the main web application, and Go for high-throughput services and infrastructure. Some well-documented examples:

  • Meta (Facebook). Facebook began as a PHP application. To keep PHP's fast edit-and-reload cycle while catching errors earlier, it built HHVM, a JIT-compiling virtual machine, and Hack, a gradually typed dialect of PHP, and moved almost its whole codebase to Hack. On the interface side Meta created React. Adding types to a PHP codebase step by step is the same journey as moving a JS codebase to TypeScript.
  • Slack. Its core application and API are PHP/Hack running on HHVM, a move it made in 2016 for speed and static types. The real-time messaging layer is a set of services written in Java and Go, a few services are Node, and the clients are JavaScript and React, with Electron on the desktop.
  • Wikipedia. MediaWiki, the software behind every Wikipedia page, is PHP. Its modern wikitext parser, Parsoid, started life as a Node.js service and was ported to PHP, where it runs about twice as fast and now ships inside MediaWiki.
  • Etsy. The marketplace runs on a large PHP back end, and its front end, a monorepo with tens of thousands of JavaScript files, was migrated to TypeScript in 2021.
  • WordPress. The PHP core runs a huge share of the web, and its block editor, Gutenberg, is built with React, with TypeScript types published for its packages. Laravel developers meet the same split: PHP on the server and a React or Vue front end through Inertia.
  • Microsoft. TypeScript 7 (2026) replaced the JavaScript-based compiler with a native one written in Go, about ten times faster on large projects. Go was chosen because its structure and garbage collection let the team port the existing code nearly line by line.
  • Uber and the cloud-native world. Uber runs one of the largest Go codebases anywhere for its back-end services, and the infrastructure most PHP apps now deploy on (Docker, Kubernetes and Terraform) is written in Go. So is the JavaScript bundler esbuild.

For a JS/TS developer the takeaway is that these languages fit together rather than compete. TypeScript stays on the front end (with Inertia it plugs straight into Laravel), PHP and Laravel carry the product's business logic, and Go appears where you need a small, fast network service or a piece of infrastructure. Knowing all three makes you useful on almost any web team.

Pros and cons

Where PHP and Laravel shine

  • Productivity. Auth, queues, mail, file storage, caching, rate limiting, scheduling and testing are built in and work together. You spend your time on the product.
  • Simple, robust deployment. Shared-nothing requests are easy to scale and hard to break: one bad request cannot take the process down. Hosting is everywhere and cheap, and Laravel Cloud and Forge make it one click.
  • Mature ecosystem. Packagist has hundreds of thousands of packages, and Laravel's first-party ones (Horizon, Reverb, Scout, Cashier, Socialite, Pulse) cover most needs.
  • Excellent documentation, and a stable yearly release cycle with gentle upgrades.
  • A modern language. Types, enums, readonly objects, property hooks, attributes and pipes make PHP 8.5 pleasant to write.
  • Full-stack TypeScript still fits. With Inertia you keep React and TypeScript for the UI and use Laravel for everything behind it.

Where it hurts

  • An inconsistent standard library. Decades of history show: str_replace($search, $replace, $subject) but strpos($haystack, $needle), and array_map and array_filter take their arguments in opposite orders.
  • Type checking happens late. Types are enforced at run time; you need PHPStan or Larastan in CI to get TypeScript-style safety before you deploy.
  • Not built for long-lived connections. WebSockets and streaming need a separate server (Reverb) or a long-running runtime (Octane), which brings back Node-style memory concerns.
  • "Magic." Facades, auto-discovery and conventions make Laravel fast to write but harder to trace until you learn where things come from. Your IDE (with the Laravel extension) and php artisan route:list help.
  • Reputation. Some teams still judge PHP by its past. Code examples you find online may be ten years out of date: prefer the official docs.

What you can build

  • SaaS products and dashboards, with billing (Cashier for Stripe and Paddle), teams and roles.
  • JSON APIs for mobile apps and SPAs, exactly like the one in this guide.
  • Content sites and e-commerce: WordPress, WooCommerce, Drupal, Magento and Statamic are all PHP.
  • Admin panels in hours with Filament, a Laravel package for forms, tables and dashboards.
  • Real-time apps with Reverb (WebSockets) and Laravel Echo on the front end.
  • AI features with the Laravel AI SDK: text generation, tool-calling agents, embeddings and vector search behind one provider-agnostic API.

Your roadmap from zero to hero

  1. Week 1: the language. Install PHP 8.5 and work through every example above. Rewrite a small JS utility of yours in PHP with strict_types.
  2. Week 2: Composer and tools. Start a tiny package with PSR-4 autoloading, Pest tests and PHPStan. Format with Laravel Pint.
  3. Week 3: Laravel basics. laravel new, routes, controllers, Blade, migrations and Eloquent. Build a to-do app with the Livewire or React starter kit.
  4. Week 4: the API from this guide. Form Requests, resources, policies, Sanctum and a full Pest suite. Aim for every endpoint covered by a feature test.
  5. Month 2: production skills. Queues and Horizon, caching with Redis, the scheduler, file storage on S3, mail, rate limiting and logging. Deploy to Laravel Cloud, Forge or a VPS.
  6. Month 3: depth. The service container and service providers, events and listeners, Octane, Reverb, and reading framework source when docs run out. Contribute a fix to a package you use.
  7. Keep going: follow the release notes each year, read Laravel News, and look at well-known open-source Laravel apps such as Pixelfed and Coolify to see how big codebases are organised.

PHP is the language that keeps showing up for work. Learn its modern form, pair it with Laravel, and you can ship a whole product while others are still choosing their stack.

— Field note

JavaScript ဒါမှမဟုတ် TypeScript နဲ့ အသက်မွေးနေသူဆိုရင် PHP အကြောင်း စကားနှစ်ခွန်း ကြားဖူးမှာပါ — web ရဲ့ အစိတ်အပိုင်း အများကြီးကို PHP က run နေတယ်၊ ပြီးတော့ PHP က ဟောင်းပြီး ရှုပ်ပွတယ်။ ပထမစကားက မှန်ပါတယ်။ ဒုတိယစကားကတော့ ၂၀၁၀ ခုနှစ်က PHP 5 ကို ပြောနေတာပါ။ ခေတ်သစ် PHP က type ပါတဲ့၊ မြန်တဲ့၊ object-oriented ဘာသာစကားဖြစ်ပြီး enum၊ readonly class၊ property hook တွေ ပါသလို PHP 8.5 ကစပြီး pipe operator လည်း ပါလာပါပြီ။ အကျော်ကြားဆုံး framework ဖြစ်တဲ့ Laravel 13 က routing၊ ORM၊ validation၊ queue၊ auth နဲ့ testing ကို တစ်စုတစ်စည်းတည်း ပေးပါတယ်။

ဒီ guide က သုညကနေ စပြီး တကယ့် Laravel code ကို ဖတ်နိုင်၊ JSON API အသေးလေးတစ်ခု ကိုယ်တိုင် ဆောက်နိုင်တဲ့ အဆင့်အထိ ခေါ်သွားပါမယ်။ အပိုင်းတိုင်းမှာ PHP idea တစ်ခုကို သင်သိပြီးသား JS/TS idea နဲ့ ဘေးချင်းယှဉ်ပြထားပြီး ဥပမာတိုင်းအောက်မှာ တစ်ကြောင်းချင်း မှတ်စု တွေ ပါပါတယ်။ Highlight လုပ်ထားတဲ့ စာကြောင်းကို နှိပ်ပြီး (ဒါမှမဟုတ် hover လုပ်ပြီး) ဘာလုပ်လဲ ဖတ်ကြည့်ပါ။ ရိုးရိုး PHP ဥပမာတွေက live ပါ — တစ်ခုခုကို ပြင်ပြီး Run ကို နှိပ်ရင် (ဒါမှမဟုတ် ⌘/Ctrl + Enter) ဒီစာမျက်နှာပေါ်မှာတင် PHP 8.5 နဲ့ run ပေးပါတယ်။

JS/TS developer တစ်ယောက်အတွက် ဘာကြောင့် PHP လဲ

PHP ကို ၁၉၉၄ ခုနှစ်မှာ Rasmus Lerdorf က သူ့ home page အတွက် tool အနည်းငယ်အဖြစ် စတင်ဖန်တီးခဲ့ပြီး နောက်ပိုင်း general-purpose ဘာသာစကားတစ်ခု ဖြစ်လာပါတယ်။ ဒီနေ့ WordPress၊ Wikipedia နဲ့ server ဘာသာစကား သိရတဲ့ website တွေရဲ့ လေးပုံသုံးပုံလောက်ကို PHP က run နေပါတယ်။ PHP 7 (၂၀၁၅) ကစပြီး အမြန်နှုန်း နှစ်ဆလောက် တိုးလာခဲ့ပြီး PHP 8 ကစပြီး JIT compiler၊ union type၊ enum၊ fiber တွေနဲ့ ပိုတင်းကျပ်ပြီး ပိုသဘာဝကျတဲ့ အပြုအမူတွေ ရလာပါတယ်။

စဉ်းစားပုံ ပြောင်းရမယ့်အချက်တွေကို ဘေးချင်းယှဉ်ကြည့်ရအောင်—

  • Request တွေက ဘာမှ မျှမသုံးဘူး (shared-nothing)။ Node server က request တွေကြား memory ထဲမှာ state ထိန်းထားတဲ့ ကြာရှည် run နေတဲ့ process တစ်ခုပါ။ ရိုးရိုး PHP ကတော့ request တိုင်းကို အသစ်စက်စက်က စတယ် — load လုပ်၊ run၊ အဖြေပြန်ပြီး အကုန်လွှင့်ပစ်တယ်။ Crash ဖြစ်တာ၊ memory leak ဖြစ်တာက request တစ်ခုတည်းကိုပဲ ထိခိုက်ပါတယ်။
  • Type တွေကို run နေချိန်မှာ စစ်တယ်။ TypeScript က build လုပ်ချိန်မှာ type စစ်ပြီး ဖျက်ပစ်ပါတယ်။ PHP ကတော့ type declaration တွေကို run နေတဲ့ program ထဲမှာ ထိန်းထားပြီး ခေါ်တိုင်း စစ်ပါတယ်။ Static analyser တွေ (PHPStan၊ Psalm) က သင်ရင်းနှီးတဲ့ build-time စစ်ဆေးမှုကို ထပ်ဖြည့်ပေးပါတယ်။
  • Array type တစ်ခုတည်းက အကုန်လုပ်တယ်။ PHP array က ordered map တစ်ခုပါ — JS array၊ object literal နဲ့ Map သုံးမျိုးလုံး တစ်ပြိုင်နက် ဖြစ်ပါတယ်။
  • Blocking ဖြစ်တာ ပုံမှန်ပါ။ သင့် code ထဲမှာ event loop မရှိ၊ async/await အရောင်ခွဲစရာ မလိုပါဘူး။ Database call က ရိုးရိုးလေး စောင့်ပါတယ်၊ concurrency ကတော့ PHP worker အများကြီးကို ဘေးချင်းယှဉ် run တာကနေ ရပါတယ်။
  • Composer က npm ပါ။ composer.json က သင့် package.json ဖြစ်ပြီး Packagist က registry ပါ။
  • Laravel က Node မှာ ဘယ်တော့မှ အခြေမကျခဲ့တဲ့ batteries-included framework ပါ — Express၊ Prisma၊ Zod၊ BullMQ၊ NextAuth နဲ့ Vitest ကို အတူတကွ ဒီဇိုင်းဆွဲပြီး တစ်နေရာတည်းမှာ document လုပ်ထားတာလို့ မြင်ကြည့်ပါ။

PHP နဲ့ Laravel ကို install လုပ်ခြင်း

macOS နဲ့ Windows မှာ အလွယ်ဆုံးနည်းက Laravel Herd ပါ — PHP၊ Composer နဲ့ Laravel installer ကို install လုပ်ပေးပြီး project တွေကို local မှာ serve လုပ်ပေးတဲ့ အခမဲ့ app ပါ။ ဘယ် OS မှာမဆို php.new က one-line installer တွေက terminal ထဲမှာ အလားတူ လုပ်ပေးပါတယ်။ ကိုယ့်မှာ ဘာရှိလဲ စစ်ကြည့်ပါ—

bash
php -v                 # PHP 8.5.x (cli)composer --version     # Composer 2.xphp -a                 # an interactive shell, like running node with no filephp hello.php          # run a script, like node hello.jsphp -S localhost:8000  # a tiny dev web server for the current folder

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. php -v က version ကို ပြပါတယ်။ 8.3 အောက်ဆိုရင် Laravel 13 အတွက် ဟောင်းလွန်းပါတယ်၊ ဒီ guide က 8.5 ကို သုံးပါတယ်။
  2. Composer က PHP ရဲ့ package manager ပါ၊ ဒီလောကရဲ့ npm ပေါ့။ vendor/ folder (သင့် node_modules) ထဲကို install လုပ်ပါတယ်။
  3. Interactive shell က ရိုက်ထည့်သလောက် PHP ကို evaluate လုပ်ပါတယ်။ Laravel project ထဲမှာတော့ သင့် app ကိုပါ load လုပ်ပေးတဲ့ php artisan tinker ကို သုံးပါမယ်။
  4. File တစ်ခုကို အပေါ်ကနေ အောက်အထိ run ပြီး ထွက်သွားပါတယ်။ အောက်က PHP သီးသန့် ဥပမာတိုင်းကို ဒီလို စမ်းပါ — .php file အဖြစ် သိမ်းပြီး run ပါ။
  5. PHP မှာ development server ပါလာလို့ Apache ဒါမှမဟုတ် Nginx မလိုဘဲ .php file တွေပါတဲ့ folder ကို serve လုပ်နိုင်ပါတယ်။ Production မှာ ဘယ်တော့မှ မသုံးပါနဲ့။

Hello, PHP

PHP file တစ်ခုက "HTML mode" နဲ့ စပြီး <?php ရောက်မှ code အဖြစ် ပြောင်းပါတယ်။ Code သက်သက်ပဲ ပါတဲ့ file က ဒီ tag နဲ့ စပြီး အပိတ် ?> ကို ချန်ထားပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. အဖွင့် tag ပါ။ ဒီနောက်က အရာအားလုံးက PHP ပါ။ Code သက်သက်ပါတဲ့ file က tag ကို ဘယ်တော့မှ မပိတ်ပါဘူး၊ ဒါမှ အပိတ်နောက်က space တွေ response ထဲ ပေါက်ကြားမသွားမှာပါ။
  2. ဒီ file အတွက် strict typing ကို ဖွင့်ပါတယ် — int လို့ ကြေညာထားတဲ့နေရာကို string ပေးရင် တိတ်တဆိတ် ပြောင်းမယ့်အစား error ဖြစ်ပါတယ်။ ကိုယ်ရေးတဲ့ file တိုင်းရဲ့ ထိပ်မှာ ထည့်ပါ၊ PHP ရဲ့ "strict": true ပါ။
  3. Variable တိုင်း $ နဲ့ စပါတယ်။ let ဒါမှမဟုတ် const မရှိပါဘူး — ပထမဆုံး assign လုပ်တာနဲ့ ဖန်တီးပြီးသား ဖြစ်ပါတယ်။ Statement တွေကို semicolon နဲ့ ဆုံးရပြီး မထည့်လို့ မရပါဘူး။
  4. Double quote တွေက JS template literal လို variable တွေကို interpolate လုပ်ပါတယ်၊ တွန့်ကွင်းတွေက နယ်နိမိတ်ကို ရှင်းစေပါတယ်။ Single quote က ဘယ်တော့မှ interpolate မလုပ်ပါဘူး။
  5. echo က print ထုတ်ပါတယ်။ . က string ဆက်တဲ့ operator ပါ (+ က ဂဏန်းပဲ ပေါင်းပါတယ်)။ PHP_EOL က လက်ရှိ OS ရဲ့ newline ပါ။
  6. echo က comma ခံပြီး တန်ဖိုးအများကြီး ယူနိုင်ပါတယ်။ Built-in function တွေက global namespace တစ်ခုတည်းမှာ ရှိလို့ s.toUpperCase() မဟုတ်ဘဲ strtoupper($s) ပါ။
  7. printf က C လို format လုပ်ပါတယ် — string အတွက် %s၊ integer အတွက် %d။ strlen က byte တွေကို ရေတွက်ပါတယ်၊ မြန်မာစာလို ASCII မဟုတ်တဲ့ စာတွေမှာ character ရေဖို့ mb_strlen ကို သုံးပါ။

Type၊ strict mode နဲ့ နှိုင်းယှဉ်ခြင်း

PHP type တွေက TypeScript annotation တွေနဲ့ အတော်တူပေမဲ့ program run နေချိန်မှာ တကယ် အတင်းကျင့်သုံးပါတယ်။ Parameter၊ return value နဲ့ class property တွေ အားလုံးကို type ပေးနိုင်ပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Parameter type တွေက နာမည်ရဲ့ ရှေ့ မှာ ရှိပြီး return type က colon နောက်မှာ ရှိပါတယ်။ PSR-12 style စည်းကမ်းအရ body ကို ကိုယ်ပိုင်စာကြောင်းပေါ်က တွန့်ကွင်းထဲ ထားပါတယ်။
  2. int|string က TS အတိုင်း union type ပါ။ ?string ဆိုတာ string|null ဖြစ်ပြီး = null က default value ပေးပါတယ်။
  3. ?? က JS အတိုင်း null coalescing operator ပါ။ မရှိတဲ့ array key တွေ၊ မသတ်မှတ်ရသေးတဲ့ variable တွေပေါ်မှာလည်း warning မပေးဘဲ အလုပ်လုပ်ပါတယ်။
  4. var_dump က တန်ဖိုးတွေကို type နဲ့တကွ print ထုတ်ပါတယ် — bool(false) bool(true) bool(false)။ PHP 8 ကစပြီး 0 == 'a' က false ပါ။ Loose == က '1' == '01' ကို ဂဏန်းအဖြစ် ပြောင်းနေတုန်းမို့ JS မှာလိုပဲ === နဲ့ !== ကို အကျင့်လုပ်ပြီး သုံးပါ။
  5. strict_types ကြောင့် string '2' ကို int parameter အတွက် ငြင်းပယ်ပါတယ်။ မပါရင် PHP က တိတ်တဆိတ် ပြောင်းပေးလိုက်မှာပါ။
  6. Type စစ်လို့ မအောင်ရင် TypeError ကို throw လုပ်ပါတယ်။ $e::class က ဘယ် object ရဲ့ class နာမည်ကိုမဆို ပေးပါတယ်။
  7. ?-> က nullsafe operator ပါ၊ PHP ရဲ့ optional chaining ပေါ့။ ဒီမှာ $user က null ဖြစ်လို့ chain တစ်ခုလုံး null ဖြစ်ပြီး ?? က guest ကို ဖြည့်ပေးပါတယ်။

Scalar type တွေက int၊ float၊ string နဲ့ bool ပါ။ ဒါတွေအပြင် PHP မှာ array၊ object၊ callable၊ iterable၊ mixed (unknown လိုမျိုး)၊ void၊ never (အမြဲ throw ဒါမှမဟုတ် exit လုပ်တဲ့ function)၊ null၊ true နဲ့ false ရှိပြီး class နဲ့ interface နာမည်တွေကိုလည်း type အဖြစ် သုံးနိုင်ပါတယ်။

Array: list နဲ့ object အတွက် type တစ်ခုတည်း

နေ့စဉ်သုံးရာမှာ အကြီးဆုံး ကွာခြားချက်ပါ။ PHP array က key တွေကနေ value တွေဆီ ordered map တစ်ခုပါ။ Key တွေက 0၊ 1၊ 2… ဆိုရင် JS array လို ပြုမူပြီး string key တွေဆိုရင် object literal လို ပြုမူပါတယ်။ ပြီးတော့ အဲဒါက reference မဟုတ်ဘဲ value တစ်ခုပါ။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. လေးထောင့်ကွင်းက array ကို ဖန်တီးပြီး key မပါရင် PHP က 0၊ 1၊ 2 လို့ နံပါတ်တပ်ပေးပါတယ်။ ဒါက list တစ်ခုပါ။
  2. $array[] = value က push လို နောက်ဆုံးမှာ ထပ်ထည့်ပါတယ်။
  3. => က key ကို value နဲ့ တွဲပါတယ်။ ဒီ array က { name: 'Aye', age: 29 } ရဲ့ အခန်းကဏ္ဍကို ယူပါတယ်။ Key တွေကို လေးထောင့်ကွင်းနဲ့ ဖတ်ပါတယ် — $user['name']။
  4. မရှိတဲ့ key ကို ဒီအတိုင်း ဖတ်ရင် warning ပေးပြီး null ရပါမယ်။ ?? ပါရင်တော့ user.email ?? 'no email' လို ဘေးကင်းပါတယ်။
  5. Destructuring က နှစ်မျိုးလုံးမှာ အလုပ်လုပ်ပါတယ် — နေရာအလိုက် (နေရာအလွတ်နဲ့ တစ်ခု ကျော်နိုင်) နဲ့ key အလိုက်။ const [first, , third] = langs နဲ့ idea တူတူပါပဲ။
  6. Array function တွေက array ကို argument အဖြစ် ယူပါတယ် — array_map(callback, array) ဖြစ်ပေမဲ့ array_filter(array, callback) ပါ။ Argument အစီအစဉ် မညီညွတ်တာ နာမည်ကြီးလို့ editor ကို အားကိုးပါ။ fn (…) => က arrow function ပါ။
  7. သတိထားစရာ — array_filter က မူလ key တွေကို ထိန်းထားလို့ $short က [1 => 'ts', 2 => 'go'] ဖြစ်ပြီး JSON object {"1":"ts","2":"go"} အဖြစ် ပြောင်းသွားပါတယ်။ array_values က list အဖြစ် နံပါတ်ပြန်တပ်ပေးပါတယ် — ["ts","go"]။
  8. Array ကို assign လုပ်ရင် copy ကူးပါတယ် (lazy လုပ်လို့ မရေးမချင်း စျေးမကြီးပါဘူး)။ $copy ကို ပြောင်းတာက $langs ကို မထိပါဘူး။ JS မှာဆိုရင် နာမည်နှစ်ခုလုံးက array တစ်ခုတည်းကို ညွှန်နေမှာပါ။
  9. Spread operator က string key တွေနဲ့လည်း အလုပ်လုပ်ပြီး နောက်က key က နိုင်ပါတယ်။ ဒါက { ...user, age: 30, city: 'Yangon' } ပါ။
  10. array_find (PHP 8.4) က Array.prototype.find လို ကိုက်ညီတဲ့ ပထမဆုံးတန်ဖိုးကို ပြန်ပေးပါတယ်။ array_find_key၊ array_any နဲ့ array_all (some နဲ့ every) လည်း ရှိပါတယ်။
  11. array_first နဲ့ array_last (PHP 8.5) က internal pointer ကို မရွှေ့ဘဲ ပထမနဲ့ နောက်ဆုံး တန်ဖိုးတွေကို ဖတ်ပေးပါတယ်။ Array အလွတ်ဆိုရင် null ပြန်ပေးပါတယ်။

Function၊ closure နဲ့ callable

Function တွေက ရင်းနှီးပြီးသားလို ခံစားရပေမဲ့ JS မှာ မရှိတဲ့ feature နှစ်ခု ပါပါတယ် — named argument တွေနဲ့ closure က ဘာကို ဖမ်းယူမလဲ ကိုယ်တိုင် ထိန်းချုပ်နိုင်တာပါ။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Named argument တွေက လိုတဲ့ parameter ဆီ တန်းကျော်သွားခွင့် ပေးပါတယ်။ JS မှာ options object နဲ့ အတုလုပ်ရပေမဲ့ PHP မှာ ဘယ် function ကိုမဆို ဒီလို ခေါ်နိုင်လို့ parameter နာမည်တွေက function ရဲ့ public API ထဲ ပါဝင်ပါတယ်။
  2. ...$numbers က ကျန်တဲ့ argument တွေကို array တစ်ခုထဲ စုပေးပါတယ် — rest parameter ...numbers: number[] ပါ။
  3. ... တစ်ခုတည်းက JS မှာ total(...[4, 5]) လို array ကို argument တွေအဖြစ် ဖြန့်ပေးပါတယ်။ ဒါက 6 9 ကို print ထုတ်ပါတယ်။
  4. Anonymous function က use ထဲမှာ မစာရင်းသွင်းရင် အပြင်ဘက် scope က ဘာမှ မမြင်ပါဘူး။ &$count က reference နဲ့ ဖမ်းယူလို့ closure က ပြောင်းလဲနိုင်ပါတယ်။ & မပါရင် copy ကိုပဲ ရမှာပါ။
  5. Arrow function fn က အပြင်ဘက် variable တွေကို အလိုအလျောက်၊ value နဲ့ ဖမ်းယူပြီး body က expression တစ်ခုတည်း ဖြစ်ပါတယ်။ JS arrow function နဲ့ အနီးစပ်ဆုံးပါ။ Float တွေက နောက်က .0 မပါဘဲ print ထွက်လို့ ဒါက 110 ကို ပြပါတယ်။
  6. First-class callable syntax name(...) က ဘယ် function ဒါမှမဟုတ် method ကိုမဆို ပို့ပေးလို့ရတဲ့ Closure object အဖြစ် ပြောင်းပေးပါတယ် — String.prototype.toUpperCase ကို ပို့သလိုပဲ၊ ဒါပေမဲ့ bind ပြီးသား၊ type-safe ဖြစ်ပါတယ်။

Pipe operator (PHP 8.5 မှာ အသစ်)

PHP ရဲ့ standard library က method မဟုတ်ဘဲ function တွေ ဖြစ်နေလို့ chain လုပ်ရင် အတွင်းကနေ အပြင်ကို ဖတ်ရပါတယ်။ PHP 8.5 က ဘယ်ဘက်က တန်ဖိုးကို ညာဘက်က callable ထဲ ပို့ပေးတဲ့ |> ကို ထည့်ပေးပါတယ်။ JS မှာ pipe proposal က နှစ်တွေကြာ ရှိနေခဲ့ပေမဲ့ PHP ကတော့ ထုတ်ပေးလိုက်ပါပြီ။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. နည်းဟောင်း — ဖတ်တဲ့ အစီအစဉ်နဲ့ ပြောင်းပြန် run တဲ့ ထပ်ဆင့်ခေါ်မှု လေးခု။
  2. Pipeline က တန်ဖိုးတစ်ခုနဲ့ စပါတယ်…
  3. …ပြီးတော့ |> တိုင်းက နောက် callable ကို အဲဒီတန်ဖိုးနဲ့ ခေါ်ပါတယ်။ trim(...) က first-class callable ဖြစ်လို့ ဒါက trim($title) ပါ။
  4. ဒီနေရာမှာ callable က argument တစ်ခုပဲ ယူနိုင်ပါတယ်။ ပိုလိုရင် arrow function နဲ့ ထုပ်ပါ။ fn ကို ဝိုင်းထားတဲ့ ကွင်းတွေက မဖြစ်မနေ လိုပါတယ်၊ မပါရင် PHP က parse မလုပ်ပေးပါဘူး။
  5. နောက်ဆုံးအဆင့်ရဲ့ ရလဒ်က expression တစ်ခုလုံးရဲ့ တန်ဖိုး ဖြစ်လာပါတယ် — php-8-5-the-pipe-operator။
  6. နှစ်မျိုးလုံးက string တူတူ ထုတ်ပေးပါတယ်။ Pipe တွေက Laravel ရဲ့ Str helper တွေ၊ ကိုယ်ပိုင် ရည်ရွယ်ချက်တစ်ခုတည်းရှိတဲ့ function အသေးလေးတွေနဲ့ လိုက်ဖက်ပါတယ်။

Class၊ enum နဲ့ interface

PHP ရဲ့ object model က JavaScript ရဲ့ prototype တွေထက် TypeScript နဲ့ ပိုနီးပါတယ် — တကယ့် class၊ interface၊ abstract class၊ run time မှာ အတင်းကျင့်သုံးတဲ့ visibility နဲ့ native enum တွေ ရှိပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Backed enum တစ်ခုပါ — case တစ်ခုစီမှာ string တန်ဖိုး ရှိပါတယ်။ TS string union နဲ့မတူဘဲ ကြေညာလို့ရတဲ့ တကယ့် type ဖြစ်ပြီး TS enum နဲ့မတူဘဲ method တွေနဲ့အတူ run time မှာ တကယ်ရှိပါတယ်။
  2. Enum တွေမှာ method တွေ ထည့်နိုင်ပြီး interface တွေကို implement လုပ်နိုင်ပါတယ်။ အထဲမှာ $this က လက်ရှိ case ပါ။
  3. match က တန်ဖိုး ပြန်ပေးတဲ့၊ fall-through မရှိတဲ့ တင်းကျပ်တဲ့ (===) switch ပါ။ ဘယ် arm နဲ့မှ မကိုက်ရင် UnhandledMatchError ကို throw လုပ်လို့ case ကျန်ခဲ့တာက တိတ်တဆိတ် undefined မဟုတ်ဘဲ ကျယ်ကျယ်လောင်လောင် bug ဖြစ်ပါတယ်။
  4. Interface က TS မှာလို အလုပ်လုပ်ပေမဲ့ ကွာခြားချက်တစ်ခု ရှိပါတယ် — class က implements HasSlug လို့ ကြေညာ ရပါတယ်။ PHP က type တွေကို ပုံသဏ္ဌာန်နဲ့ မဟုတ်ဘဲ နာမည်နဲ့ စစ်ပါတယ်။
  5. final က subclass လုပ်ခွင့် ပိတ်ပါတယ်။ readonly class က property တိုင်းကို တစ်ကြိမ်ပဲ assign လုပ်ခွင့်ပေးပါတယ် — frozen TS class လို immutable value object ပါ။
  6. Constructor promotion — visibility keyword ပါတဲ့ parameter တစ်ခုက property အဖြစ်ပါ ကြေညာပြီး assign လုပ်ပေးပါတယ်။ ဒီလေးကြောင်းက property စာရင်းနဲ့ $this->title = $title; စာကြောင်းတွေကို အစားထိုးပါတယ်၊ TS ရဲ့ constructor(public title: string) အတိုင်းပါပဲ။ နောက်ဆုံး comma ထည့်ခွင့် ရှိပါတယ်။
  7. Property နဲ့ method တွေကို . နဲ့ မဟုတ်ဘဲ -> နဲ့ ရောက်ပါတယ် (dot ကို string ဆက်ဖို့ ယူထားပြီးပါပြီ)။ $this ကို အမြဲ ရေးရပါတယ်။
  8. new က object ကို ဖန်တီးပါတယ်။ Named argument တွေကြောင့် status ကို ကျော်ပြီး tags ကိုပဲ သတ်မှတ်နိုင်ပါတယ်။ ဒါက hello-laravel · Not live yet ကို print ထုတ်ပါတယ်။
  9. from က သိမ်းထားတဲ့ တန်ဖိုးကို case အဖြစ် ပြန်ပြောင်းပေးပြီး မသိတဲ့ တန်ဖိုးဆိုရင် throw လုပ်ပါတယ်၊ tryFrom ကတော့ null ပြန်ပေးပါတယ်။ ->name က case နာမည် Published ပါ။
  10. Readonly property ကို ရေးရင် Error ကို throw လုပ်ပါတယ် — "Cannot modify readonly property Post::$title"။ Immutable object ကို "ပြောင်း" ချင်ရင် အောက်နှစ်ပိုင်းမှာ ပြထားသလို ပြင်ထားတဲ့ copy တစ်ခု လုပ်ပါ။

Laravel မှာ အမြဲတွေ့ရမယ့် အပိုင်း နောက်ထပ်နှစ်ခု ရှိပါတယ်။ Trait တွေက class တစ်ခုက use SomeTrait; နဲ့ ဆွဲသွင်းတဲ့ ပြန်သုံးလို့ရတဲ့ method အစုအဝေးတွေပါ (ဥပမာ Laravel model တွေက HasFactory ကို သုံးပါတယ်)၊ mix-in တစ်မျိုးပေါ့။ Static member တွေက instance မဟုတ်ဘဲ class ပိုင်ဖြစ်ပြီး Post::query() ဒါမှမဟုတ် Status::from() လို :: နဲ့ ရောက်ပါတယ်။

Property hook နဲ့ asymmetric visibility (PHP 8.4)

TypeScript မှာဆိုရင် get နဲ့ set accessor တွေ ဒါမှမဟုတ် public getter ပါတဲ့ private field တစ်ခု ရေးမှာပါ။ PHP 8.4 က idea နှစ်ခုလုံးကို property ပေါ်မှာ တိုက်ရိုက် ထားပေးပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Asymmetric visibility — $logins ကို ဘယ်သူမဆို ဖတ်နိုင်ပေမဲ့ class ကပဲ ရေးနိုင်ပါတယ်။ Private field နဲ့ getter တွဲရေးစရာ မလိုတော့ပါဘူး။
  2. Property hook တစ်ခုပါ။ $user->email = … လို့ ရေးတဲ့ code က ဒီ set block ကို run ပေမဲ့ အပြင်ကကြည့်ရင် ရိုးရိုး property လိုပဲ မြင်ရပါတယ်။
  3. ကိုယ်ပိုင် hook ထဲမှာ property ကို assign လုပ်တာက တန်ဖိုးကို သိမ်းပါတယ်။ ဒီမှာ email တိုင်းကို ဝင်လာချိန်မှာ စစ်ဆေးပြီး စာလုံးအသေး ပြောင်းပါတယ်။
  4. get hook တစ်ခုတည်းပါတဲ့ virtual property ပါ။ ကိုယ်ပိုင် သိုလှောင်မှု မရှိဘဲ ဖတ်တိုင်း တွက်ချက်ပါတယ်၊ TS getter လိုပါပဲ။
  5. Constructor ရဲ့ assign လုပ်မှုကလည်း set hook ကို ဖြတ်သွားလို့ ပထမဆုံး တန်ဖိုးကိုတောင် စစ်ဆေးပါတယ်။
  6. aye@example.com example.com 1 ကို print ထုတ်ပါတယ်။
  7. အပြင်ကနေ ရေးရင် မအောင်မြင်ပါဘူး — "Cannot modify private(set) property User::$logins from global scope"။
  8. PHP 8.4 ကစပြီး JS မှာလို new ကို ကွင်းနဲ့ မထုပ်ဘဲ method ကို တန်းခေါ်တာ၊ property ကို တန်းဖတ်တာ လုပ်နိုင်ပါပြီ။ ဒါက shop.dev ကို print ထုတ်ပါတယ်။

Clone-with နဲ့ immutable update (PHP 8.5)

TS မှာ ပြောင်းထားတဲ့ copy ရဖို့ { ...price, amount: 1250 } လို့ ရေးပါတယ်။ Readonly PHP object တွေမှာ ဒီအတွက် boilerplate အများကြီး လိုခဲ့ပါတယ်။ PHP 8.5 က clone ကို ပြောင်းချင်တဲ့ property တွေ လက်ခံခွင့်ပေးပြီး immutable method တစ်ခုကို ခေါ်ပြီး ရလဒ်ကို လွှင့်ပစ်မိတာ မဖြစ်အောင် #[\NoDiscard] ကိုလည်း ထည့်ပေးပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. ငွေကို cent အရေအတွက် integer အဖြစ် သိမ်းပါ။ Float က PHP မှာရော JS မှာပါ 0.1 ကို အတိအကျ မထိန်းနိုင်ပါဘူး။
  2. Attribute တစ်ခုပါ (#[…] လို့ ရေးတဲ့ PHP ရဲ့ decorator)။ #[\NoDiscard] က return value ကို လျစ်လျူရှုတိုင်း PHP ကို warning ပေးခိုင်းပါတယ်။ ရှေ့က backslash က "global namespace" ကို ဆိုလိုပါတယ်။
  3. clone($object, [...]) က object ကို copy ကူးပြီး copy ပေါ်မှာ ဖော်ပြထားတဲ့ property တွေကို သတ်မှတ်ပါတယ်။ Visibility ကို လေးစားလို့ readonly property တွေကို class အတွင်းကနေပဲ ဒီလို သတ်မှတ်နိုင်ပါတယ်၊ with… method ဆိုတာ ဒါအတွက်ပါပဲ။
  4. $price ကို မထိပါဘူး။ ဒါက 1000 -> 1250 EUR ကို print ထုတ်ပါတယ်။
  5. (void) cast က "ဒါကို ရည်ရွယ်ပြီး လျစ်လျူရှုတာပါ" လို့ ပြောပြီး warning ကို ငြိမ်သွားစေပါတယ်။
  6. ဒီစာကြောင်းက ရလဒ်ကို သုံးဖို့ မေ့နေလို့ PHP က attribute ရဲ့ message ပါတဲ့ warning ကို print ထုတ်ပါတယ် — return value ကို "should either be used or intentionally ignored by casting it as (void), Money is immutable; use the returned copy"။

Error နဲ့ exception

Go နဲ့မတူဘဲ PHP က exception တွေကို သုံးလို့ ဒီအပိုင်းက အိမ်ပြန်ရောက်သလို ခံစားရပါမယ် — throw၊ try၊ catch၊ finally။ ကွာခြားချက် နှစ်ခု ရှိပါတယ် — class နဲ့ catch လုပ်ရပြီး PHP 8 ကစပြီး engine အမှားအားလုံးနီးပါး (type error၊ null ပေါ်က ခေါ်မှု၊ သုညနဲ့ စားခြင်း) က fatal error မဟုတ်တော့ဘဲ catch လုပ်လို့ရတဲ့ exception တွေ ဖြစ်ပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Custom error တွေက JS မှာ class NotFound extends Error လို Exception ဒါမှမဟုတ် သူ့ child တစ်ခုကို extend လုပ်တဲ့ class တွေပါ။ Body အလွတ်နဲ့ လုံလောက်ပါတယ်။
  2. throw က expression တစ်ခုဖြစ်လို့ ?? ရဲ့ ညာဘက်မှာ ထားနိုင်ပါတယ် — "user ကို ပေး၊ ဒါမှမဟုတ် throw လုပ်"။
  3. ဒီခေါ်မှုက throw လုပ်လို့ try block ရဲ့ ကျန်တဲ့အပိုင်းကို ကျော်သွားပါတယ်။
  4. catch က ကိုင်တွယ်မယ့် class ကို နာမည်တပ်ပြီး | နဲ့ အများကြီး ဖမ်းနိုင်ပါတယ်။ စာရင်းမှာ မပါတာတွေကတော့ stack အပေါ်ကို ဆက်တက်သွားပါတယ်။
  5. finally က JS အတိုင်း throw ဖြစ်ဖြစ်၊ မဖြစ်ဖြစ် run ပါတယ်။
  6. Function ဟောင်းအများအပြားက false ဒါမှမဟုတ် null ပြန်ပေးပြီး အမှားကို အချက်ပြပါတယ်။ JSON_THROW_ON_ERROR ကို ပေးလိုက်ရင် json_decode က JSON.parse လို throw လုပ်ပါတယ်။ ဒီမှာ message က "Syntax error" ပါ။

Namespace၊ autoloading နဲ့ Composer

PHP မှာ file ကို load လုပ်ပေးတဲ့ import မရှိပါဘူး။ အဲဒီအစား class တွေက namespace တွေထဲ နေပြီး Composer က class တစ်ခုကို ပထမဆုံး သုံးတဲ့အခါ သူ့ file ကို ရှာပေးတဲ့ autoloader ကို ထုတ်ပေးပါတယ်။ နာမည်တွေကို path တွေဆီ ချိတ်ပေးတဲ့ စည်းကမ်းကို PSR-4 လို့ ခေါ်ပါတယ် — App\Money\Money က src/Money/Money.php ထဲမှာ ရှိပါတယ်။

json
{    "name": "aye/shop",    "require": {        "php": "^8.5",        "nesbot/carbon": "^3.0"    },    "require-dev": {        "pestphp/pest": "^5.0"    },    "autoload": {        "psr-4": { "App\\": "src/" }    },    "scripts": {        "test": "pest"    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Package နာမည်က အမြဲ vendor/name ဖြစ်ပါတယ်၊ scoped npm package @aye/shop လိုပါပဲ။
  2. require က dependencies ပါ။ PHP version ကိုတောင် require လုပ်နိုင်ပါတယ် — ဒီထက်ဟောင်းရင် Composer က install မလုပ်ပေးပါဘူး။ Version range တွေက npm လို ^ caret စည်းကမ်းကိုပဲ သုံးပါတယ်။
  3. require-dev က devDependencies ပါ။ Pest က Jest ပုံစံ API ရှိတဲ့ testing framework ပါ။
  4. PSR-4 စည်းကမ်း — App\ နဲ့ စတဲ့ class မှန်သမျှကို src/ ကနေ load လုပ်ပါတယ်။ (JSON ထဲမှာ backslash ကို escape လုပ်ရလို့ နှစ်ခါ ရေးထားပါတယ်။)
  5. scripts က npm script တွေလို အလုပ်လုပ်ပါတယ် — composer test က Pest ကို run ပါတယ်။
php
<?php // src/Money/Money.phpdeclare(strict_types=1); namespace App\Money; final readonly class Money{    public function __construct(public int $amount) {}} // public/index.phprequire __DIR__ . '/../vendor/autoload.php'; use App\Money\Money;use function App\Support\format_money; echo format_money(new Money(1250));

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. namespace declaration က ဒီ file ရဲ့ class တွေ ဘယ် folder ပိုင်လဲ နာမည်ပေးပါတယ်။ Import path ထဲက / လို backslash တွေက အပိုင်းတွေကို ခွဲပါတယ်။
  2. Entry file က Composer ရဲ့ autoloader ကို တစ်ကြိမ်ပဲ include လုပ်ပါတယ်။ အဲဒီနောက် class တိုင်းက ပထမဆုံး သုံးချိန်မှာ ကိုယ့်ဘာသာ load ဖြစ်ပါတယ်၊ import graph မရှိ၊ bundler မလိုပါဘူး။
  3. use က နာမည်ကို alias ပဲ ပေးပါတယ်၊ ဒါကြောင့် \App\Money\Money အစား Money လို့ ရေးနိုင်ပါတယ်။ သူ့ဘာသာ ဘာမှ load မလုပ်ပါဘူး။
  4. Function တွေကို use function နဲ့ import လုပ်ပါတယ်။ ရိုးရိုး function တွေကို PSR-4 က autoload မလုပ်ပေးလို့ သူတို့ file တွေကို composer.json ထဲက autoload.files အောက်မှာ စာရင်းသွင်းပါ။
  5. တကယ့် project မှာ ဒီ file နှစ်ခုက သီးခြားစီ ဖြစ်ပါတယ်၊ ဒီမှာတော့ block တစ်ခုထဲ ဆံ့အောင် အတူပြထားတာပါ။ Laravel က ဒါတွေအားလုံးကို ချိတ်ဆက်ပေးထားပါတယ်။
bash
composer init                          # npm initcomposer require guzzlehttp/guzzle     # npm install axioscomposer require --dev pestphp/pest    # npm install -D vitestcomposer install                       # npm ci: install exactly what composer.lock sayscomposer update                        # npm updatecomposer dump-autoload                 # rebuild the class map after moving filescomposer audit                         # npm audit

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. မေးခွန်း အနည်းငယ် မေးပြီး composer.json ကို ရေးပေးပါတယ်။
  2. Package တစ်ခုကို require ထဲ ထည့်ပြီး vendor/ ထဲ install လုပ်ပါတယ်။ Guzzle က PHP code အများစု သုံးတဲ့ HTTP client ပါ (Laravel က Http::get() အဖြစ် ထုပ်ပေးထားပါတယ်)။
  3. composer.lock က သင့် package-lock.json ပါ။ Commit လုပ်ပြီး CI နဲ့ server တွေပေါ်မှာ composer install ကို run ပါ။
  4. PSR-4 နဲ့ဆိုရင် မလိုသလောက်ပါပဲ၊ ဒါပေမဲ့ namespace နာမည်ပြောင်းပြီးနောက် ဒါမှမဟုတ် autoload.files ထဲ ထည့်ပြီးနောက် အသုံးဝင်ပါတယ်။

PHP က သင့် code ကို ဘယ်လို run လဲ

Request တစ်ခုအောက်မှာ ဘာဖြစ်လဲ သိထားရင် PHP ရဲ့ ဒီဇိုင်းအများစုကို ရှင်းပြနိုင်ပြီး Laravel ဘာကြောင့် ဒီလို ခံစားရလဲ နားလည်ပါလိမ့်မယ်။

၁။ Engine: source ကနေ opcode ဆီ

Zend Engine က .php file ကို ဖတ်ပြီး abstract syntax tree အဖြစ် parse လုပ်ကာ ကျစ်လျစ်တဲ့ bytecode ဖြစ်တဲ့ opcode တွေအဖြစ် compile လုပ်ပြီး virtual machine က execute လုပ်ပါတယ်။ V8 က JS ကို Ignition အတွက် bytecode အဖြစ် ပြောင်းတာနဲ့ ပုံသဏ္ဌာန် တူတူပါပဲ။

၂။ OPcache နဲ့ JIT

Request တိုင်းမှာ file တိုင်းကို compile လုပ်ရင် အလဟဿ ဖြစ်မှာမို့ OPcache က compile ပြီးသား opcode တွေကို shared memory ထဲ သိမ်းထားပြီး နောက် request က တန်း run ပါတယ်။ PHP 8 က ဒီအပေါ်မှာ hot code ကို machine code အဖြစ် ပြောင်းပေးတဲ့ JIT compiler ကို ထပ်ထည့်ခဲ့ပါတယ်။ Image processing လို CPU အလုပ်များတဲ့ အလုပ်တွေမှာ အကူအညီ အများဆုံးဖြစ်ပြီး ပုံမှန် web request တွေကတော့ database ကို စောင့်ရင်းနဲ့ အချိန်ကုန်ပါတယ်။

၃။ PHP-FPM: request တစ်ခုကို worker တစ်ခု

Production မှာ Nginx လို web server က request တစ်ခုစီကို worker process အစုအဝေး ဖြစ်တဲ့ PHP-FPM ဆီ လွှဲပေးပါတယ်။ Worker တစ်ခုက တစ်ကြိမ်မှာ request တစ်ခုကို အစကနေ အဆုံးအထိ ကိုင်တွယ်ပြီး အဲဒီ request ဖန်တီးခဲ့တာ အားလုံးကို ပြန်လွှတ်ပေးပါတယ်။ Concurrency က event loop ကနေ မဟုတ်ဘဲ pool အရွယ်အစားကနေ လာပြီး worker တွေ အားနေသရွေ့ request နှေးတစ်ခုက တခြားဟာတွေကို ဘယ်တော့မှ မပိတ်ဆို့ပါဘူး။

Shared-nothing model ကို ဒီစာမျက်နှာပေါ်က runtime မှာတင် မြင်နိုင်ပါတယ် — Run တိုင်းက request အသစ်တစ်ခုပါ။ Run ကို နှစ်ခါ နှိပ်ကြည့်ပါ၊ counter က အကြိမ်တိုင်း သုညကနေ ပြန်စပါတယ်။

phpပြင်ပြီး Run ကို နှိပ်ကြည့်ပါ

Tab က indent · ⌘/Ctrl + Enter က run · Esc က editor ထဲက ထွက်

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. ရိုးရိုး function တစ်ခုပါ။ Node server မှာဆိုရင် module အဆင့်က counter တစ်ခုက request တစ်ခုကနေ နောက်တစ်ခုဆီ ဆက်တိုးနေမှာပါ — PHP က ဘာလုပ်လဲ ကြည့်ပါ။
  2. static variable က request တစ်ခုအတွင်းမှာ call တွေကြား သူ့ value ကို ထိန်းထားပါတယ်၊ closure က ဖမ်းထားတဲ့ variable လိုပေါ့။ PHP မှာ module state နဲ့ အနီးစပ်ဆုံး အရာပါ။
  3. 3 ကို print ထုတ်ပါတယ် — ဒီ request ထဲမှာ သုံးကြိမ် ခေါ်ထားလို့ပါ။ ပြန် Run ရင်လည်း 3 ပဲ ရပါတယ်၊ နောက် request က ဗလာ အခြေအနေကနေ စလို့ပါ။ Production မှာ PHP-FPM အောက်က request တိုင်းလည်း ဒီအတိုင်းပါပဲ။
  4. $GLOBALS က global variable အားလုံးကို ကိုင်ထားပြီး ??= က JS မှာလိုပဲ key မရှိမှ assign လုပ်ပါတယ်။ ဒီလို global "cache" က request တစ်ခုစာပဲ ခံလို့ တကယ့် cache တွေကို Redis၊ APCu ဒါမှမဟုတ် database မှာ ထားကြတာပါ။
  5. Request တစ်ခုစီမှာ ကိုယ်ပိုင် memory လည်း ရပြီး request ပြီးတာနဲ့ ပြန်လွှတ်ပါတယ်။ memory_get_peak_usage() က ဒီ script သုံးခဲ့တဲ့ အများဆုံး ပမာဏ (kilobyte ရာဂဏန်းလောက်) ကို ပြလို့ request တစ်ခုထဲက leak က နောက်တစ်ခုမှာ စုပုံမလာနိုင်ပါဘူး။

၄။ ကြာရှည် run တဲ့ worker တွေ: Octane နဲ့ FrankenPHP

Request တိုင်းမှာ framework ကို boot လုပ်ရတာ millisecond အနည်းငယ် ကုန်ပါတယ်။ Laravel Octane (FrankenPHP၊ Swoole ဒါမှမဟုတ် RoadRunner နဲ့) က သင့် app ကို တစ်ကြိမ် boot လုပ်ပြီး request တွေကြား memory ထဲ ထိန်းထားလို့ Node server နဲ့ အများကြီး ပိုနီးပြီး အများကြီး ပိုမြန်ပါတယ်။ အပေးအယူကလည်း Node ရဲ့ အပေးအယူပါပဲ — static state က request တွေကြား ကျန်နေလို့ ပေါက်ကြားမသွားအောင် ဂရုစိုက်ရပါတယ်။

၅။ Memory

PHP က တန်ဖိုးအများစုကို နောက်ဆုံး reference ပျောက်သွားတဲ့ အခိုက်မှာပဲ ပြန်လွှတ်ပါတယ် (reference counting)၊ cycle collector က တစ်ခုနဲ့တစ်ခု ညွှန်နေတဲ့ object တွေကို ရှင်းပေးပါတယ်။ ရိုးရိုး worker တွေကို request တစ်ခုစီပြီးတိုင်း လွှင့်ပစ်လို့ PHP မှာ memory leak က ပြဿနာ ခဏခဏ မဖြစ်ပါဘူး။ Octane၊ queue worker နဲ့ တခြား ကြာရှည် run တဲ့ script တွေမှာတော့ Node မှာလိုပဲ ပြဿနာ ဖြစ်နိုင်ပါတယ်။

Laravel 13 ကို ပုံတစ်ပုံတည်းနဲ့

Laravel ကို ၂၀၁၁ ခုနှစ်မှာ Taylor Otwell က ဖန်တီးခဲ့ပြီး နှစ်စဉ် major version တစ်ခု ထုတ်ပါတယ်။ Laravel 13 (၂၀၂၆ မတ်လ) က PHP 8.3 နဲ့အထက် လိုအပ်ပြီး bug fix တွေကို ၂၀၂၇ တတိယသုံးလပတ်အထိ၊ security fix တွေကို ၂၀၂၈ မတ်လအထိ ရပါတယ်။ Breaking change နည်းအောင် ရည်ရွယ်ပြီး ညင်ညင်သာသာ upgrade လုပ်ရတဲ့ version ဖြစ်ပြီး configuration အတွက် PHP attribute တွေ ပိုများလာတာ၊ JSON:API resource တွေ၊ first-party AI SDK၊ semantic (vector) search နဲ့ သေးငယ်တဲ့ တိုးတက်မှု အများအပြားကို ထည့်ထားပါတယ်။

အမြန်ဆုံး ဝင်ရောက်နည်းက သင်သုံးနေပြီးသား tool တွေပေါ် ချိတ်ကြည့်တာပါ—

  • Routing နဲ့ controller → Express ဒါမှမဟုတ် Next.js route handler။
  • Eloquent ORM နဲ့ migration → Prisma ဒါမှမဟုတ် Drizzle၊ ဒါပေမဲ့ Active Record ပုံစံ — model object က ကိုယ့်ကိုယ်ကို save လုပ်ပါတယ်။
  • Form Request နဲ့ validation rule → API ရဲ့ အစွန်းမှာ ထားတဲ့ Zod schema။
  • API Resource → JSON response ကို ပုံဖော်ဖို့ သုံးတဲ့ DTO ဒါမှမဟုတ် select။
  • Queue နဲ့ job → BullMQ၊ worker တွေကို php artisan queue:work နဲ့ စတင်ပါတယ်။
  • Sanctum၊ Fortify နဲ့ policy → sign-in အတွက် NextAuth ဒါမှမဟုတ် Lucia၊ နောက် ကိုယ်ပိုင် permission စစ်ဆေးမှုတွေ။
  • Blade၊ Livewire ဒါမှမဟုတ် Inertia → server-rendered template တွေ ဒါမှမဟုတ် သီးခြား API မလိုတဲ့ React၊ Vue ဒါမှမဟုတ် Svelte front end။
  • Pest → Vitest ဒါမှမဟုတ် Jest။ Artisan → သင့် package.json script တွေနဲ့ code generator တွေ။ Reverb → Socket.IO။

Project တစ်ခု ဖန်တီးခြင်း

bash
composer global require laravel/installerlaravel new blogcd blogphp artisan install:apicomposer run devphp artisan make:model Post -mfphp artisan route:list

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. npm i -g create-next-app လို laravel command ကို global install လုပ်ပါတယ်။ Herd မှာ ပါပြီးသားပါ။
  2. App ကို ဖန်တီးပြီး မေးခွန်း အနည်းငယ် မေးပါတယ် — starter kit (မရွေး၊ React၊ Vue၊ Svelte ဒါမှမဟုတ် Livewire၊ sign-in screen တွေ ဆောက်ပြီးသား)၊ database (default က SQLite မို့ setup လုပ်စရာ ဘာမှမလို) နဲ့ test framework (Pest ကို ရွေးပါ)။
  3. routes/api.php နဲ့ API token တွေအတွက် Sanctum ကို ထည့်ပေးပါတယ်။ အဲဒီ file ထဲက route တိုင်းကို /api အောက်မှာ serve လုပ်ပါတယ်။
  4. Development server၊ queue worker၊ live log viewer နဲ့ Vite ကို terminal တစ်ခုတည်းမှာ အတူ စတင်ပါတယ် — Laravel ရဲ့ npm run dev ပေါ့။ App က http://localhost:8000 မှာ ရှိပါတယ်။
  5. Artisan က Laravel ရဲ့ CLI ပါ။ make:* command တွေက file တွေ ထုတ်ပေးပါတယ် — ဒီမှာ model တစ်ခုနဲ့ သူ့ migration (-m) နဲ့ factory (-f)။
  6. Route တိုင်းကို method၊ URL၊ နာမည်နဲ့ handler တို့နဲ့တကွ ပြပါတယ်။ URL က မျှော်လင့်သလို မကိုက်တဲ့အခါ အသုံးဝင်ပါတယ်။

အများဆုံး ထိရမယ့် folder တွေနဲ့ သူတို့ရဲ့ JS တူညီချက်တွေ—

  • routes/ — page တွေအတွက် web.php၊ JSON အတွက် api.php၊ command နဲ့ schedule လုပ်ထားတဲ့ task တွေအတွက် console.php။
  • app/Models/ — Eloquent model တွေ၊ table တစ်ခုကို class တစ်ခု။
  • app/Http/Controllers/၊ Requests/၊ Resources/ — request ကိုင်တွယ်ခြင်း၊ validation နဲ့ response ပုံဖော်ခြင်း။
  • database/migrations/၊ factories/၊ seeders/ — schema ပြောင်းလဲမှု၊ test အတွက် data အတု၊ အစပိုင်း data။
  • resources/views/ နဲ့ resources/js/ — Blade template တွေ ဒါမှမဟုတ် Inertia သုံးတဲ့အခါ သင့် React/Vue code။
  • config/ နဲ့ .env — setting တွေ။ Code က config('app.name') ကို ဖတ်ပြီး config file တွေကပဲ env() ကို ဖတ်ပါတယ်။
  • bootstrap/app.php — route၊ middleware နဲ့ exception ကိုင်တွယ်ပုံကို register လုပ်တဲ့ နေရာတစ်ခုတည်း။
  • tests/ — Pest test တွေ၊ HTTP အဆင့် test တွေအတွက် Feature/ နဲ့ ရိုးရိုး class တွေအတွက် Unit/။

Request တိုင်းက လမ်းကြောင်း တူတူကို ဖြတ်ပါတယ် — public/index.php က app ကို boot လုပ်၊ router က URL ကို ကိုက်ညီအောင် ရှာ၊ request က middleware (auth၊ CSRF၊ rate limit) တွေကို ဖြတ်ပြီး controller run ကာ သူ ပြန်ပေးတာ မှန်သမျှ response ဖြစ်လာပါတယ်။ Array ဒါမှမဟုတ် model ကို ပြန်ပေးရင် Laravel က JSON ပို့ပါတယ်။ Express middleware နဲ့ handler လို ထင်ရရင် အဲဒီအတိုင်းပါပဲ။

ဒီ guide ရဲ့ ကျန်တဲ့အပိုင်းမှာ blog API အသေးလေးတစ်ခု ဆောက်ပါမယ် — publish လုပ်ပြီးသား post တွေကို စာရင်းပြတဲ့ public endpoint တွေနဲ့ ကိုယ်ပိုင် post တွေကို ရေး၊ ပြင်၊ ဖျက်ဖို့ login လိုတဲ့ endpoint တွေပါ။

Migration: code ထဲက schema

php
<?php use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema; return new class extends Migration{    public function up(): void    {        Schema::create('posts', function (Blueprint $table) {            $table->id();            $table->foreignId('user_id')->constrained()->cascadeOnDelete();            $table->string('title');            $table->string('slug')->unique();            $table->text('body');            $table->string('status')->default('draft');            $table->json('tags')->nullable();            $table->timestamp('published_at')->nullable();            $table->timestamps();        });    }     public function down(): void    {        Schema::dropIfExists('posts');    }};

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Migration file က anonymous class တစ်ခုကို ပြန်ပေးပါတယ်။ Disk ပေါ်က နာမည်မှာ timestamp ပါပြီး (2026_10_13_000000_create_posts_table.php) migration တွေ run မယ့် အစီအစဉ်ကို သတ်မှတ်ပါတယ်။
  2. up က ပြောင်းလဲမှုကို အသက်သွင်းပါတယ်။ php artisan migrate က မ run ရသေးတဲ့ migration မှန်သမျှကို run ပြီး migrations table ထဲ မှတ်တမ်းတင်ပါတယ်။
  3. id လို့ ခေါ်တဲ့ အလိုလိုတိုးတဲ့ big-integer primary key။
  4. Column နာမည်ကနေ ခန့်မှန်းပြီး users.id ဆီ foreign key ပါတဲ့ user_id column။ User ကို ဖျက်ရင် သူ့ post တွေပါ ပျက်ပါတယ်။
  5. Unique index ဖြစ်လို့ slug တူတဲ့ post နှစ်ခုကို database ကိုယ်တိုင် ငြင်းပယ်ပါတယ်။
  6. Status ကို ရိုးရိုးစာသားအဖြစ် သိမ်းပြီး model က PHP enum အဖြစ် ပြောင်းပေးပါမယ်။
  7. Eloquent က ဖြည့်ပေးတဲ့ created_at နဲ့ updated_at ကို ထည့်ပါတယ်၊ Prisma ရဲ့ @default(now()) နဲ့ @updatedAt လိုပါ။
  8. down က up ကို ပြန်ဖျက်ပေးလို့ php artisan migrate:rollback က နောက်ပြန်ဆုတ်နိုင်ပါတယ်။ migrate:fresh က အကုန်ဖျက်ပြီး အစကပြန်စလို့ development မှာ အဆင်ပြေပါတယ်။

Eloquent model

Model ဆိုတာ table တစ်ခုနဲ့ ချိတ်ထားတဲ့ class တစ်ခုပါ။ Laravel 13 က အများစုကို protected property တွေအစား class ပေါ်က attribute တွေနဲ့ configure လုပ်ခွင့်ပေးပါတယ်။

php
<?php namespace App\Enums; enum PostStatus: string{    case Draft = 'draft';    case Published = 'published';}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. အစောပိုင်းက backed enum idea အတိုင်းပဲ၊ app/Enums အောက်မှာ ကိုယ်ပိုင် file နဲ့ပါ။ Model က draft ဒါမှမဟုတ် published ကို သိမ်းပြီး enum case ကို ပြန်ပေးပါမယ်။
php
<?php namespace App\Models; use App\Enums\PostStatus;use Illuminate\Database\Eloquent\Attributes\Fillable;use Illuminate\Database\Eloquent\Attributes\Scope;use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;use Illuminate\Support\Str; #[Fillable(['title', 'body', 'status', 'tags', 'published_at'])]class Post extends Model{    use HasFactory;     protected $attributes = ['status' => 'draft'];     protected function casts(): array    {        return [            'status' => PostStatus::class,            'tags' => 'array',            'published_at' => 'datetime',        ];    }     protected static function booted(): void    {        static::creating(function (Post $post): void {            $post->slug ??= Str::slug($post->title).'-'.Str::lower(Str::random(6));        });         static::saving(function (Post $post): void {            if ($post->status === PostStatus::Published) {                $post->published_at ??= now();            }        });    }     public function user(): BelongsTo    {        return $this->belongsTo(User::class);    }     #[Scope]    protected function published(Builder $query): void    {        $query->where('status', PostStatus::Published)->whereNotNull('published_at');    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Mass assignment ကာကွယ်မှု။ Post::create($data) လို array တစ်ခုကနေ ဖြည့်တဲ့အခါ ဒီ column တွေကိုပဲ ဖြည့်နိုင်ပါတယ်။ user_id ဒါမှမဟုတ် id ကို ခိုးထည့်လာတဲ့ request က အဲဒါတွေကို overwrite မလုပ်နိုင်ပါဘူး။ Laravel 13 က ဒါကို #[Fillable] attribute နဲ့ ကြေညာခွင့်ပေးပါတယ် (အရင်က $fillable property ပါ)။
  2. Class နာမည်က table ကို ပေးပါတယ် — Post က posts ကို ဖတ်ရေးလုပ်ပါတယ်။ Sync လုပ်ထားရမယ့် schema file မရှိဘဲ column တွေက database ကနေ လာပါတယ်။
  3. Test နဲ့ seeder တွေအတွက် model ကို Post::factory() ပေးတဲ့ trait ပါ။
  4. Model အသစ်အတွက် default attribute တန်ဖိုးတွေ — database က ကိုယ်ပိုင် default ကို မဖြည့်ခင်ကတည်းက post က draft ဖြစ်နေအောင်ပါ။
  5. Cast တွေက column တွေကို ဖတ်ရေးတဲ့အချိန်မှာ ပြောင်းပေးပါတယ် — status က PostStatus enum၊ JSON tags column က PHP array နဲ့ published_at က Carbon date object (ပိုဖော်ရွေတဲ့ Date လို့ မြင်ပါ) ဖြစ်လာပါတယ်။
  6. Model event တွေ။ Post တစ်ခုကို ပထမဆုံး insert မလုပ်ခင် slug မပေးထားရင် (??= က null ဖြစ်မှသာ assign လုပ်) title ကနေ hello-world-k3x9qa လို slug တစ်ခု လုပ်ပေးပါတယ်။
  7. saving က insert နဲ့ update တိုင်းမတိုင်ခင် run ပါတယ်။ Post ကို published အဖြစ် ပထမဆုံး save လုပ်တဲ့အခါ live ဖြစ်တဲ့ အချိန်ကို တံဆိပ်ခတ်ပေးပါတယ်။
  8. Relationship တစ်ခုပါ — $post->user က စာရေးသူကို load လုပ်ပါတယ်။ Foreign key user_id ကို method နာမည်ကနေ ခန့်မှန်းပါတယ်။
  9. #[Scope] နဲ့ မှတ်ထားတဲ့ local scope က query တိုင်းကို ပြန်သုံးလို့ရတဲ့ filter တစ်ခု ထည့်ပေးပါတယ် — Post::published()->get()။ Chain လုပ်လို့ရတဲ့ နာမည်ပါ Prisma where လိုပါပဲ။
php
<?php namespace App\Models; use Database\Factories\UserFactory;use Illuminate\Database\Eloquent\Attributes\Fillable;use Illuminate\Database\Eloquent\Attributes\Hidden;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Relations\HasMany;use Illuminate\Foundation\Auth\User as Authenticatable;use Illuminate\Notifications\Notifiable;use Laravel\Sanctum\HasApiTokens; #[Fillable(['name', 'email', 'password'])]#[Hidden(['password', 'remember_token'])]class User extends Authenticatable{    /** @use HasFactory<UserFactory> */    use HasApiTokens, HasFactory, Notifiable;     protected function casts(): array    {        return [            'email_verified_at' => 'datetime',            'password' => 'hashed',        ];    }     public function posts(): HasMany    {        return $this->hasMany(Post::class);    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. #[Hidden] က ဒီ column တွေကို model ရဲ့ array နဲ့ JSON ပုံစံတိုင်းကနေ ဖယ်ထားလို့ password hash တစ်ခုက API response ထဲ မတော်တဆ ဘယ်တော့မှ ပေါက်ကြားမသွားပါဘူး။
  2. Class တစ်ခုက trait အများကြီး သုံးနိုင်ပါတယ်။ HasApiTokens က Sanctum ကနေ လာပြီး user ကို API token ဖန်တီးခွင့် ပေးပါတယ်။
  3. hashed cast က plain password ကို assign လုပ်လိုက်တဲ့ အခိုက်မှာပဲ hash လုပ်ပါတယ်၊ default က bcrypt ပါ။
  4. belongsTo ရဲ့ တစ်ဖက်ခြမ်းပါ — $user->posts က အဲဒီ user ရဲ့ post collection ဖြစ်ပြီး $user->posts() က ဆက်တည်ဆောက်လို့ရတဲ့ query ပါ။

Eloquent နဲ့ query လုပ်ခြင်း

ဒါတွေကို app တစ်ခုလုံး load လုပ်ထားတဲ့ Laravel ရဲ့ REPL ဖြစ်တဲ့ php artisan tinker ထဲမှာ စမ်းကြည့်ပါ။ စာကြောင်းတိုင်းမှာ သူ run မယ့် SQL ကို မှတ်ထားပါတယ်။

php
$user = User::factory()->create(['name' => 'Aye']); $post = $user->posts()->create([    'title' => 'Hello Laravel',    'body' => 'My first post.',    'tags' => ['php', 'laravel'],]); $post->update(['status' => PostStatus::Published]); $latest = Post::published()    ->with('user')    ->whereJsonContains('tags', 'php')    ->latest('published_at')    ->paginate(10); $titles = $user->posts()->pluck('title');$counts = User::withCount('posts')->orderByDesc('posts_count')->first(); $found = Post::where('slug', $post->slug)->firstOrFail();$found->delete();

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Factory က data အတုနဲ့ row တစ်ခု ဖန်တီးပြီး ပေးလိုက်တဲ့ field တွေကိုပဲ override လုပ်ပါတယ်။ INSERT INTO users …။
  2. Relationship ကနေ ဖန်တီးရင် user_id ကို ဖြည့်ပေးပါတယ်။ #[Fillable] column တွေကိုပဲ လက်ခံပြီး slug က creating event ကနေ လာကာ tags ကို JSON အဖြစ် သိမ်းပါတယ်။
  3. update က attribute တွေကို ပြောင်းပြီး တစ်ဆင့်တည်းနဲ့ save လုပ်ပါတယ်။ Model ရဲ့ saving event က published_at ကို တံဆိပ်ခတ်ပါတယ်။ UPDATE posts SET status = …, published_at = …။
  4. Model ထဲက scope — WHERE status = 'published' AND published_at IS NOT NULL။
  5. Eager loading။ with မပါရင် loop ထဲမှာ $post->user ကို ဖတ်တိုင်း post တစ်ခုကို query တစ်ခု run ပါတယ် ("N+1" ပြဿနာ)။ ပါရင်တော့ Laravel က စာရေးသူ အားလုံးကို query တစ်ခုတည်း ထပ်ပြီး load လုပ်ပါတယ်။
  6. JSON column ထဲမှာ query လုပ်ပြီး MySQL၊ PostgreSQL ဒါမှမဟုတ် SQLite အတွက် မှန်ကန်တဲ့ SQL အဖြစ် ဘာသာပြန်ပေးပါတယ်။
  7. paginate က query ကို LIMIT/OFFSET နဲ့ run၊ စုစုပေါင်းကို ရေတွက်ပြီး request ထဲက ?page= ကို ဖတ်ပါတယ်။ Controller ကနေ ပြန်ပေးရင် data၊ links နဲ့ meta ပါတဲ့ JSON ဖြစ်လာပါတယ်။
  8. pluck က column တစ်ခုကို Collection အဖြစ် ပြန်ပေးပါတယ် — chain လုပ်လို့ရတဲ့ map၊ filter၊ groupBy နဲ့ နောက်ထပ် တစ်ရာလောက် ပါတဲ့ Laravel ရဲ့ array wrapper ပါ၊ lodash နဲ့ အတော်တူပါတယ်။
  9. withCount က post တွေကို load မလုပ်ဘဲ subquery နဲ့ posts_count column ကို ထည့်ပေးပါတယ်။
  10. firstOrFail က ဘာမှ မကိုက်ရင် ModelNotFoundException ကို throw လုပ်ပြီး Laravel က အဲဒါကို 404 response အဖြစ် ပြောင်းပေးပါတယ်။

Form Request နဲ့ validation

Validation က ကိုယ်ပိုင် class ထဲမှာ နေလို့ controller တွေ တိုတိုလေး ရှိပါတယ်။ Input မအောင်ရင် Laravel က controller မ run ခင် request ကို ရပ်ပြီး 422 နဲ့ error စာရင်း JSON ကို ပြန်ပေးပါတယ်၊ throw လုပ်တဲ့ Zod safeParse နဲ့ အတော်တူပါတယ်။

php
<?php namespace App\Http\Requests; use App\Enums\PostStatus;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Validation\Rule; class StorePostRequest extends FormRequest{    public function rules(): array    {        return [            'title' => ['required', 'string', 'max:120'],            'body' => ['required', 'string'],            'status' => ['sometimes', Rule::enum(PostStatus::class)],            'tags' => ['sometimes', 'array', 'max:5'],            'tags.*' => ['string', 'distinct', 'max:20'],        ];    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. php artisan make:request StorePostRequest နဲ့ ထုတ်ထားတာပါ။ authorize() method မရှိရင် ခေါ်သူတိုင်းကို ခွင့်ပြုပါတယ်၊ permission တွေကို အောက်က policy က ကိုင်တွယ်ပါတယ်။
  2. Rule တွေကို ဘယ်ကနေ ညာ ဖတ်ပါ — ရှိရမယ်၊ အလွတ်မဖြစ်ရ၊ string ဖြစ်ရမယ်၊ အများဆုံး စာလုံး ၁၂၀။ မအောင်မြင်မှု တစ်ခုစီမှာ ဘာသာပြန်လို့ရတဲ့ အသင့် message ပါပါတယ်။
  3. sometimes က field ကို ပို့မှသာ စစ်ပါတယ်။ Rule::enum က enum ရဲ့ တန်ဖိုးတွေဖြစ်တဲ့ draft ဒါမှမဟုတ် published ကိုပဲ လက်ခံပါတယ်။
  4. tags.* က array ထဲက item တိုင်းကို သက်ဆိုင်ပါတယ် — string ဖြစ်ရမယ်၊ ထပ်နေလို့ မရ၊ တိုရမယ်။ Controller ထဲမှာ $request->validated() က ဒီမှာ နာမည်တပ်ထားတဲ့ field တွေကိုပဲ ပြန်ပေးပြီး client ပို့လာတဲ့ တခြားဘာကိုမှ ဘယ်တော့မှ မပေးပါဘူး။

API Resource: JSON ကို ပုံဖော်ခြင်း

php
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource{    public function toArray(Request $request): array    {        return [            'id' => $this->id,            'title' => $this->title,            'slug' => $this->slug,            'body' => $this->body,            'status' => $this->status,            'tags' => $this->tags ?? [],            'author' => $this->whenLoaded('user', fn () => $this->user->name),            'published_at' => $this->published_at?->toIso8601String(),        ];    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Resource ဆိုတာ ပြင်ပကမ္ဘာအတွက် model ရဲ့ type ပါတဲ့ မြင်ကွင်းတစ်ခုပါ။ Database column တွေ ပြောင်းသွားပေမဲ့ API က မပြောင်းဘဲ ရှိနေနိုင်ပါတယ်။ php artisan make:resource PostResource နဲ့ ထုတ်ထားတာပါ။
  2. $this က ထုပ်ထားတဲ့ model ကို ကိုယ်စားပြုလို့ $this->id က $post->id ကို ဖတ်ပါတယ်။
  3. Enum ကို JSON ထဲ သူ့တန်ဖိုးအဖြစ် ရေးလို့ client တွေက "draft" ဒါမှမဟုတ် "published" ကို မြင်ရပါတယ်။
  4. whenLoaded က controller က user ကို eager-load လုပ်ထားမှသာ စာရေးသူကို ထည့်ပေးလို့ resource တစ်ခုက မမျှော်လင့်တဲ့ query တွေကို ဘယ်တော့မှ မဖြစ်စေပါဘူး။
  5. Date တွေက JS ရဲ့ new Date() အတွက် အသင့်ဖြစ်တဲ့ ISO 8601 string တွေ ဖြစ်လာပါတယ်။ Nullsafe ?-> က date မရှိတဲ့ draft တွေကို ကိုင်တွယ်ပေးပါတယ်။

Client တွေက JSON:API standard ကို မျှော်လင့်ရင် Laravel 13 မှာ JSON:API format အပြည့် (type၊ id၊ attributes၊ relationships၊ sparse fieldset နဲ့ include) ကို ထုတ်ပေးတဲ့ JSON:API resource တွေလည်း ပါပါတယ်။ ဒီလို ရိုးရိုး JsonResource ကတော့ ပိုရိုးရှင်းတဲ့ default ပါ။

Controller၊ policy နဲ့ route

Policy တစ်ခုက model တစ်ခုနဲ့ ဘယ်သူ ဘာလုပ်ခွင့်ရှိလဲ ဆိုတဲ့ စည်းမျဉ်းတွေကို ထိန်းထားပါတယ်။ Laravel က Post အတွက် PostPolicy ကို နာမည်နဲ့ ရှာတွေ့လို့ register လုပ်စရာ ဘာမှ မလိုပါဘူး။

php
<?php namespace App\Policies; use App\Models\Post;use App\Models\User; class PostPolicy{    public function update(User $user, Post $post): bool    {        return $user->id === $post->user_id;    }     public function delete(User $user, Post $post): bool    {        return $user->id === $post->user_id;    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Method တစ်ခုစီက မေးခွန်းတစ်ခုကို ဖြေပါတယ် — sign-in ဝင်ထားတဲ့ ဒီ user က ဒီ post ကို update လုပ်ခွင့်ရှိလား? စာရေးသူပဲ လုပ်ခွင့်ရှိပါတယ်။ False ပြန်ရင် 403 ထွက်ပါတယ်။
php
<?php namespace App\Http\Controllers; use App\Enums\PostStatus;use App\Http\Requests\StorePostRequest;use App\Http\Resources\PostResource;use App\Jobs\NotifyFollowers;use App\Models\Post;use Illuminate\Http\JsonResponse;use Illuminate\Http\Resources\Json\AnonymousResourceCollection;use Illuminate\Http\Response;use Illuminate\Routing\Attributes\Controllers\Authorize; class PostController extends Controller{    public function index(): AnonymousResourceCollection    {        $posts = Post::published()->with('user')->latest('published_at')->paginate(10);         return PostResource::collection($posts);    }     public function show(Post $post): PostResource    {        abort_unless($post->status === PostStatus::Published, 404);         return new PostResource($post->load('user'));    }     public function store(StorePostRequest $request): JsonResponse    {        $post = $request->user()->posts()->create($request->validated());         if ($post->status === PostStatus::Published) {            NotifyFollowers::dispatch($post);        }         return (new PostResource($post))->response()->setStatusCode(201);    }     #[Authorize('update', 'post')]    public function update(StorePostRequest $request, Post $post): PostResource    {        $post->update($request->validated());         return new PostResource($post);    }     #[Authorize('delete', 'post')]    public function destroy(Post $post): Response    {        $post->delete();         return response()->noContent();    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Publish လုပ်ပြီးသား post တွေကို တစ်မျက်နှာ ဆယ်ခုနှုန်း စာရင်းပြပါတယ်။ Resource collection က paginator ရဲ့ links နဲ့ meta ကို JSON ထဲ ထိန်းထားပါတယ်။
  2. Route model binding။ Route မှာ {post} ပါပြီး parameter ကို Post $post လို့ type ပေးထားလို့ Laravel က post ကို id နဲ့ load လုပ်ပေးပြီး မရှိရင် 404 ပြန်ပါတယ်။ findOrFail boilerplate မလိုပါဘူး။
  3. Draft တွေကို အများပြည်သူ မမြင်ရပါဘူး — abort_unless က 404 ဖြစ်လာမယ့် HTTP exception ကို throw လုပ်ပါတယ်။
  4. Laravel က StorePostRequest type ကို မြင်ပြီး ဒီ method မ run ခင် validate လုပ်ပါတယ်။ အထဲမှာ data က မှန်ကန်ကြောင်း သိပြီးသားပါ။
  5. $request->user() က Sanctum token ကနေ ရတဲ့ sign-in ဝင်ထားတဲ့ user ပါ။ သူ့ posts() relationship ကနေ ဖန်တီးတာက စာရေးသူကို သတ်မှတ်ပေးလို့ client တစ်ခုက တခြားသူအဖြစ် ဘယ်တော့မှ post မတင်နိုင်ပါဘူး။
  6. Job တစ်ခုကို queue ပေါ် တင်ပြီး ချက်ချင်း ပြန်လာပါတယ်။ Worker တစ်ခုက background မှာ ယူလုပ်ပါတယ်။
  7. Resource က ပုံမှန်အားဖြင့် 200 ပြန်ပေမဲ့ အသစ်ဖန်တီးထားတဲ့ post က 201 Created ဖြစ်သင့်ပါတယ်။
  8. Laravel 13 ရဲ့ controller attribute တွေပါ — update မ run ခင် post route parameter နဲ့ policy ရဲ့ update method ကို ခေါ်ပါတယ်။ သူစိမ်းတစ်ယောက်က 403 ရပြီး ဒီ method ဘယ်တော့မှ မ run ပါဘူး။
  9. 204 No Content response ပါ၊ အောင်မြင်တဲ့ DELETE ရဲ့ ပုံမှန်အဖြေပါ။
php
<?php use App\Http\Controllers\PostController;use Illuminate\Http\Request;use Illuminate\Support\Facades\Route; Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum'); Route::apiResource('posts', PostController::class)->only(['index', 'show']); Route::middleware('auth:sanctum')->group(function () {    Route::apiResource('posts', PostController::class)->only(['store', 'update', 'destroy']);});

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. အရိုးရှင်းဆုံး route — URL တစ်ခုနဲ့ closure တစ်ခု။ သူ ပြန်ပေးတာ မှန်သမျှ response ဖြစ်ပြီး model တစ်ခုက (hidden column တွေ ဖယ်ပြီး) JSON ဖြစ်ပါတယ်။
  2. apiResource က standard REST route တွေကို register လုပ်ပြီး တစ်ခုစီကို controller method တစ်ခုဆီ ညွှန်ပါတယ် — GET /api/posts → index၊ GET /api/posts/{post} → show။ ဒီနှစ်ခုက public ပါ။
  3. Group ထဲက route တွေမှာ မှန်ကန်တဲ့ Sanctum token (Authorization: Bearer …) လိုပါတယ် — POST /api/posts → store၊ PUT/PATCH /api/posts/{post} → update၊ DELETE → destroy။ တခြားသူတွေက 401 ရပါတယ်။

Queue နဲ့ job

Email ပို့တာ၊ ပုံ resize လုပ်တာ၊ တခြား API ခေါ်တာလို နှေးတဲ့ ဘယ်အရာမဆို user ကို မစောင့်ခိုင်းသင့်ပါဘူး။ Job က အလုပ်ကို ဖော်ပြတဲ့ class ဖြစ်ပြီး queue က (database၊ Redis ဒါမှမဟုတ် SQS ထဲ) သိမ်းထားကာ worker process က run ပါတယ်။

php
<?php namespace App\Jobs; use App\Models\Post;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Queue\Queueable;use Illuminate\Queue\Attributes\Backoff;use Illuminate\Queue\Attributes\Tries;use Illuminate\Support\Facades\Log; #[Tries(3)]#[Backoff([10, 60])]class NotifyFollowers implements ShouldQueue{    use Queueable;     public function __construct(public Post $post) {}     public function handle(): void    {        Log::info('Notifying followers', ['post' => $this->post->id]);    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Laravel 13 က job တွေကို attribute တွေနဲ့ configure လုပ်ပါတယ် — failed အဖြစ် မမှတ်ခင် အများဆုံး သုံးကြိမ် ကြိုးစားပါ။
  2. ဒုတိယအကြိမ် မကြိုးစားခင် ၁၀ စက္ကန့်၊ တတိယအကြိမ် မတိုင်ခင် ၆၀ စက္ကန့် စောင့်ပါ။
  3. ShouldQueue က job ကို ချက်ချင်း မ run ဘဲ queue ဆီ ပို့ပေးတာပါ။ php artisan make:job NotifyFollowers နဲ့ ထုတ်ထားတာပါ။
  4. Model ရဲ့ id ကိုပဲ queue ပေါ် သိမ်းပါတယ်။ Worker က job ကို run တဲ့အခါ database ကနေ copy အသစ်ကို ပြန် load လုပ်ပါတယ်။
  5. handle က worker process ထဲမှာ အလုပ်လုပ်ပါတယ်။ သူ့ parameter တွေကို service container ကနေ ဖြည့်ပေးလို့ ဒီမှာ mailer ဒါမှမဟုတ် HTTP client တောင်းနိုင်ပါတယ်။

Job တွေကို process လုပ်ဖို့ php artisan queue:work ကို run ပါ (composer run dev က လုပ်ပေးပြီးသားပါ)။ Production မှာ worker ကို process manager နဲ့ အသက်ရှင်အောင် ထားရပြီး Redis queue တွေအတွက် dashboard ပါတဲ့ Laravel Horizon ကို သုံးနိုင်ပါတယ်။ Schedule လုပ်ထားတဲ့ task တွေ (သင့် cron job တွေ) ကို routes/console.php ထဲမှာ Schedule::job(new SendDigest)->dailyAt('08:00') လို ကြေညာပါတယ်။

Sanctum နဲ့ token

Sanctum က user တစ်ယောက်စီကို personal_access_tokens table ထဲ hash လုပ်ပြီး သိမ်းထားတဲ့ ရိုးရှင်းတဲ့ API token တွေ ပေးပါတယ်။ Login route တစ်ခုက password ကို စစ်ပြီး token တစ်ခု ပြန်ပေးပါတယ်—

php
Route::post('/tokens', function (Request $request) {    $request->validate([        'email' => ['required', 'email'],        'password' => ['required'],    ]);     $user = User::where('email', $request->email)->first();     if (! $user || ! Hash::check($request->password, $user->password)) {        throw ValidationException::withMessages(['email' => 'Wrong email or password.']);    }     $token = $user->createToken('cli', ['posts:write']);     return ['token' => $token->plainTextToken];});

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. $request->validate() က Form Request ရဲ့ inline ပုံစံဖြစ်ပြီး route အသေးလေးတွေအတွက် အဆင်ပြေပါတယ်။ (User၊ Hash နဲ့ ValidationException အတွက် use စာကြောင်းတွေကို ဒီမှာ ချန်ထားပါတယ်။)
  2. Hash::check က password ကို သိမ်းထားတဲ့ hash နဲ့ နှိုင်းယှဉ်ပါတယ်။ ValidationException ကို throw လုပ်ရင် email အောက်မှာ message ပါတဲ့ 422 ပြန်ပြီး တခြား validation error တွေနဲ့ ပုံသဏ္ဌာန် တူတူပါပဲ။
  3. နာမည်နဲ့ optional ability (scope) တွေ ပါတဲ့ token တစ်ခု ဖန်တီးပါတယ်၊ နောက်မှ $request->user()->tokenCan('posts:write') နဲ့ စစ်နိုင်ပါတယ်။
  4. Plain token ကို တစ်ကြိမ်တည်းပဲ ပြပြီး သူ့ hash ကိုပဲ သိမ်းပါတယ်။ Client က Authorization: Bearer <token> အဖြစ် ပို့ပါတယ်။

React SPA လို domain တူတူပေါ်က browser front end အတွက် Sanctum က token အစား လုံခြုံတဲ့ cookie session တွေကို သုံးနိုင်လို့ localStorage ထဲ ဘာမှ သိမ်းစရာ မလိုပါဘူး။ Starter kit တွေမှာ sign-in၊ registration၊ password reset နဲ့ email verification တွေ ဆောက်ပြီးသား ပါလာပါတယ်။

Pest နဲ့ testing

Pest က Jest ဒါမှမဟုတ် Vitest လို ဖတ်ရပါတယ်။ Feature test တွေက test တစ်ခုစီအတွက် in-memory SQLite database အသစ်နဲ့ app တစ်ခုလုံးကို ဖြတ်ပြီး HTTP request အတု ပို့ပါတယ်။

php
<?php use App\Enums\PostStatus;use App\Jobs\NotifyFollowers;use App\Models\Post;use App\Models\User;use Illuminate\Support\Facades\Queue;use Laravel\Sanctum\Sanctum; it('lists only published posts', function () {    Post::factory()->published()->count(2)->create();    Post::factory()->create();     $this->getJson('/api/posts')        ->assertOk()        ->assertJsonCount(2, 'data');}); it('creates a post and queues the notification', function () {    Queue::fake();    Sanctum::actingAs($user = User::factory()->create());     $this->postJson('/api/posts', [        'title' => 'Hello Laravel',        'body' => 'My first post.',        'status' => 'published',    ])        ->assertCreated()        ->assertJsonPath('data.status', 'published');     expect($user->posts()->first())        ->status->toBe(PostStatus::Published)        ->published_at->not->toBeNull();     Queue::assertPushed(NotifyFollowers::class);}); it('rejects invalid input', function () {    Sanctum::actingAs(User::factory()->create());     $this->postJson('/api/posts', ['title' => ''])        ->assertUnprocessable()        ->assertJsonValidationErrors(['title', 'body']);}); it('does not let strangers edit a post', function () {    $post = Post::factory()->create();    Sanctum::actingAs(User::factory()->create());     $this->patchJson("/api/posts/{$post->id}", ['title' => 'Mine now', 'body' => '…'])        ->assertForbidden();});

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. it('…', fn) က Vitest အတိုင်း test တစ်ခုပါ။ အားလုံးကို php artisan test ဒါမှမဟုတ် ./vendor/bin/pest နဲ့ run ပါ။
  2. State ပါတဲ့ factory တွေ — published post နှစ်ခုနဲ့ draft တစ်ခု။ published() state ကို အောက်က factory ထဲမှာ သတ်မှတ်ထားပါတယ်။
  3. getJson က တကယ့် server မလိုဘဲ app ကို ဖြတ်ပြီး request ပို့ကာ assert လုပ်လို့ရတဲ့ response ကို ပြန်ပေးပါတယ်။
  4. Queue::fake() က job တွေကို run မယ့်အစား မှတ်တမ်းတင်ပါတယ်၊ queue အတွက် vi.mock လိုပါ။
  5. Test ရဲ့ ကျန်တဲ့အပိုင်းအတွက် user အသစ်တစ်ယောက်အဖြစ် sign-in ဝင်ပြီး token မလိုပါဘူး။
  6. Pest ရဲ့ expect chain တွေ — ->status နဲ့ ->published_at တစ်ခုစီက post ရဲ့ အဲဒီ property ကို ဖတ်ပြီး စစ်ပါတယ်။
  7. Field နှစ်ခုလုံးအတွက် error ပါတဲ့ 422 — controller မ run ခင် Form Request က သူ့အလုပ် လုပ်ပြီးပါပြီ။
  8. #[Authorize] attribute နဲ့ policy က request ကို 403 နဲ့ ငြင်းပယ်ပါတယ်။
php
<?php namespace Database\Factories; use App\Enums\PostStatus;use App\Models\User;use Illuminate\Database\Eloquent\Factories\Factory; class PostFactory extends Factory{    public function definition(): array    {        return [            'user_id' => User::factory(),            'title' => fake()->sentence(4),            'body' => fake()->paragraphs(3, true),            'tags' => fake()->randomElements(['php', 'laravel', 'js', 'ts'], 2),        ];    }     public function published(): static    {        return $this->state(fn () => [            'status' => PostStatus::Published,            'published_at' => now()->subDay(),        ]);    }}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Post အတုအတွက် default attribute တွေ။ fake() က Faker library ပါ — နာမည်၊ စာကြောင်း၊ email နဲ့ အခြားများ။
  2. Factory ကို တန်ဖိုးအဖြစ် ပေးလိုက်ရင် ဆက်စပ်နေတဲ့ user ကို အရင်ဖန်တီးပြီး သူ့ id ကို သုံးပါတယ်။
  3. State ဆိုတာ နာမည်ပါတဲ့ ပြောင်းလဲမှုတစ်ခုပါ။ Test တွေက Post::factory()->published() ကို ခေါ်ပါတယ်။

Front end: Blade၊ Livewire ဒါမှမဟုတ် React နဲ့ Inertia

Laravel က Blade template တွေနဲ့ HTML ကို ကိုယ်တိုင် render လုပ်နိုင်သလို Livewire နဲ့ JS မရေးဘဲ interactivity ထည့်နိုင်ပြီး Inertia ကနေ React၊ Vue ဒါမှမဟုတ် Svelte front end ဆီ page တွေ လွှဲပေးနိုင်ပါတယ်။ JS developer တွေ များသောအားဖြင့် ရွေးတာက Inertia ပါ — controller က prop တွေ ပြန်ပေးပြီး React component က render လုပ်ပါတယ်။ သီးခြား API မရှိ၊ configure လုပ်ရမယ့် client-side router မရှိ၊ ပထမ load အတွက် fetch call တွေ မလိုပါဘူး။

php
// routes/web.phpuse App\Http\Resources\PostResource;use App\Models\Post;use Inertia\Inertia; Route::get('/', function () {    return Inertia::render('posts/index', [        'posts' => PostResource::collection(Post::published()->latest()->take(10)->get()),    ]);});

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. resources/js/pages/posts/index.tsx ကို render လုပ်ပြီး posts ကို prop အဖြစ် ပေးပါတယ်။ ပထမအကြိမ် ဝင်တဲ့အခါ Laravel က prop တွေ ထည့်ထားတဲ့ HTML ကို ပို့ပြီး နောက်ပိုင်း click တွေက JSON ကိုပဲ fetch လုပ်ပါတယ်။
tsx
// resources/js/pages/posts/index.tsximport { Link } from '@inertiajs/react'; type Post = { id: number; title: string; slug: string; author?: string }; export default function Index({ posts }: { posts: { data: Post[] } }) {  return (    <ul>      {posts.data.map((post) => (        <li key={post.id}>          <Link href={'/posts/' + post.id}>{post.title}</Link>        </li>      ))}    </ul>  );}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. TypeScript နဲ့ ရေးထားတဲ့ ပုံမှန် React component တစ်ခုပါ။ သူ့ prop တွေက controller ပေးလိုက်တာ အတိအကျပါ၊ resource collection က data အောက်မှာ ရောက်လာပါတယ်။
  2. Inertia ရဲ့ Link က Next.js Link လို page အပြည့် reload မလုပ်ဘဲ page တွေကို လဲပေးပါတယ်။ Starter kit တွေက URL ကို လက်နဲ့ မဆောက်ရအောင် type ပါတဲ့ route helper တွေ ထည့်ပေးပါတယ်။
blade
{{-- resources/views/posts/index.blade.php --}}<x-layout title="Journal">    @foreach ($posts as $post)        <article>            <h2>{{ $post->title }}</h2>            <p>{{ $post->user->name }} · {{ $post->published_at->diffForHumans() }}</p>        </article>    @endforeach</x-layout>

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Server ပေါ်မှာ HTML render လုပ်ချင်ရင် Laravel ရဲ့ template ဘာသာစကား Blade နဲ့ စာရင်း တူတူပါ။ {{-- --}} က comment ပါ။
  2. <x-layout> က component တစ်ခုပါ — resources/views/components ထဲက file တစ်ခုဖြစ်ပြီး JSX component လို prop နဲ့ children တွေနဲ့ သုံးပါတယ်။
  3. @foreach၊ @if စတာတွေက ရိုးရိုး PHP အဖြစ် compile လုပ်ပြီး cache လုပ်ထားတဲ့ directive တွေပါ။
  4. {{ }} က JSX လိုပဲ တန်ဖိုးကို HTML-escape လုပ်ပြီး print ထုတ်လို့ user input က markup ထိုးသွင်းလို့ မရပါဘူး။ diffForHumans() က "2 days ago" လို print ထုတ်ပါတယ်။

ကုမ္ပဏီကြီးတွေက TypeScript/JavaScript၊ Go နဲ့ PHP ကို ဘယ်လို တွဲသုံးလဲ

Product ကြီးတွေက ဘာသာစကား တစ်ခုတည်းနဲ့ run လေ့ မရှိပါဘူး။ ထပ်ခါထပ်ခါ တွေ့ရမယ့် ပုံစံက interface အတွက် TypeScript ဒါမှမဟုတ် JavaScript၊ အဓိက web application အတွက် PHP၊ ပြီးတော့ throughput မြင့်တဲ့ service တွေနဲ့ infrastructure အတွက် Go ပါ။ မှတ်တမ်းကောင်းကောင်း ရှိတဲ့ ဥပမာ တချို့—

  • Meta (Facebook)။ Facebook က PHP application တစ်ခုအဖြစ် စခဲ့ပါတယ်။ PHP ရဲ့ မြန်ဆန်တဲ့ ပြင်ပြီး reload လုပ်တဲ့ cycle ကို ထိန်းထားရင်း error တွေကို စောစော ဖမ်းနိုင်ဖို့ JIT-compile လုပ်တဲ့ virtual machine HHVM နဲ့ PHP ရဲ့ တဖြည်းဖြည်း type ထည့်လို့ရတဲ့ dialect Hack ကို ဆောက်ပြီး codebase တစ်ခုလုံးနီးပါးကို Hack ဆီ ရွှေ့ခဲ့ပါတယ်။ Interface ဘက်မှာ Meta က React ကို ဖန်တီးခဲ့ပါတယ်။ PHP codebase တစ်ခုကို အဆင့်ဆင့် type ထည့်တာက JS codebase ကို TypeScript ဆီ ရွှေ့တာနဲ့ ခရီးစဉ်တူပါပဲ။
  • Slack။ သူ့ core application နဲ့ API က HHVM ပေါ်မှာ run တဲ့ PHP/Hack ဖြစ်ပြီး အမြန်နှုန်းနဲ့ static type အတွက် ၂၀၁၆ ခုနှစ်မှာ ပြောင်းခဲ့တာပါ။ Real-time messaging အလွှာက Java နဲ့ Go နဲ့ ရေးထားတဲ့ service အစုဖြစ်ပြီး service အနည်းငယ်က Node ဖြစ်ကာ client တွေက desktop မှာ Electron နဲ့ JavaScript နဲ့ React ပါ။
  • Wikipedia။ Wikipedia စာမျက်နှာတိုင်းရဲ့ နောက်က software ဖြစ်တဲ့ MediaWiki က PHP ပါ။ ခေတ်သစ် wikitext parser Parsoid က Node.js service အဖြစ် စခဲ့ပြီး PHP ဆီ port လုပ်လိုက်တဲ့အခါ နှစ်ဆလောက် ပိုမြန်လာကာ အခု MediaWiki ထဲမှာ ပါလာပါတယ်။
  • Etsy။ Marketplace က PHP back end ကြီးတစ်ခုပေါ်မှာ run ပြီး JavaScript file သောင်းချီပါတဲ့ monorepo ဖြစ်တဲ့ သူ့ front end ကို ၂၀၂၁ ခုနှစ်မှာ TypeScript ဆီ ရွှေ့ခဲ့ပါတယ်။
  • WordPress။ PHP core က web ရဲ့ အစိတ်အပိုင်း အများကြီးကို run နေပြီး သူ့ block editor Gutenberg ကို React နဲ့ ဆောက်ထားကာ package တွေအတွက် TypeScript type တွေ ထုတ်ပေးထားပါတယ်။ Laravel developer တွေလည်း ဒီလို ခွဲခြားမှုကို တွေ့ရပါတယ် — server မှာ PHP၊ Inertia ကနေတစ်ဆင့် React ဒါမှမဟုတ် Vue front end။
  • Microsoft။ TypeScript 7 (၂၀၂၆) က JavaScript အခြေခံ compiler ကို Go နဲ့ ရေးထားတဲ့ native compiler နဲ့ အစားထိုးခဲ့ပြီး project ကြီးတွေမှာ ဆယ်ဆလောက် ပိုမြန်ပါတယ်။ Go ရဲ့ ဖွဲ့စည်းပုံနဲ့ garbage collection က ရှိပြီးသား code ကို စာကြောင်းအလိုက်နီးပါး port လုပ်နိုင်စေလို့ Go ကို ရွေးခဲ့တာပါ။
  • Uber နဲ့ cloud-native လောက။ Uber က back-end service တွေအတွက် ကမ္ဘာပေါ်က အကြီးဆုံး Go codebase တွေထဲက တစ်ခုကို run ပြီး PHP app အများစု အခု deploy လုပ်ကြတဲ့ infrastructure (Docker၊ Kubernetes နဲ့ Terraform) ကို Go နဲ့ ရေးထားပါတယ်။ JavaScript bundler esbuild ကလည်း Go ပါ။

JS/TS developer တစ်ယောက်အတွက် သင်ခန်းစာက ဒီဘာသာစကားတွေက ယှဉ်ပြိုင်တာထက် အတူတကွ အံဝင်ခွင်ကျ ဖြစ်တာပါ။ TypeScript က front end မှာ ဆက်ရှိနေပြီး (Inertia နဲ့ Laravel ထဲ တိုက်ရိုက် ချိတ်ပါတယ်) PHP နဲ့ Laravel က product ရဲ့ business logic ကို ထမ်းကာ Go က သေးငယ်ပြီး မြန်တဲ့ network service ဒါမှမဟုတ် infrastructure အစိတ်အပိုင်း လိုတဲ့နေရာမှာ ပေါ်လာပါတယ်။ သုံးခုလုံး သိထားရင် web team ဘယ်ဟာမှာမဆို အသုံးဝင်တဲ့သူ ဖြစ်ပါလိမ့်မယ်။

အားသာချက်နဲ့ အားနည်းချက်

PHP နဲ့ Laravel ထူးချွန်တဲ့ နေရာတွေ

  • ထုတ်လုပ်နိုင်စွမ်း။ Auth၊ queue၊ mail၊ file storage၊ caching၊ rate limiting၊ scheduling နဲ့ testing တွေ ပါပြီးသားဖြစ်ပြီး အတူတကွ အလုပ်လုပ်ပါတယ်။ Product ပေါ်မှာပဲ အချိန်ပေးရပါတယ်။
  • ရိုးရှင်းပြီး ခိုင်မာတဲ့ deployment။ Shared-nothing request တွေက scale လုပ်ရ လွယ်ပြီး ပျက်ဖို့ ခက်ပါတယ် — request ဆိုးတစ်ခုက process ကို မချနိုင်ပါဘူး။ Hosting က နေရာတိုင်းမှာ ရှိပြီး စျေးချိုကာ Laravel Cloud နဲ့ Forge က click တစ်ချက်တည်းနဲ့ ဖြစ်စေပါတယ်။
  • ရင့်ကျက်တဲ့ ecosystem။ Packagist မှာ package သိန်းနဲ့ချီ ရှိပြီး Laravel ရဲ့ first-party package တွေ (Horizon၊ Reverb၊ Scout၊ Cashier၊ Socialite၊ Pulse) က လိုအပ်ချက် အများစုကို ဖြည့်ပေးပါတယ်။
  • အလွန်ကောင်းတဲ့ documentation နဲ့ ညင်သာတဲ့ upgrade တွေပါတဲ့ တည်ငြိမ်တဲ့ နှစ်စဉ် release cycle။
  • ခေတ်မီ ဘာသာစကား။ Type၊ enum၊ readonly object၊ property hook၊ attribute နဲ့ pipe တွေက PHP 8.5 ကို ရေးရတာ သာယာစေပါတယ်။
  • Full-stack TypeScript လည်း ဆက်ဆံ့ပါတယ်။ Inertia နဲ့ UI အတွက် React နဲ့ TypeScript ကို ဆက်သုံးပြီး နောက်ကွယ်က အရာအားလုံးအတွက် Laravel ကို သုံးနိုင်ပါတယ်။

နာကျင်စေတဲ့ နေရာတွေ

  • မညီညွတ်တဲ့ standard library။ ဆယ်စုနှစ်ချီ သမိုင်းက ပေါ်နေပါတယ် — str_replace($search, $replace, $subject) ဖြစ်ပေမဲ့ strpos($haystack, $needle) ဖြစ်ပြီး array_map နဲ့ array_filter က argument တွေကို ပြောင်းပြန် အစီအစဉ်နဲ့ ယူပါတယ်။
  • Type စစ်တာ နောက်ကျတယ်။ Type တွေကို run time မှာ အတင်းကျင့်သုံးလို့ deploy မလုပ်ခင် TypeScript ပုံစံ ဘေးကင်းမှု ရဖို့ CI ထဲမှာ PHPStan ဒါမှမဟုတ် Larastan လိုပါတယ်။
  • ကြာရှည် connection တွေအတွက် မဆောက်ထားဘူး။ WebSocket နဲ့ streaming တွေမှာ သီးခြား server (Reverb) ဒါမှမဟုတ် ကြာရှည် run တဲ့ runtime (Octane) လိုပြီး Node ပုံစံ memory စိုးရိမ်မှုတွေ ပြန်ပါလာပါတယ်။
  • "မှော်ဆန်မှု"။ Facade၊ auto-discovery နဲ့ convention တွေက Laravel ကို ရေးရ မြန်စေပေမဲ့ အရာတွေ ဘယ်ကလာလဲ မသင်မချင်း ခြေရာခံရ ခက်စေပါတယ်။ သင့် IDE (Laravel extension နဲ့) နဲ့ php artisan route:list က ကူညီပါတယ်။
  • နာမည်ဂုဏ်သတင်း။ အချို့ team တွေက PHP ကို သူ့အတိတ်နဲ့ ဆက်ဆုံးဖြတ်နေတုန်းပါ။ Online မှာ တွေ့တဲ့ code ဥပမာတွေက ဆယ်နှစ် ခေတ်နောက်ကျနေနိုင်လို့ official doc တွေကို ဦးစားပေးပါ။

ဘာတွေ ဆောက်နိုင်လဲ

  • Billing (Stripe နဲ့ Paddle အတွက် Cashier)၊ team နဲ့ role တွေပါတဲ့ SaaS product နဲ့ dashboard တွေ။
  • ဒီ guide ထဲကအတိုင်း mobile app နဲ့ SPA တွေအတွက် JSON API တွေ။
  • Content site နဲ့ e-commerce — WordPress၊ WooCommerce၊ Drupal၊ Magento နဲ့ Statamic အားလုံး PHP ပါ။
  • Form၊ table နဲ့ dashboard တွေအတွက် Laravel package ဖြစ်တဲ့ Filament နဲ့ နာရီပိုင်းအတွင်း admin panel တွေ။
  • Reverb (WebSocket) နဲ့ front end မှာ Laravel Echo သုံးတဲ့ real-time app တွေ။
  • Laravel AI SDK နဲ့ AI feature တွေ — text generation၊ tool-calling agent၊ embedding နဲ့ vector search တွေကို provider မခွဲခြားတဲ့ API တစ်ခုတည်းနောက်မှာ။

သုညကနေ hero ဖြစ်ဖို့ လမ်းပြမြေပုံ

  1. ပထမအပတ်: ဘာသာစကား။ PHP 8.5 ကို install လုပ်ပြီး အပေါ်က ဥပမာတိုင်းကို လုပ်ကြည့်ပါ။ ကိုယ့် JS utility အသေးလေးတစ်ခုကို strict_types နဲ့ PHP အဖြစ် ပြန်ရေးကြည့်ပါ။
  2. ဒုတိယအပတ်: Composer နဲ့ tool တွေ။ PSR-4 autoloading၊ Pest test တွေနဲ့ PHPStan ပါတဲ့ package အသေးလေးတစ်ခု စလုပ်ပါ။ Laravel Pint နဲ့ format လုပ်ပါ။
  3. တတိယအပတ်: Laravel အခြေခံ။ laravel new၊ route၊ controller၊ Blade၊ migration နဲ့ Eloquent။ Livewire ဒါမှမဟုတ် React starter kit နဲ့ to-do app တစ်ခု ဆောက်ပါ။
  4. စတုတ္ထအပတ်: ဒီ guide ထဲက API။ Form Request၊ resource၊ policy၊ Sanctum နဲ့ Pest suite အပြည့်။ Endpoint တိုင်းကို feature test နဲ့ ဖုံးအုပ်ဖို့ ရည်မှန်းပါ။
  5. ဒုတိယလ: production ကျွမ်းကျင်မှု။ Queue နဲ့ Horizon၊ Redis နဲ့ caching၊ scheduler၊ S3 ပေါ်က file storage၊ mail၊ rate limiting နဲ့ logging။ Laravel Cloud၊ Forge ဒါမှမဟုတ် VPS ပေါ် deploy လုပ်ပါ။
  6. တတိယလ: နက်နက်ရှိုင်းရှိုင်း။ Service container နဲ့ service provider၊ event နဲ့ listener၊ Octane၊ Reverb နဲ့ doc တွေ ကုန်သွားတဲ့အခါ framework source ကို ဖတ်ခြင်း။ ကိုယ်သုံးနေတဲ့ package တစ်ခုကို fix တစ်ခု contribute လုပ်ပါ။
  7. ဆက်သွားပါ — နှစ်စဉ် release note တွေကို လိုက်ဖတ်၊ Laravel News ကို ဖတ်ပြီး codebase ကြီးတွေ ဘယ်လို စီစဉ်ထားလဲ သိဖို့ Pixelfed နဲ့ Coolify လို နာမည်ကြီး open-source Laravel app တွေကို လေ့လာပါ။

PHP ဆိုတာ အလုပ်ချိန်ရောက်တိုင်း ရောက်လာတတ်တဲ့ ဘာသာစကားပါ။ သူ့ရဲ့ ခေတ်မီပုံစံကို သင်ယူပြီး Laravel နဲ့ တွဲလိုက်ရင် တခြားသူတွေ stack ရွေးနေတုန်းမှာပဲ product တစ်ခုလုံးကို ship လုပ်နိုင်ပါပြီ။

— Field note

Filed underphplaraveltypescriptjavascriptgobackend

Written byZin Min Htun (Wine Chanz)Senior Full-Stack Developer · Da Nang, VietnamSay hello at campMore entries