সরাসরি পর্বে যান
পর্ব ৭০ · ৯৩-এর মধ্যে · মডিউল ১০

Laravel — Eloquent, Blade আর PHP-র আধুনিক পথ

Laravel — Eloquent ORM, Blade Templates and Modern PHP

Taught in Bangla, this lesson shows modern PHP and the Laravel framework: Composer, artisan and project structure, routes and controllers for the Bazarghar products API, Eloquent as Active Record with migrations and seeders, relations and fixing N+1 with eager loading, mass assignment and validation with FormRequest, Blade escaping and the risk of raw output, auth, Sanctum, queues, scheduler, Inertia, Livewire and tests, and deploying on shared hosting versus a VPS.

এই পর্বে যা শিখবেন

  • Laravel-এর কোড পড়ার মতো আধুনিক PHP, আর Composer ও artisan-এর কাজ
  • Route, controller, Eloquent model, migration আর seeder দিয়ে বাজারঘরের পণ্যের API আর পাতা বানাতে
  • N+1 মেপে with() দিয়ে সারাতে, আর mass assignment, validation ও Blade escaping ঠিকভাবে ব্যবহার করতে
  • Auth, Sanctum, queue, scheduler, Inertia আর Livewire কোথায় বসে, তা চিনতে
  • Shared hosting আর VPS-এ Laravel চালানোর পার্থক্য বুঝে কখন Laravel বেছে নেবেন তা ঠিক করতে

১ · PHP-র জায়গা, পুরোনো বদনাম আর Laravel-এর জন্ম

এ পর্যন্ত বাজারঘর বানিয়েছেন React, Express আর Next.js দিয়ে, সব JavaScript আর TypeScript-এ। এবার একই দোকানের একটি অংশ (পণ্যের তালিকা, একটি পণ্য, নতুন পণ্য যোগ করা, আর বিভাগের সম্পর্ক) বানাবেন Laravel দিয়ে। Laravel লেখা PHP ভাষায়। লক্ষ্য একটাই: একই কাজ অন্য ভাষার একটি পরিণত framework কীভাবে করে, তা পাশাপাশি দেখা।

PHP ওয়েবের সবচেয়ে পুরোনো server-side ভাষাগুলোর একটি। WordPress-এর মতো বহুল ব্যবহৃত CMS PHP-তে লেখা, আর পুরোনো-নতুন অসংখ্য সাইট এখনো PHP-তে চলে। বাংলাদেশে এর একটি বাস্তব কারণ আছে: স্থানীয় hosting কোম্পানির সাধারণ shared hosting প্যাকেজে সাধারণত cPanel, PHP আর MySQL আগে থেকেই থাকে। তাই অনেক ছোট ব্যবসা, এজেন্সি আর freelancer PHP দিয়ে শুরু করেন বলে দেখা যায়। কত শতাংশ, তা আমরা বলছি না, কারণ নির্ভরযোগ্য স্থানীয় হিসাব আমাদের হাতে নেই।

পুরোনো PHP ছিল পাড়ার একটি খোলা রান্নাঘরের মতো: যে কেউ ঢুকে যেকোনো হাঁড়িতে যা খুশি দিতে পারত, আর মাঝে মাঝে খাবারে কী পড়েছে কেউ জানত না। আধুনিক PHP আর Laravel একই রান্নাঘরকে একটি গোছানো রেস্তোরাঁর রান্নাঘর বানিয়েছে: কোন মশলা কোন তাকে (type), কে কোন চুলায় (route আর controller), আর কাঁচামাল ঢোকার আগে যাচাই (validation). চুলা একই, নিয়ম বদলেছে।

পুরোনো বদনাম আর আধুনিক PHP

PHP-র বদনামের পেছনে সত্যি কারণ ছিল। পুরোনো সংস্করণে register_globals-এর মতো সেটিং URL-এর মানকে সরাসরি variable বানিয়ে দিত। অনেক টিউটোরিয়ালে ইনপুট সরাসরি SQL-এর লেখায় জোড়া হতো, ফলে SQL injection ছিল সাধারণ ঘটনা। HTML, SQL আর logic এক ফাইলে মিশে থাকত। এই ছবিটাই অনেকের মনে রয়ে গেছে।

কিন্তু ভাষাটি অনেক বদলেছে। PHP 7 (২০১৫ সালের শেষে) গতিতে বড় লাফ দিয়েছিল বলে ব্যাপকভাবে জানা যায়। PHP 8 (২০২০) এনেছে named argument, attribute, match আর JIT. ঝুঁকিপূর্ণ পুরোনো সেটিংগুলো বহু আগেই সরানো হয়েছে। আর Composer (২০১২ সালের দিকে, Nils Adermann আর Jordi Boggiano-র শুরু করা) PHP-তে npm-এর মতো package ব্যবস্থাপনা এনেছে। লেখার সময়ে PHP-র সর্বশেষ ধারা 8.5. এই পর্বের সব run হয়েছে PHP 8.5.12-এ।

PHP শুরু করেন Rasmus Lerdorf, ১৯৯৪–৯৫ সালে, নিজের ওয়েব পাতার জন্য ছোট কিছু tool হিসেবে। Laravel-এর শুরু Taylor Otwell-এর হাতে, ২০১১ সালে। প্রচলিত বর্ণনা অনুযায়ী তিনি তখনকার জনপ্রিয় CodeIgniter-এ built-in login-এর মতো কিছু জিনিসের অভাব দেখে নিজের framework লেখেন। প্রথম সংস্করণ ২০১১ সালের মাঝামাঝি আসে। এখন প্রতি বছর একটি বড় সংস্করণ আসে। Laravel 13 প্রকাশিত হয় ১৭ মার্চ ২০২৬-এ। Docs অনুযায়ী এটি bug fix পাবে ২০২৭-এর তৃতীয় প্রান্তিক পর্যন্ত, আর security fix ১৭ মার্চ ২০২৮ পর্যন্ত।

PHP পড়ার জন্য যতটুকু দরকার

সাইটে আলাদা PHP কোর্স নেই, তাই শুধু এই পর্বের কোড পড়ার মতো কয়েকটি জিনিস। ভাষার বাকিটা JavaScript আর TypeScript-এর (পর্ব ০৬) সাথে মিলিয়ে নিলেই হবে।

<?php
$price = 420;                       // variable-এর আগে সবসময় $
$product->name;                     // object-এর property:  JS-এর product.name
Product::create([...]);             // class-এর static method:  Product.create(...)
$row = ['name' => 'Tea', 'price' => 100];   // array, key => value (JS object-এর মতো)
echo "দাম {$price} টাকা" . PHP_EOL; // "..." ভেতরে variable; . দিয়ে string জোড়া

function total(int $qty, ?int $discount = null): int { ... }  // type, ?int মানে null-ও চলবে
$cheap = array_filter($items, fn ($p) => $p->price < 200);   // arrow function: fn

#[Fillable(['name', 'email', 'password'])]   // attribute: class-এর ওপর ঘোষণা (TS-এর decorator-এর মতো দেখতে)
class User extends Authenticatable { }

Queue::route(ProcessPodcast::class, connection: 'redis');     // named argument
public function __construct(public int $orderId) {}           // constructor-এ সরাসরি property

শেষ চারটি লাইন আধুনিক PHP-র চিহ্ন। #[Fillable(...)] লাইনটি বানানো নয়: Laravel 13-এর নতুন project-এর User model ঠিক এভাবেই শুরু হয়। namespace App\Models; আর use হলো JavaScript-এর module আর import-এর কাছাকাছি ধারণা। আর <?php দিয়ে ফাইল শুরু হয়।

২ · Composer, artisan আর একটি Laravel project-এর গঠন

Node.js-এ যেমন npm, PHP-তে তেমন Composer. Package আসে Packagist থেকে, তালিকা থাকে composer.json-এ, আর ঠিক কোন version বসল তা থাকে composer.lock-এ (lockfile). নতুন project বানানো একটি command:

সত্যিকারের run · Composer 2.11.1 · PHP 8.5.12 · লম্বা তালিকা ছাঁটা

$ composer create-project laravel/laravel bazarghar
Creating a "laravel/laravel" project at "./bazarghar"
Installing laravel/laravel (v13.11.0)
  - Downloading laravel/laravel (v13.11.0)
  - Installing laravel/laravel (v13.11.0): Extracting archive
Created project in ~/bazarghar
…
Lock file operations: 110 installs, 0 updates, 0 removals
…
  - Locking laravel/framework (v13.39.0)
…
  - Locking phpunit/phpunit (12.5.42)
…
$ php artisan --version
Laravel Framework 13.39.0

লক্ষ করুন দুটি আলাদা version. laravel/laravel (v13.11.0) হলো শুরুর কাঠামো, মানে ফোল্ডার আর কয়েকটি ফাইল। আসল framework হলো laravel/framework (v13.39.0), যা vendor/-এ বসে আর Composer দিয়ে হালনাগাদ হয়। এক project-এ ১১০টি package এসেছে, তবে সবই PHP-র, কোনো node_modules নয়। Frontend asset-এর জন্য Vite আলাদাভাবে npm দিয়ে চলে।

artisan: project-এর নিজস্ব সহকারী

Artisan হলো Laravel-এর command-line tool, project-এর ভেতরে php artisan দিয়ে চলে। কাজ দুই রকম: ফাইল বানানো (make:*) আর অ্যাপ চালানো বা দেখা (migrate, route:list, test, queue:work). বাজারঘরের জন্য:

সত্যিকারের run · Laravel 13.39.0 · path ছাঁটা

$ php artisan make:model Category -m
 INFO Model [app/Models/Category.php] created successfully.
 INFO Migration [database/migrations/2026_10_31_200349_create_categories_table.php] created successfully.

$ php artisan make:model Product -mfs
 INFO Model [app/Models/Product.php] created successfully.
 INFO Factory [database/factories/ProductFactory.php] created successfully.
 INFO Migration [database/migrations/2026_10_31_200350_create_products_table.php] created successfully.
 INFO Seeder [database/seeders/ProductSeeder.php] created successfully.

$ php artisan make:controller ProductController --resource
$ php artisan make:request StoreProductRequest
$ php artisan make:test ProductApiTest
$ php artisan install:api
 INFO API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model.

-mfs মানে migration, factory আর seeder একসাথে। এটি Laravel-এর convention-এর শক্তি: Product model-এর table হবে products, foreign key category_id, আর এগুলো আলাদা করে বলতে হয় না। install:api নতুন project-এ routes/api.php ফাইল বানায়, আর Sanctum-এর কথা মনে করিয়ে দেয় (৮ নম্বর অংশে আসছে)। ফোল্ডারগুলোর মানচিত্র:

ফোল্ডার বা ফাইলকী থাকেExpress প্রকল্পে কাছাকাছি
routes/web.php, routes/api.phpURL থেকে কোন controllerapp.get(...), router
app/Http/ControllersRequest সামলানোর classroute handler বা controller
app/ModelsEloquent modelPrisma model ও client
database/migrations, seedersSchema-র ধাপ আর শুরুর ডেটাprisma/migrations, seed script
resources/viewsBlade templateReact component (অন্য জগৎ)
public/index.phpসব request-এর একমাত্র প্রবেশপথserver.js
.envDatabase, APP_KEY, APP_DEBUG.env

Laravel-এ একটি request-এর পথ

Every request enters through public/index.php and leaves as HTML (Blade) or JSON

Browser থেকে public/index.php, middleware, router, controller, Eloquent হয়ে Blade বা JSON পর্যন্ত Laravel-এর request-এর পথ Browser বা curl · GET /products public/index.php (একমাত্র দরজা) Middleware-এর স্তর CORS, trim, session, CSRF … Router routes/web.php · routes/api.php Controller + FormRequest ProductController@page Eloquent → SQL Product::with('category') Database SQLite / MySQL Blade → HTML · বা · JSON
পথটি পর্ব ০২-এর request-যাত্রার সাথে মিলিয়ে দেখুন। পার্থক্য শুধু নামে: Express-এর app.use এখানে middleware, app.get এখানে route, আর Prisma-র জায়গায় Eloquent.

৩ · Route আর controller: বাজারঘরের পণ্যের API

Laravel-এ দুটি route ফাইল। routes/web.php-এর route পায় web middleware group: cookie, session আর CSRF যাচাই, কারণ এগুলো browser-এর পাতা। routes/api.php-এর route-এর আগে নিজে থেকে /api বসে, আর সেখানে session থাকে না, কারণ API সাধারণত token দিয়ে চলে। বাজারঘরের দুটি ফাইল:

// routes/web.php
Route::get('/products', [ProductController::class, 'page']);   // Blade পাতা

// routes/api.php
Route::apiResource('products', ProductController::class)
    ->only(['index', 'store', 'show']);                          // তিনটি JSON route

সত্যিকারের run · php artisan route:list --path=products

 GET|HEAD api/products .. products.index › ProductController@index
 POST api/products .. products.store › ProductController@store
 GET|HEAD api/products/{product} .. products.show › ProductController@show
 GET|HEAD products .. ProductController@page

 Showing [4] routes

একটি লাইনে তিনটি REST route, নামসহ। পর্ব ২৫-এ Express-এ এগুলো হাতে লিখেছিলেন। এবার controller:

class ProductController extends Controller
{
    public function index()
    {
        return Product::with('category')->inStock()->orderBy('price')->paginate(3);
    }

    public function store(StoreProductRequest $request)
    {
        $product = Product::create($request->validated());
        return response()->json($product, 201);
    }

    public function show(Product $product)          // route model binding
    {
        return $product->load('category');
    }

    public function page()
    {
        return view('products', [
            'products' => Product::with('category')->inStock()->orderBy('price')->get(),
        ]);
    }
}

তিনটি জিনিস Express-এ আপনাকে নিজে করতে হতো, এখানে framework করে দিচ্ছে। প্রথম, controller একটি model বা array ফেরত দিলেই Laravel সেটিকে JSON বানায়, আর paginate(3) নিজেই current_page, data, পরের পাতার link যোগ করে (পর্ব ৩৪)। দ্বিতীয়, show(Product $product): URL-এর {product} দেখে Laravel নিজেই সেই id-র পণ্য খুঁজে আনে। একে বলে route model binding. না পেলে 404. তৃতীয়, store-এ StoreProductRequest লেখামাত্র validation হয়ে যায়, controller-এর ভেতরে পৌঁছানোর আগেই।

সত্যিকারের run · local server, 127.0.0.1-এ · দ্বিতীয়টিতে stock 0-এর পণ্য বাদ, দাম অনুযায়ী সাজানো, পাতায় ৩টি · লম্বা অংশ ছাঁটা

$ curl -s /api/products/1
{"id":1,"category_id":1,"name":"Miniket Rice 5kg","price":420,"stock":30,"created_at":"2026-10-31T20:04:40.000000Z","updated_at":"2026-10-31T20:04:40.000000Z","category":{"id":1,"name":"Grocery","created_at":"2026-10-31T20:04:40.000000Z","updated_at":"2026-10-31T20:04:40.000000Z"}}

$ curl -s /api/products
{
    "current_page": 1,
    "data": [
        {
            "id": 5,
            "category_id": 3,
            "name": "Turmeric 200g",
            "price": 95,
            "stock": 25,
            …
            "category": {
                "id": 3,
                "name": "Spices",
                …
            }
        },
        {
            "id": 6,
            "category_id": 3,
            "name": "Red Chilli 200g",
            "price": 110,
            …
        },
        {
            "id": 4,
            "category_id": 2,
            "name": "Green Tea 100g",
            "price": 180,
            …

404, আর একটি বিপজ্জনক ডিফল্ট

নেই এমন পণ্য চাইলে route model binding 404 দেয়। কিন্তু আমাদের .env-এ ছিল APP_DEBUG=true, যা নতুন project-এ local কাজের জন্য স্বাভাবিক। তখন উত্তরটি দেখুন:

সত্যিকারের run · APP_DEBUG=true · path ছাঁটা, ৩০টির বেশি trace লাইন বাদ

$ curl -s -i /api/products/999 -H 'Accept: application/json'
HTTP/1.1 404 Not Found
X-Powered-By: PHP/8.5.12
Content-Type: application/json

{
    "message": "No query results for model [App\\Models\\Product] 999",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
    "file": "~/bazarghar/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
    "line": 773,
    "trace": [ … server-এর পুরো ফাইল-পথ, middleware-এর নাম আর লাইন নম্বর … ]
}

Production-এ APP_DEBUG=false বাধ্যতামূলক। Debug চালু থাকলে যেকোনো error-এ server-এর ফাইল-পথ, ব্যবহৃত package আর কোডের লাইন বাইরে চলে যায়, আর বড় error page-এ পরিবেশের তথ্যও দেখা যেতে পারে। আক্রমণকারীর কাছে এটি একটি মানচিত্র। false হলে একই 404-এ শুধু একটি ছোট বার্তা যায়। পর্ব ২৮-এর (Error handling) “নিরাপদ বার্তা”-র নিয়ম এখানেও খাটে। আরেকটি ছোট ব্যাপার: X-Powered-By: PHP/8.5.12 header-ও version জানিয়ে দেয়, সেটি php.ini-তে expose_php = Off দিয়ে বন্ধ করা যায়।

নিচের stepper-এ GET /products (Blade পাতা) request-টি ধাপে ধাপে দেখুন। Global middleware-এর নামগুলো উপরের আসল trace থেকে নেওয়া।

  1. index.phpKernelঅ্যাপ চালু
  2. Middlewareglobal + webপরপর স্তর
  3. Routerpage()route মিলল
  4. Eloquent২টি queryপণ্য + বিভাগ
  5. BladeHTML200 OK
public/index.phpধাপ ১
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
Apache বা nginx সব URL এই একটি ফাইলে পাঠায়
Middlewareধাপ ২
ValidatePathEncoding → InvokeDeferredCallbacks
→ TrustProxies → HandleCors
→ PreventRequestsDuringMaintenance → ValidatePostSize
→ TrimStrings → ConvertEmptyStringsToNull
তারপর web group: cookie, session, CSRF …
Routerধাপ ৩
GET /products  →  ProductController@page
routes/web.php-এর ৪টির মধ্যে মিলল
Eloquentধাপ ৪
select * from "products" where "stock" > ? order by "price" asc
select * from "categories" where "categories"."id" in (1, 2, 3)
stock 0-এর Soybean Oil বাদ, ফল ৬টি পণ্য
Blade → HTMLধাপ ৫
<h1>বাজারঘর</h1>
<ul>
    <li>Turmeric 200g (Spices): 95 টাকা</li>
    …
    <li>Miniket Rice 5kg (Grocery): 420 টাকা</li>
</ul>
  1. এক দরজা। Web server প্রতিটি URL public/index.php-এ পাঠায়। সেটি অ্যাপ তৈরি করে request-টি framework-এর হাতে দেয়।
  2. Middleware-এর স্তর। আসল trace-এ global middleware ছিল এই ক্রমে: path যাচাই, পরে চালানোর কাজের ব্যবস্থা, proxy, CORS, maintenance, post size, তারপর লেখার দুই প্রান্তের ফাঁকা অংশ ছাঁটা আর ফাঁকা string-কে null বানানো। Web route হলে এরপর session আর CSRF. ধারণাটি পর্ব ২৬-এর পাইপলাইনের মতোই।
  3. Route মিলল। GET /products গেল ProductController-এর page() method-এ।
  4. দুটি query. with('category') থাকায় পণ্যের জন্য একটি আর সব বিভাগের জন্য একটি query. inStock() scope শর্ত যোগ করেছে stock > 0, তাই ৭টির মধ্যে ৬টি পণ্য এলো (তখন Darjeeling Tea যোগ হয়ে গিয়েছিল)।
  5. Blade পাতা বানায়। products.blade.php HTML হয়ে ফেরে, status 200. আসল পাতাটি ৭ নম্বর অংশে পুরোটা দেখবেন।

৪ · Eloquent: Active Record, migration আর seeder

পর্ব ৩৮-এ ORM-এর দুই ঘরানা দেখেছিলেন। Eloquent হলো Active Record: প্রতিটি object একটি সারি, আর সেই object নিজেই নিজেকে পড়ে, লেখে আর মোছে। Prisma-তে আপনি একটি আলাদা client-কে ডেটা লেখার নির্দেশ দেন। Eloquent-এ পণ্যের object নিজেই নিজেকে সেভ করে।

চায়ের দোকানের বাকির খাতার কথা ভাবুন। Prisma-র ধরন হলো দোকানদার: আপনি তাঁকে বলেন, “রহিম ভাইয়ের পাতায় ২০ টাকা লিখুন”। Active Record-এ প্রতিটি পাতা যেন নিজেই কলম ধরতে পারে: রহিম ভাইয়ের পাতাকে বললেন “২০ টাকা যোগ হলো, নিজেকে হালনাগাদ করুন”, আর পাতা নিজেই খাতায় লিখে ফেলল। কাজ দ্রুত, কিন্তু পাতা আর খাতা একে অপরের সাথে শক্ত করে বাঁধা।

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['category_id', 'name', 'price', 'stock'];

    public function category(): BelongsTo           // প্রতিটি পণ্য একটি বিভাগের
    {
        return $this->belongsTo(Category::class);
    }

    public function scopeInStock(Builder $query): void   // ->inStock() হিসেবে ডাকা যায়
    {
        $query->where('stock', '>', 0);
    }
}

Model-এ column-এর তালিকা নেই। Eloquent table থেকে column নিজেই নেয়, আর table-এর নাম আসে class-এর নাম থেকে। scopeInStock একটি query scope: বারবার লাগা শর্তকে নাম দেওয়া। এবার আসল query builder-এর SQL আর save() দেখুন:

সত্যিকারের run · Laravel 13.39.0 · SQLite · php artisan bazarghar:builder নামের ছোট command, যা নিচের কোড চালায়

$q = Product::where('price', '<', 200)->where('stock', '>', 0)->orderBy('price');
$this->line($q->toSql());                          // SQL, মান ছাড়া
$this->line(json_encode($q->getBindings()));       // মানগুলো আলাদা
$this->line($q->pluck('name')->implode(', '));

$p = new Product(['category_id' => 2, 'name' => 'Lemon Tea 50g', 'price' => 70, 'stock' => 9]);
$this->line('exists before save: '.var_export($p->exists, true));
$p->save();                                        // নিজেই insert লেখে
select * from "products" where "price" < ? and "stock" > ? order by "price" asc
[200,0]
Turmeric 200g, Red Chilli 200g, Green Tea 100g
exists before save: false
exists after save: true, id=8
insert into "products" ("category_id", "name", "price", "stock", "updated_at", "created_at") values (?, ?, ?, ?, ?, ?)

দুটি জিনিস লক্ষ করুন। মানগুলো SQL-এর লেখায় নেই, ? হয়ে আলাদা গেছে: এটি parameterized query, তাই SQL injection-এর পথ বন্ধ। আর save() নিজেই বুঝেছে নতুন object, তাই insert, সাথে created_at আর updated_at নিজে বসিয়েছে। exists ছিল false, পরে true. একই object-এ আবার save() দিলে হবে update. এই আচরণটি নিচে Python-এ ছোট একটি মডেলে নিজে চালিয়ে দেখুন।

# শেখানোর মডেল: Eloquent-এর মতো Active Record, Python-এর sqlite3 দিয়ে (আসল Laravel নয়)
import sqlite3
db = sqlite3.connect(':memory:')
db.execute('create table products (id integer primary key, name text, price int, stock int)')
class Query:                                  # where() জুড়ে SQL বানায়
    def __init__(self, model): self.model, self.wheres, self.args = model, [], []
    def where(self, col, op, val):
        self.wheres.append(f'"{col}" {op} ?'); self.args.append(val); return self
    def get(self):
        sql = f'select * from "{self.model.table}"'
        if self.wheres: sql += ' where ' + ' and '.join(self.wheres)
        print('SQL:', sql, self.args)
        return [self.model(**dict(zip(('id', 'name', 'price', 'stock'), r)))
                for r in db.execute(sql, self.args)]

class Product:
    table = 'products'
    def __init__(self, **attrs): self.__dict__.update(attrs); self.exists = 'id' in attrs
    @classmethod
    def where(cls, *a): return Query(cls).where(*a)
    def save(self):                           # object নিজেই নিজেকে লেখে
        if self.exists:
            db.execute('update products set price = ? where id = ?', (self.price, self.id))
            print('SQL: update ... where id =', self.id)
        else:
            cur = db.execute('insert into products (name, price, stock) values (?, ?, ?)',
                             (self.name, self.price, self.stock))
            self.id, self.exists = cur.lastrowid, True
            print('SQL: insert ... id =', self.id)

for name, price, stock in [('Turmeric 200g', 95, 25), ('Green Tea 100g', 180, 7), ('Miniket Rice 5kg', 420, 30)]:
    Product(name=name, price=price, stock=stock).save()
cheap = Product.where('price', '<', 200).where('stock', '>', 0).get()
print([p.name for p in cheap])
cheap[0].price = 99; cheap[0].save()

প্রথম তিনটি save()-এ object নতুন, তাই SQL: insert ... id = 1, 2, 3. তারপর where দুবার জুড়ে একটি SQL হলো: where "price" < ? and "stock" > ? [200, 0], আসল Eloquent-এর লাইনের সাথে প্রায় হুবহু। ফল ['Turmeric 200g', 'Green Tea 100g']. শেষে প্রথম পণ্যের দাম বদলে আবার save(): এবার exists সত্য, তাই SQL: update ... where id = 1. আসল Eloquent আরও একটি কাজ করে যা এখানে নেই: শুধু বদলানো column-গুলো (“dirty” attribute) update-এ পাঠায়।

Migration আর seeder

Schema বদলের নিয়ম পর্ব ৪০-এ শিখেছেন: প্রতিটি বদল একটি ক্রমিক ফাইল, যা প্রতিটি পরিবেশে একবার চলে। Laravel-এর migration PHP-তে লেখা, SQL-এ নয়, তাই একই ফাইল SQLite, MySQL আর PostgreSQL-এ চলে।

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->foreignId('category_id')->constrained()->restrictOnDelete();
    $table->string('name');
    $table->unsignedInteger('price');           // টাকা, পূর্ণসংখ্যায়
    $table->unsignedInteger('stock')->default(0);
    $table->timestamps();                       // created_at, updated_at
});

constrained() নাম থেকে বুঝে নেয় যে category_id যাবে categories.id-এ, আর restrictOnDelete() পণ্য থাকা অবস্থায় বিভাগ মুছতে দেয় না। Seeder শুরুর ডেটা বসায়: ৩টি বিভাগ, প্রতিটিতে ২টি করে পণ্য, মোট ৬টি।

সত্যিকারের run · php artisan migrate:fresh --seed

 Dropping all tables .. 4.67ms DONE
 INFO Running migrations.
 0001_01_01_000000_create_users_table .......... 26.06ms DONE
 0001_01_01_000001_create_cache_table .......... 16.83ms DONE
 0001_01_01_000002_create_jobs_table ........... 26.34ms DONE
 2026_10_31_200349_create_categories_table ...... 8.81ms DONE
 2026_10_31_200350_create_products_table ........ 5.49ms DONE
 INFO Seeding database.
 Database\Seeders\ProductSeeder ...................... 51 ms DONE

migrate:fresh সব table মুছে দেয়। প্রথম লাইনটিই “Dropping all tables”. Local-এ এটি সুবিধার, production-এ সর্বনাশ। Production-এ চলে শুধু php artisan migrate --force, যা বাকি থাকা নতুন migration চালায়। প্রথম তিনটি migration Laravel নিজে দেয়: user, cache আর queue-র jobs table. শেষেরটি ৮ নম্বর অংশে কাজে লাগবে।

৫ · সম্পর্ক আর N+1: ৭টি query থেকে ২টি

Model-এ সম্পর্ক লেখা হয় method হিসেবে। Product-এ belongsTo(Category::class), আর Category-তে উল্টো দিক hasMany(Product::class). একে-বহু সম্পর্কের নকশা পর্ব ৪১-এ দেখেছেন। Eloquent-এ সম্পর্কটি property-র মতো পড়া যায়: $product->category->name. আর এই সুবিধার ভেতরেই একটি ফাঁদ লুকিয়ে আছে।

যখন আপনি $product->category লেখেন আর বিভাগ আগে আনা হয়নি, Eloquent তখনই একটি query পাঠায়। একে বলে lazy loading. একটি পণ্যে সমস্যা নেই। কিন্তু ৬টি পণ্যের তালিকায় লুপ চালালে ১টি query তালিকার জন্য, আর ৬টি বিভাগের জন্য: N+1, যা পর্ব ৪২-এ Prisma দিয়ে মেপেছিলেন। Laravel-এ মাপা হলো এভাবে:

DB::enableQueryLog();
foreach (Product::all() as $product) {
    $product->category->name;                       // প্রতিবার একটি করে query
}
$this->line('Lazy loading: '.count(DB::getQueryLog()).' queries');

DB::flushQueryLog();
foreach (Product::with('category')->get() as $product) {
    $product->category->name;                       // আগেই আনা, query নেই
}
$this->line('Eager loading: '.count(DB::getQueryLog()).' queries');

সত্যিকারের run · Laravel 13.39.0 · SQLite · ৩টি বিভাগ, ৬টি পণ্য

$ php artisan bazarghar:queries
Lazy loading: 7 queries
Eager loading: 2 queries
 select * from "products" []
 select * from "categories" where "categories"."id" in (1, 2, 3) []

৭ থেকে ২. ভালো খবর হলো সংখ্যাটি আর পণ্যের সংখ্যার সাথে বাড়ে না: ৬০০টি পণ্যেও with()-এ ২টি query, lazy-তে ৬০১টি। লক্ষ করুন, মাত্র ৩টি আলাদা বিভাগ থাকলেও lazy loading ৬টি query পাঠিয়েছে, কারণ Eloquent প্রতিটি পণ্যের সম্পর্ক আলাদাভাবে আনে, একই বিভাগ আগে এসেছে কি না মনে রাখে না। একে বলে eager loading. নিচের stepper-এ with()-এর ভেতরের ধাপগুলো দেখুন।

1$q = Product::with('category');2$products = $q->get();   // query 13ids = unique(category_id)4Category::whereIn('id', ids)   // query 25foreach … $p->category->name
এই মুহূর্তের মান
query গোনা
মেমরিতে পণ্য
category_id-গুলো
মেমরিতে বিভাগ
lazy হলে এ পর্যন্ত
  1. শুধু পরিকল্পনা। with('category') কোনো query পাঠায় না, শুধু মনে রাখে যে পরে category সম্পর্কটিও আনতে হবে। query গোনা ০।
  2. প্রথম query. get() সব ৬টি পণ্য আনল। তাদের category_id: ১, ১, ২, ২, ৩, ৩। এ পর্যন্ত lazy আর eager দুটিতেই ১টি query.
  3. Id জোগাড়। Eloquent মেমরিতেই আলাদা id-গুলো বের করল: ১, ২, ৩। এতে কোনো query লাগে না।
  4. দ্বিতীয় query. একবারে where "categories"."id" in (1, 2, 3), তিনটি বিভাগ এলো। Eloquent প্রতিটি পণ্যের সাথে তার বিভাগ জুড়ে দিল। মোট ২টি query.
  5. লুপে আর query নেই। $p->category->name মেমরি থেকে পড়ে। Lazy হলে এই লুপেই ৬টি নতুন query যেত, মোট ৭টি।

একই কৌশল Python-এর sqlite3 দিয়ে একটি মডেলে চালিয়ে দেখুন। Fixture হুবহু বাজারঘরের: ৩টি বিভাগ, ৬টি পণ্য।

# শেখানোর মডেল: lazy loading বনাম with()-এর মতো eager loading, query গুনে (আসল Eloquent নয়)
import sqlite3
db = sqlite3.connect(':memory:')
db.executescript('''
create table categories (id integer primary key, name text);
create table products (id integer primary key, category_id int, name text);
insert into categories values (1,'Grocery'),(2,'Tea'),(3,'Spices');
insert into products values (1,1,'Miniket Rice 5kg'),(2,1,'Soybean Oil 1L'),(3,2,'Sylhet Tea 400g'),
  (4,2,'Green Tea 100g'),(5,3,'Turmeric 200g'),(6,3,'Red Chilli 200g');
''')
log = []
def q(sql, args=()):
    log.append(sql); return db.execute(sql, args).fetchall()

# Lazy: প্রতিটি product-এর category আলাদা query-তে
log.clear()
for pid, cid, name in q('select * from products'):
    q('select * from categories where id = ? limit 1', (cid,))
print('lazy :', len(log), 'queries')

# Eager: সব product, তারপর দরকারি সব category একবারে (where id in ...)
log.clear()
products = q('select * from products')
ids = sorted({cid for _, cid, _ in products})
marks = ', '.join('?' * len(ids))
cats = {i: n for i, n in q(f'select * from categories where id in ({marks})', ids)}
print('eager:', len(log), 'queries ->', log[1].replace(marks, ', '.join(map(str, ids))))
print([f'{name} ({cats[cid]})' for _, cid, name in products][:3])

for n in (6, 60, 600):                       # product বাড়লে
    print(f'N={n:>3}: lazy {1 + n:>3} queries, eager 2 queries')

মডেলটি আসল Laravel-এর সংখ্যা মিলিয়েছে: lazy : 7 queries আর eager: 2 queries, দ্বিতীয় query-টি select * from categories where id in (1, 2, 3). শেষ তিনটি লাইন সূত্রটি দেখায়: lazy-তে query = 1 + N, তাই N= 60-এ ৬১টি আর N=600-এ ৬০১টি। Eager-এ সব সময় ২টি, প্রতিটি সম্পর্কের জন্য একটি করে বাড়তি। প্রতিটি query-তে ডেটাবেস পর্যন্ত যাওয়া-আসায় ধরুন ১ ms লাগে (একটি অনুমান), তাহলে ৬০০টি পণ্যে শুধু অপেক্ষাতেই lazy-র প্রায় ৬০১ ms, eager-এর প্রায় ২ ms.

ভুলটি আগেই ধরুন। Laravel-এ Model::preventLazyLoading() নামে একটি সেটিং আছে, যা চালু থাকলে lazy loading হলেই exception দেয়। সাধারণত এটি শুধু production-এর বাইরে চালু রাখা হয়, যেমন AppServiceProvider-এ Model::preventLazyLoading(! app()->isProduction());. তাহলে development-এ N+1 সঙ্গে সঙ্গে চোখে পড়ে। আপনার version-এর docs-এ নামটি মিলিয়ে নিন।

৬ · Mass assignment আর validation: 201 আর 422

Product::create($data) একসাথে অনেক column বসায়। একে বলে mass assignment. বিপদ হলো, $data যদি সরাসরি ক্রেতার পাঠানো JSON হয়, তাহলে ক্রেতা যেকোনো column বদলাতে পারেন: id, is_admin, দাম। পর্ব ২৭-এ Zod দিয়ে এর বিরুদ্ধে পাহারা বসিয়েছিলেন। Laravel-এ পাহারা দুই স্তরে।

প্রথম স্তর model-এ: protected $fillable = [...], অর্থাৎ শুধু এই column-গুলো একসাথে বসানো যাবে। এর নাম fillable. বাকি key চুপচাপ বাদ পড়ে। আসল run-এ ইচ্ছা করে is_admin পাঠানো হয়েছিল:

সত্যিকারের run · Laravel 13.39.0

// $product->fill(['name' => 'Tea', 'price' => 100, 'is_admin' => true]);
$ php artisan bazarghar:massassign
{"name":"Tea","price":100}

দ্বিতীয় স্তর request-এ: FormRequest. এটি একটি আলাদা class, যেখানে নিয়ম লেখা থাকে। Controller-এ $request->validated() শুধু নিয়মে থাকা field-গুলো দেয়।

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // শুধু demo: আসল দোকানে এখানে admin কি না যাচাই হবে
    }

    public function rules(): array
    {
        return [
            'category_id' => ['required', 'integer', 'exists:categories,id'],
            'name'        => ['required', 'string', 'max:120'],
            'price'       => ['required', 'integer', 'min:1'],
            'stock'       => ['sometimes', 'integer', 'min:0'],
        ];
    }
}

exists:categories,id নিয়মটি ডেটাবেসে গিয়ে দেখে বিভাগটি সত্যিই আছে কি না। আর authorize() হলো কে এই কাজ করতে পারবেন, তার যাচাই (পর্ব ৪৮)। Demo-তে true, যা আসল অ্যাপে বিপজ্জনক, তাই মন্তব্যে লেখা আছে। এবার দুটি আসল request:

সত্যিকারের run · local server · body-তে ইচ্ছা করে বাড়তি id আর created_at

$ curl -s -i -X POST /api/products -H 'Accept: application/json' -H 'Content-Type: application/json' \
    -d '{"category_id":2,"name":"Lemon Tea 50g","price":70,"stock":9,"id":1,"created_at":"2000-01-01"}'
HTTP/1.1 201 Created
Content-Type: application/json

{"category_id":2,"name":"Lemon Tea 50g","price":70,"stock":9,"updated_at":"2026-11-02T19:10:27.000000Z","created_at":"2026-11-02T19:10:27.000000Z","id":9}

$ curl -s -i -X POST /api/products -H 'Accept: application/json' -H 'Content-Type: application/json' \
    -d '{"category_id":99,"price":0}'
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{"message":"The selected category id is invalid. (and 2 more errors)","errors":{"category_id":["The selected category id is invalid."],"name":["The name field is required."],"price":["The price field must be at least 1."]}}

প্রথমটিতে ক্রেতা id: 1 চেয়েছিলেন, পেলেন 9. created_at ২০০০ সাল চেয়েছিলেন, পেলেন আজকের সময়। দুটিই validated()-এ বাদ পড়েছে। দ্বিতীয়টিতে তিনটি ভুল একসাথে, প্রতিটি field-এর নামে আলাদা তালিকায়, status 422. লক্ষ করুন status-এর লেখা Unprocessable Content: HTTP-র নতুন নির্দেশিকা RFC 9110 (২০২২) এই নামটি ব্যবহার করে, পুরোনো লেখায় “Unprocessable Entity” দেখবেন। সংখ্যা একই।

1POST /api/products {category_id:99, price:0}2store(StoreProductRequest $request)3rules() একে একে যাচাই4throw ValidationException5422 + {"message", "errors"}
এই মুহূর্তের মান
Accept
errors-এ field
controller-এর ভেতরে গেল?
ডেটাবেসে লেখা
status
  1. Request এলো। Body-তে category_id ৯৯ (নেই এমন বিভাগ), price ০, আর name নেই। Header-এ Accept: application/json.
  2. Method-এ ঢোকার আগে। Laravel দেখল store চায় একটি StoreProductRequest. তাই সেটি বানিয়ে আগে authorize() আর তারপর নিয়ম চালায়। Controller-এর কোড এখনো চলেনি।
  3. তিনটি নিয়ম ভাঙল। exists:categories,id ডেটাবেসে ৯৯ খুঁজে পায়নি, name নেই, price ১-এর কম। Laravel প্রথম ভুলে থামে না, সব field যাচাই করে সব ভুল জমায়।
  4. Exception. ভুল থাকায় ValidationException ছোড়া হলো। কোনো insert হয়নি।
  5. 422. Request JSON চায়, তাই উত্তর JSON: message আর field ধরে errors. HTML form হলে Laravel আগের পাতায় redirect করে ভুলগুলো session-এ রাখত।

আকৃতিটি তুলনা করুন। পর্ব ৩৩-এ problem details (application/problem+json) দেখেছিলেন, যেখানে type, title, status থাকে। Laravel-এর ডিফল্ট নিজস্ব: message আর errors. পর্ব ৬৯-এর FastAPI-ও 422 দেয়, তবে detail নামের একটি তালিকায়, যেখানে প্রতিটি ভুলের অবস্থান (loc) থাকে। Status একই, আকার আলাদা। তাই React-এর frontend কোন backend-এর সাথে কথা বলছে, তা জেনেই ভুল দেখানোর কোড লিখতে হয়।

দুই স্তরের পাহারা নিচের JavaScript মডেলে নিজে চালিয়ে দেখুন।

// শেখানোর মডেল: Eloquent-এর $fillable আর validated() কীভাবে বাড়তি field ছেঁটে ফেলে (আসল Laravel নয়)
class Model {
  static fillable = [];
  constructor() { this.attributes = {}; }
  fill(input) {                               // শুধু তালিকায় থাকা key রাখে, বাকিগুলো চুপচাপ বাদ
    for (const [k, v] of Object.entries(input)) {
      if (this.constructor.fillable.includes(k)) this.attributes[k] = v;
    }
    return this;
  }
}
class Product extends Model { static fillable = ['category_id', 'name', 'price', 'stock']; }
class Unguarded extends Model { static fillable = ['*']; fill(input) { Object.assign(this.attributes, input); return this; } }

const body = { name: 'Tea', price: 100, is_admin: true, id: 1 }; // ক্রেতার পাঠানো JSON
console.log('fillable :', JSON.stringify(new Product().fill(body).attributes));
console.log('unguarded:', JSON.stringify(new Unguarded().fill(body).attributes));

// FormRequest-এর validated(): rules-এ নেই এমন key ফলেই আসে না
const rules = {
  category_id: v => Number.isInteger(v) && [1, 2, 3].includes(v) || 'The selected category id is invalid.',
  name: v => (typeof v === 'string' && v.length > 0) || 'The name field is required.',
  price: v => (Number.isInteger(v) && v >= 1) || 'The price field must be at least 1.',
};
function validate(input) {
  const errors = {}, validated = {};
  for (const [field, check] of Object.entries(rules)) {
    const r = check(input[field]);
    if (r === true) validated[field] = input[field]; else errors[field] = [r];
  }
  return Object.keys(errors).length ? { status: 422, errors } : { status: 201, validated };
}
console.log(JSON.stringify(validate({ category_id: 2, name: 'Darjeeling Tea', price: 350, is_admin: true })));
console.log(JSON.stringify(validate({ category_id: 99, price: 0 })));

প্রথম লাইন আসল Laravel-এর ফলের সাথে হুবহু: fillable : {"name":"Tea","price":100}. দ্বিতীয় লাইন দেখায় পাহারা না থাকলে কী হয়: "is_admin":true,"id":1 দুটিই ঢুকে গেছে। Laravel-এ এর সমান হলো protected $guarded = [];, যা অনেক টিউটোরিয়ালে “সহজ করতে” লেখা থাকে। তৃতীয় লাইনে ঠিক ইনপুট: status 201, আর validated-এ is_admin নেই। চতুর্থ লাইনে ভুল ইনপুট: status 422, আর তিনটি বার্তা আসল run-এর সাথে একই।

$request->all() দিয়ে create নয়। Product::create($request->all()) লিখলে পুরো পাহারা নির্ভর করে শুধু $fillable-এর ওপর। কেউ পরে $fillable-এ price রাখলেন, আর এখন এমন একটি route যেখানে ক্রেতা শুধু নাম বদলাতে পারার কথা, সেখানেও দাম বদলানো যাবে। নিয়ম: সবসময় validated(), আর $fillable সংকীর্ণ।

৭ · Blade: template, escaping আর {!! !!}-এর বিপদ

React-এ পাতা বানাতেন JSX দিয়ে, browser-এ বা server-এ। Laravel-এর পুরোনো আর এখনো খুব প্রচলিত পথ হলো server-এ HTML বানানো, Blade template দিয়ে। এটি পর্ব ০৩-এর MPA ধরন: প্রতিটি পাতা server-এ পুরো তৈরি হয়ে আসে। বাজারঘরের পণ্যের পাতা:

<h1>বাজারঘর</h1>
<ul>
@forelse ($products as $product)
    <li>{{ $product->name }} ({{ $product->category->name }}): {{ $product->price }} টাকা</li>
@empty
    <li>কোনো পণ্য নেই</li>
@endforelse
</ul>
<p>{{ '<script>alert(1)</script>' }}</p>

সত্যিকারের run · curl -s /products

<h1>বাজারঘর</h1>
<ul>
    <li>Turmeric 200g (Spices): 95 টাকা</li>
    <li>Red Chilli 200g (Spices): 110 টাকা</li>
    <li>Green Tea 100g (Tea): 180 টাকা</li>
    <li>Sylhet Tea 400g (Tea): 260 টাকা</li>
    <li>Darjeeling Tea (Tea): 350 টাকা</li>
    <li>Miniket Rice 5kg (Grocery): 420 টাকা</li>
</ul>
<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>

@forelse আর @empty হলো Blade-এর directive: তালিকা ফাঁকা হলে অন্য লাইন। শেষ লাইনটি ইচ্ছা করে রাখা: <script> লেখাটি browser-এ লেখা হিসেবেই দেখাবে, চলবে না। কারণ {{ }} সবকিছু escape করে, ঠিক যেমন React-এর JSX { }-এ করে (পর্ব ০৮)। Blade ফাইল প্রথমবার সাধারণ PHP-তে compile হয়ে storage/framework/views-এ জমা থাকে, আর {{ $x }} হয়ে যায় e($x), যা ভেতরে PHP-র htmlspecialchars ডাকে।

যেখানে পাহারা আপনি নিজে সরান

Blade-এ আরেকটি লেখার ধরন আছে: {!! $x !!}. এটি কিছুই escape করে না। React-এর dangerouslySetInnerHTML-এর সমান, শুধু নামে ভয়ের কোনো চিহ্ন নেই। ধরুন বাজারঘরে ক্রেতার রিভিউ দেখানো হচ্ছে, আর একজন রিভিউতে script লিখে দিলেন:

সত্যিকারের run · Laravel 13.39.0 · একই মান দুই ধরনে

// escape.blade.php:   <p>{{ $review }}</p>   <p>{!! $review !!}</p>
// $review = '<b>ভালো</b><script>steal()</script>'
$ php artisan bazarghar:blade
<p>&lt;b&gt;ভালো&lt;/b&gt;&lt;script&gt;steal()&lt;/script&gt;</p>
<p><b>ভালো</b><script>steal()</script></p>

// attr.blade.php:   <input value="{{ $name }}">   ,   $name = '" onmouseover="alert(1)'
$ php artisan bazarghar:attr
<input value="&quot; onmouseover=&quot;alert(1)">

দ্বিতীয় লাইনটি একটি stored XSS: রিভিউ ডেটাবেসে থাকবে, আর যতজন পাতাটি খুলবেন, প্রত্যেকের browser-এ steal() চলবে, তাঁদের session-এর অধিকারে। তৃতীয় run দেখায় {{ }} attribute-এর ভেতরেও নিরাপদ, কারণ " হয়ে যায় &quot;. তালিকাটি পর্ব ৫০-এর XSS অংশের সাথে মিলিয়ে নিন, আর তত্ত্ব আছে XSS আর CSP পর্বে।

// শেখানোর মডেল: Blade-এর {{ }} আর {!! !!}, ছোট একটি template engine-এ (আসল Blade নয়)
// Blade {{ }}-কে PHP-র htmlspecialchars দিয়ে escape করে; এখানে একই পাঁচটি অক্ষর
const e = s => String(s)
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;').replace(/'/g, '&#039;');

function render(template, data) {
  return template
    .replace(/\{!!\s*(\w+)\s*!!\}/g, (_, k) => String(data[k]))   // raw: যা আছে তাই
    .replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => e(data[k]));       // escaped
}

const review = '<b>ভালো</b><script>steal()</script>';
const tpl = '<p>{{ review }}</p>\n<p>{!! review !!}</p>';
console.log(render(tpl, { review }));

// attribute-এর ভেতরেও উদ্ধৃতি চিহ্ন escape হওয়া জরুরি
const name = '" onmouseover="alert(1)';
console.log(render('<input value="{{ name }}">', { name }));
console.log(render('<input value="{!! name !!}">', { name }));

মডেলের প্রথম দুই লাইন আসল Blade-এর সাথে অক্ষরে অক্ষরে মিলেছে: <p>&lt;b&gt;ভালো… আর raw <p><b>ভালো</b><script>steal()</script></p>. তৃতীয় লাইনও আসল run-এর মতো: value="&quot; onmouseover=&quot;alert(1)". চতুর্থ লাইন দেখায় raw হলে কী হতো: value="" onmouseover="alert(1)", মানে উদ্ধৃতি চিহ্নটি attribute বন্ধ করে নতুন একটি event handler খুলে দিয়েছে। Escape করার অক্ষর মাত্র পাঁচটি: & < > " '.

তাহলে {!! !!} কখন? শুধু যখন HTML-টি আপনার নিজের, বিশ্বস্ত উৎসের, যেমন নিজের লেখা Markdown থেকে নিজের server-এ বানানো HTML. ব্যবহারকারীর লেখা কখনো সরাসরি নয়। যদি সত্যিই ব্যবহারকারীকে কিছু HTML (মোটা লেখা, link) দিতে চান, তাহলে একটি পরীক্ষিত HTML sanitizer দিয়ে অনুমোদিত tag ছাড়া সব সরিয়ে তারপর দেখান, আর সাথে Content Security Policy রাখুন। কোড review-তে {!! খোঁজা একটি ভালো অভ্যাস।

৮ · Auth, Sanctum, queue, scheduler, Inertia, Livewire আর test

Laravel-কে “batteries included” বলা হয়, কারণ ওপরের সবকিছুর বাইরেও অনেক কিছু একই বাক্সে আসে। এই অংশে প্রতিটির জায়গা চিনিয়ে দেওয়া হলো। বিস্তারিত তত্ত্ব আগের পর্বগুলোতে আছে।

Login: starter kit আর Sanctum

Laravel 13-এর docs অনুযায়ী laravel new চালালে একটি starter kit বেছে নেওয়া যায়: React, Vue, Svelte বা Livewire. প্রতিটিতে login, registration, password reset, email যাচাই আর two-factor authentication তৈরি থাকে। ভেতরের কাজটি করে Laravel Fortify নামের package, আর সব কোড আপনার project-এর ভেতরে কপি হয়, তাই নিজের মতো বদলানো যায়। Login-এর মূল ধারণা, cookie, session আর CSRF, পর্ব ৪৫-এ শিখেছেন। Starter kit সেগুলোই তৈরি অবস্থায় দেয়।

API-র জন্য আছে Sanctum. এর দুটি কাজ: mobile অ্যাপ বা অন্য client-এর জন্য সাধারণ API token, আর একই domain-এর SPA-র জন্য cookie-ভিত্তিক login. লেখার সময়ে Packagist-এ Sanctum-এর সর্বশেষ version 4.3.3. আমাদের install:api run শুধু routes/api.php বানিয়ে HasApiTokens trait যোগ করতে বলেছে। Token login এই পর্বে চালানো হয়নি। Token আর JWT-র পার্থক্য পর্ব ৪৬-এ দেখেছেন: Sanctum-এর token সাধারণত ডেটাবেসে hash হিসেবে থাকে, তাই বাতিল করা সহজ।

Queue আর scheduler

পর্ব ৬২-এ BullMQ আর Redis দিয়ে যা করেছিলেন, Laravel-এ তা framework-এর ভেতরেই আছে। নতুন project-এ QUEUE_CONNECTION=database, অর্থাৎ job জমা হয় ডেটাবেসের jobs table-এ, Redis ছাড়াই। একটি job class:

class SendInvoiceEmail implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;                                // মোট ৩ বার চেষ্টা

    public function __construct(public int $orderId) {}  // job-এ শুধু id

    public function handle(): void
    {
        Log::info("invoice email sent for order {$this->orderId}");
    }
}
// কোথাও থেকে:  SendInvoiceEmail::dispatch(501);

সত্যিকারের run · Laravel 13.39.0 · database queue

$ php artisan bazarghar:dispatch
jobs waiting: 1
$ php artisan queue:work --once
 2026-11-02 19:08:21 App\Jobs\SendInvoiceEmail .. RUNNING
 2026-11-02 19:08:21 App\Jobs\SendInvoiceEmail .. 19.83ms DONE
$ tail -1 storage/logs/laravel.log
[2026-11-02 19:08:21] local.INFO: invoice email sent for order 501

dispatch শুধু সারিতে রাখে, তাই সারিতে ১টি। queue:work হলো worker, যা আলাদা process হিসেবে চলে। --once দিয়ে একটি job চালিয়ে থামানো হয়েছে, production-এ এটি সারাক্ষণ চলে আর কেউ নজর রাখে (যেমন Supervisor বা systemd). Redis queue-র জন্য Laravel-এর নিজস্ব dashboard Horizon-ও আছে। পর্ব ৬২-এর সব নিয়ম, idempotent handler আর job-এ শুধু id, এখানেও খাটে।

নির্দিষ্ট সময়ের কাজ লেখা হয় routes/console.php-এ, scheduler দিয়ে:

Schedule::command('bazarghar:report')->dailyAt('09:00')->timezone('Asia/Dhaka');

সত্যিকারের run · app-এর time zone UTC

$ php artisan schedule:list

 0 3 * * * php artisan bazarghar:report .. Next Due: 7 hours from now

ঢাকার সকাল ৯টা দেখাচ্ছে 0 3 * * *, কারণ app UTC-তে চলে আর ঢাকা UTC+6. পর্ব ৬২-এর time zone-এর শিক্ষা এখানে নিজের চোখে দেখলেন। Laravel-এর scheduler নিজে ঘড়ি দেখে না। Server-এর cron প্রতি মিনিটে একবার php artisan schedule:run চালায়, আর Laravel ঠিক করে এই মিনিটে কোন কাজের সময় হয়েছে। তাই crontab-এ মাত্র একটি লাইন লাগে।

React বা Vue চাইলে: Inertia, আর JavaScript কম চাইলে: Livewire

🔗 Inertia

Inertia Laravel-এর route আর controller রেখে পাতা আঁকে React, Vue বা Svelte দিয়ে। Controller JSON API বানায় না, পাতার নাম আর props পাঠায়। আলাদা API আর client-side router লাগে না। Laravel 13-এর React starter kit Inertia 3 আর React 19 দিয়ে বানানো।

React জানলে সহজ পথ

⚡ Livewire

Livewire দিয়ে PHP class আর Blade-এ interactive UI বানানো যায়। Button চাপলে ছোট request server-এ যায়, PHP-র state বদলায়, আর বদলানো HTML ফিরে আসে। JavaScript প্রায় লিখতে হয় না। লেখার সময়ে Livewire 4.

মূলত Blade আর PHP-র দল

Test: PHPUnit বা Pest

Laravel-এ নতুন project-এ test চালানোর সব ব্যবস্থা থাকে। RefreshDatabase প্রতিটি test-এর জন্য পরিষ্কার ডেটাবেস দেয়, factory নমুনা ডেটা বানায়, আর postJson বা getJson server না চালিয়েই request পাঠায়। আমাদের project-এ PHPUnit 12. অনেকে Pest পছন্দ করেন, যা PHPUnit-এর ওপরে আরও ছোট লেখার ধরন দেয় (লেখার সময়ে Pest 5.3.5). দুটিই php artisan test দিয়ে চলে।

public function test_it_rejects_a_zero_price(): void
{
    $category = Category::factory()->create();

    $this->postJson('/api/products', [
        'category_id' => $category->id,
        'name' => 'Free Tea',
        'price' => 0,
    ])->assertUnprocessable()->assertJsonValidationErrors('price');
}

public function test_out_of_stock_products_are_hidden(): void
{
    Product::factory()->create(['name' => 'Gone', 'stock' => 0]);
    Product::factory()->create(['name' => 'Here', 'stock' => 2]);

    $this->getJson('/api/products')->assertOk()
        ->assertJsonCount(1, 'data')->assertJsonPath('data.0.name', 'Here');
}

সত্যিকারের run · PHPUnit 12.5.42 · একটি local কম্পিউটারে একবার · লাইনের মাঝের ফাঁকা জায়গা ছোট করা

$ php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ that true is true

   PASS  Tests\Feature\ExampleTest
  ✓ the application returns a successful response              0.12s

   PASS  Tests\Feature\ProductApiTest
  ✓ it creates a product                                       0.08s
  ✓ it rejects a zero price                                    0.01s
  ✓ out of stock products are hidden                           0.01s

  Tests:    5 passed (11 assertions)
  Duration: 0.45s

একটি অদ্ভুত ব্যাপার আমরা দেখেছি: AI coding tool-এর পরিবেশ থেকে একই command চালালে Laravel পুরো তালিকার বদলে এক লাইনের JSON দিয়েছে, যেমন {"tool":"phpunit","result":"passed","tests":5,…}. Laravel 13-এর project-এ laravel/agent-detector নামের একটি package থাকে, আর ফলটি দেখে মনে হয় সেটি এমন পরিবেশ চিনে output বদলায়। উপরের output সাধারণ terminal-এর, যা আপনি দেখবেন। Test-এর কৌশল নিয়ে বিস্তারিত পর্ব ৭৫-এ (API integration test).

৯ · Deploy: shared hosting বনাম VPS, আর কখন Laravel বেছে নেবেন

Node.js অ্যাপ চালাতে সাধারণত একটি সারাক্ষণ চলা process লাগে। PHP-র ধরন আলাদা: web server প্রতিটি request-এ PHP চালায় (সাধারণত PHP-FPM দিয়ে), কাজ শেষে সেই request-এর সব মেমরি ছেড়ে দেয়। এই কারণেই সাধারণ shared hosting-এ PHP অ্যাপ সহজে চলে। Laravel-ও চলে, তবে কয়েকটি শর্তে। নিচের তালিকা সাধারণ চিত্র। প্রতিটি hosting-এর সুবিধা আলাদা, তাই আপনার প্যাকেজের নিয়ম মিলিয়ে নিন।

বিষয়cPanel-এর shared hostingVPS
PHP versionপ্যানেল থেকে বাছাই, Laravel 13-এর জন্য 8.3 বা নতুন থাকতে হবেনিজে install আর হালনাগাদ
Document rootDomain-কে public/-এ দেখাতে হবে; না পারলে ঝুঁকিnginx বা Apache-তে নিজে ঠিক করা
Composer আর SSHঅনেক প্যাকেজে আছে, কিছুতে নেই বা সীমিতপুরো নিয়ন্ত্রণ
Databaseসাধারণত MySQL বা MariaDB তৈরি থাকেযা খুশি, নিজে চালাতে হয়
Queue workerসারাক্ষণ চলা process প্রায়ই নিষেধ; cron দিয়ে কাজ চালানো যায়Supervisor বা systemd দিয়ে সবসময় চালু
SchedulercPanel-এর cron-এ এক লাইনcrontab-এ এক লাইন
দায়িত্বServer-এর নিরাপত্তা hosting দেখেOS, firewall, backup সব আপনার

সবচেয়ে বড় ভুল: পুরো project-কে public_html-এ রাখা। তখন domain-এর root হয় project-এর মূল ফোল্ডার, আর ভুল কনফিগারেশনে .env (database password, APP_KEY) বা storage/logs browser থেকে পড়া যেতে পারে। নিরাপদ নিয়ম: project থাকবে public_html-এর বাইরে, আর শুধু public/ ফোল্ডারটি হবে domain-এর document root. cPanel-এ addon domain বা subdomain-এর document root বদলানো যায় কি না, তা দেখে নিন। আর অবশ্যই APP_ENV=production, APP_DEBUG=false.

একটি সাধারণ deploy-এর ধাপ (VPS-এ বা SSH আছে এমন shared hosting-এ), উদাহরণ হিসেবে, এখানে চালানো হয়নি:

composer install --no-dev --optimize-autoloader   # dev package বাদ
php artisan migrate --force                        # production-এ শুধু নতুন migration
php artisan optimize                               # config, route, view cache
# crontab বা cPanel cron-এ একটি লাইন, প্রতি মিনিটে:
* * * * * cd ~/bazarghar && php artisan schedule:run >> /dev/null 2>&1

Shared hosting-এ queue worker সবসময় চালু রাখা না গেলে অনেকে scheduler দিয়ে প্রতি মিনিটে queue:work --stop-when-empty চালান: সারি খালি হলে worker নিজে থামে। ছোট দোকানে এটি চলে, তবে job শুরু হতে এক মিনিট পর্যন্ত দেরি হতে পারে, আর একসাথে অনেক worker চলার ঝুঁকিও থাকে (Laravel-এর withoutOverlapping() এটি আটকায়)। Deploy-এর পুরো বিষয়টি মডিউল ১২-এ (Production-এ পাঠানো): কোথায় চালাবেন তার তুলনা পর্ব ৮২-এ (কোথায় চালাবেন — VPS, shared hosting, PaaS আর serverless), Docker-এ অ্যাপ পর্ব ৮০-এ, আর CI/CD পর্ব ৮১-এ। Stack বাছাইয়ের ছক আসবে মডিউল ১৩-এর পর্ব ৮৫-এ।

কখন Laravel বেছে নেবেন

✅ ভালো মেলে

Admin panel, ফর্ম, রিপোর্ট আর CRUD-ভারী ব্যবসার অ্যাপ। ছোট দল যারা auth, queue, mail, scheduler সব এক জায়গায় চায়। যে hosting-এ PHP আর MySQL আগে থেকেই আছে। দলে PHP জানা মানুষ আছেন, বা পুরোনো PHP অ্যাপ আধুনিক করতে হবে।

দ্রুত, পরিণত, এক বাক্সে

🤔 ভেবে দেখুন

পুরো দল TypeScript-এ, আর frontend আর backend-এ একই type ভাগ করতে চান (পর্ব ৩৭)। হাজার হাজার একসাথে খোলা WebSocket সংযোগ (Laravel-এ Reverb আছে, তবে মডেল আলাদা)। Data science বা AI-এর Python library সরাসরি দরকার (তখন পর্ব ৬৮–৬৯-এর Django বা FastAPI). Edge বা serverless-এ ছোট function.

দলের দক্ষতাই প্রথম প্রশ্ন

কোনো framework সবার জন্য সেরা নয়। Express-এ আপনি প্রতিটি অংশ বেছে জোড়া দেন: নমনীয়, কিন্তু সিদ্ধান্ত অনেক। Laravel সিদ্ধান্তগুলো আগেই নিয়ে রেখেছে: দ্রুত শুরু, কিন্তু তার পথে চলতে হয়। Django-র দর্শন Laravel-এর কাছাকাছি, আর Rails দুটিরই পূর্বসূরি ধরা যায় (পর্ব ৭১)।

আরও গভীরে: Laravel-এর কেন্দ্রে একটি service container: class-এর constructor-এ type দেখে Laravel নিজে dependency বানিয়ে দেয়, যা পর্ব ২৯-এর dependency injection-এর স্বয়ংক্রিয় রূপ। Route::get বা DB::table-এর মতো “facade” আসলে container থেকে আসল object খুঁজে আনার একটি static-চেহারার পথ। প্রতিটি request-এ পুরো অ্যাপ নতুন করে চালু হয়, তাই config আর route cache (php artisan optimize) production-এ গতি দেয়, আর PHP-র OPcache compile করা কোড মেমরিতে রাখে। Octane নামের package অ্যাপকে মেমরিতে রেখে দেয় (Node.js-এর মতো দীর্ঘজীবী process), তখন এক request-এর state পরের request-এ চলে যাওয়ার নতুন ঝুঁকি আসে।

পরীক্ষা ও ইন্টারভিউতে:

  • Eloquent কোন pattern? Active Record: object নিজেই save(), delete() করে।
  • N+1 কীভাবে সারাবেন? with('relation') দিয়ে eager loading; ৬টি পণ্যে ৭টি query থেকে ২টি।
  • $fillable আর $guarded-এর পার্থক্য? প্রথমটি অনুমোদিত column-এর তালিকা, দ্বিতীয়টি নিষিদ্ধের তালিকা। $guarded = [] মানে কোনো পাহারা নেই।
  • {{ }} আর {!! !!}? প্রথমটি htmlspecialchars দিয়ে escape করে, দ্বিতীয়টি raw, তাই XSS-এর ঝুঁকি।
  • Shared hosting-এ document root কোথায়? Project-এর public/ ফোল্ডার, পুরো project নয়।

১০ · AI-এর যুগে: Laravel কোড যাচাই

Laravel-এর দশ বছরের বেশি পুরোনো টিউটোরিয়াল আর Stack Overflow উত্তর ইন্টারনেটে ভরা। AI সেখান থেকেই শিখেছে, তাই প্রায়ই পুরোনো version-এর কোড দেয়, যা দেখতে ঠিক কিন্তু আপনার version-এ ভুল জায়গায় বা অনিরাপদ। সাধারণ ভুলগুলো:

AI-এর ভুলযা ঘটবেযাচাই
app/Http/Kernel.php-এ middleware বা app/Console/Kernel.php-এ scheduleLaravel 11 থেকে নতুন project-এ ফাইলগুলো নেই; এখন bootstrap/app.php আর routes/console.phpphp artisan --version আর সেই version-এর docs
protected $guarded = []; আর create($request->all())Mass assignment খোলাবাড়তি field পাঠিয়ে ফল দেখা
Blade-এ {!! $comment !!}Stored XSS{!! খুঁজে প্রতিটির কারণ লেখা
লুপে $p->category, with() নেইN+1, তালিকা বড় হলে ধীরQuery log বা preventLazyLoading
DB::select("... where id = $id")SQL injectionসবসময় binding বা Eloquent
FormRequest-এর authorize() সবসময় trueযে কেউ admin-এর কাজ করতে পারেঅন্য user দিয়ে test লেখা
নেই এমন package বা artisan commandInstall বা run-এ ব্যর্থ, বা ভুয়া packagePackagist-এ খোঁজা, php artisan list
Production-এ APP_DEBUG=true, বা migrate:freshতথ্য ফাঁস, বা সব ডেটা মুছে যাওয়াDeploy-এর ধাপ নিজে পড়া

AI-কে কাজ দেওয়ার সময় version আর নিরাপত্তার শর্তগুলো নিজেই লিখে দিন। তাহলে পুরোনো ধরন কম আসবে, আর মেলানোর মতো একটি তালিকাও পাবেন।

আমি Laravel ব্যবহার করছি (version: [এখানে php artisan --version-এর ফল], PHP [php -v-এর ফল])। বাজারঘরের জন্য Review model লাগবে: প্রতিটি review একটি product-এর, তাতে rating (১ থেকে ৫) আর comment. শর্ত: ১. আমার version-এর ফাইল-গঠন মেনে চলো (bootstrap/app.php, routes/console.php). পুরোনো Kernel.php ব্যবহার করবে না। ২. migration, model ($fillable সংকীর্ণ), FormRequest (422-এ errors), আর controller দাও। create-এ শুধু validated()। ৩. Blade-এ comment শুধু {{ }} দিয়ে দেখাবে, {!! !!} নয়। ৪. Product-এর তালিকায় review-এর সংখ্যা দেখাতে N+1 এড়াও, আর বলো কয়টি query হবে। ৫. authorize()-এ শুধু login করা ক্রেতা review দিতে পারবেন। ৬. নতুন package যোগ করবে না, যা নেই এমন option বা command বানাবে না। শেষে একটি feature test দাও যা 201, 422 আর বাড়তি field বাদ পড়া যাচাই করে।

AI-এর লেখা Laravel কোড হাতে পেলে

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১PHP-র “অনিরাপদ ভাষা” বদনাম কতটা ভাষার দোষ ছিল, আর কতটা অভ্যাস আর ecosystem-এর? কী বদলে পরিস্থিতি পাল্টাল?

দুটোই ছিল, তবে অভ্যাসের ভাগ বড়। ভাষার দিক থেকে পুরোনো PHP কিছু বিপজ্জনক সুবিধা দিত। register_globals চালু থাকলে URL-এর ?admin=1 সরাসরি $admin variable হয়ে যেত, আর কোডে আগে থেকে মান না বসালে আক্রমণকারী নিজেই সেটি ঠিক করে দিতে পারতেন। “Magic quotes” ইনপুটে নিজে থেকে backslash বসিয়ে নিরাপত্তার একটি ভুয়া অনুভূতি দিত। পুরোনো mysql_* function-এ parameterized query সহজ ছিল না। এই সেটিংগুলো PHP 5-এর সময়েই ধীরে ধীরে বাতিল হয়, আর PHP 7-এর আগে-পরে পুরোপুরি সরে যায়।

কিন্তু বড় সমস্যা ছিল সংস্কৃতি। PHP শেখা খুব সহজ ছিল, আর hosting সস্তা ও সবখানে। লাখো মানুষ প্রথম ওয়েব পাতা PHP-তে লিখেছেন, অনেকে কোনো নিরাপত্তা শিক্ষা ছাড়াই। টিউটোরিয়ালে "SELECT * FROM users WHERE id = " . $_GET['id'] ধরনের লাইন ছিল সাধারণ, আর সেগুলো কপি হয়ে ছড়িয়েছে। একই সময়ে অন্য ভাষাতেও SQL injection হতো, কিন্তু PHP-র ব্যবহার এত বেশি ছিল যে উদাহরণও বেশি।

পরিবর্তন এসেছে কয়েক দিক থেকে। PDO-র মতো API parameterized query সহজ করল। Composer আর PHP-FIG-এর PSR নিয়মগুলো (autoloading, coding style, HTTP interface) একটি ভাগ করা ecosystem বানাল, ফলে মানুষ নিজে সবকিছু না লিখে পরীক্ষিত package ব্যবহার করতে শুরু করলেন। PHP 7 আর 8 type আর ভালো error আচরণ আনল। আর Laravel ও Symfony-র মতো framework নিরাপদ পথকেই সহজ পথ বানাল: Eloquent নিজেই binding দেয়, Blade নিজেই escape করে, form-এ CSRF token নিজে থেকে থাকে।

এই পর্বের run-গুলোই প্রমাণ: query-তে ?, Blade-এ &lt;script&gt;, আর বাড়তি field বাদ। তবে বিপদ পুরো যায়নি, শুধু জায়গা বদলেছে। এখন ভুল হয় {!! !!}, $guarded = [], DB::raw, আর production-এ APP_DEBUG=true-এ, অর্থাৎ যেখানে কেউ সচেতনভাবে ডিফল্ট পাহারা সরান।

মূল উপলব্ধি: ভাষার নিরাপত্তা অনেকটাই নির্ভর করে তার সবচেয়ে সহজ পথটি নিরাপদ কি না তার ওপর। আধুনিক PHP আর Laravel সহজ পথকে নিরাপদ করেছে; ঝুঁকি এখন আসে সেই পথ ছেড়ে বেরোলে।

প্র ০২Active Record (Eloquent, Rails) আর Data Mapper (Doctrine, অনেকটা Prisma-র মতো আলাদা client): কোনটি কখন ভালো, আর বড় project-এ Active Record নিয়ে কী অভিযোগ ওঠে?

দুটি নামই জনপ্রিয় হয় Martin Fowler-এর ২০০২ সালের বই Patterns of Enterprise Application Architecture থেকে। Active Record-এ একটি object একসাথে দুটি কাজ করে: ব্যবসার নিয়ম (যেমন “stock শূন্যের নিচে যাবে না”) আর ডেটাবেসে নিজেকে পড়া-লেখা। Data Mapper-এ এই দুটি আলাদা: object শুধু ডেটা আর নিয়ম জানে, ডেটাবেসের কথা জানে না, আর আলাদা একটি mapper বা repository তাকে সেভ করে। PHP-তে Doctrine ORM এই ধরনের পরিচিত উদাহরণ, যা Symfony-র সাথে বেশি দেখা যায়।

Active Record-এর শক্তি গতি। Product::create, $product->save(), $category->products, সবকিছু এক জায়গায়, কম কোডে। CRUD-ভারী অ্যাপে, যেখানে model প্রায় হুবহু table, এটি খুব স্বাভাবিক। Rails এই পথ জনপ্রিয় করেছিল, আর Laravel সেই ধারাতেই এসেছে।

অভিযোগগুলোও পরিচিত। প্রথম, model বড় হতে থাকে (“fat model”), কারণ query, সম্পর্ক, নিয়ম, event সব এক class-এ জমে। দ্বিতীয়, ডেটাবেস ছাড়া ব্যবসার নিয়ম test করা কঠিন, কারণ object নিজেই ডেটাবেসের সাথে বাঁধা। তৃতীয়, query লুকিয়ে থাকে: $product->category দেখতে property, কিন্তু আসলে একটি query, আর সেখান থেকেই N+1. এই পর্বে ঠিক সেটিই মাপা হয়েছে: ৭ বনাম ২।

বাস্তবে বড় Laravel দলগুলো মাঝামাঝি পথ নেন। Eloquent রাখেন, কিন্তু জটিল ব্যবসার নিয়ম আলাদা service বা “action” class-এ নেন (পর্ব ২৯-এর ভাগ), query-গুলো scope-এ নাম দেন, আর preventLazyLoading দিয়ে লুকোনো query ধরেন। Data Mapper অনেক বেশি নিয়ন্ত্রণ দেয়, কিন্তু শুরু করতে ও শিখতে বেশি সময় লাগে। কোনটি “ঠিক”, তা নিয়ে সম্প্রদায়ে মতভেদ আছে, আর দুই পক্ষেরই যুক্তি আছে।

মূল উপলব্ধি: Active Record ছোট থেকে মাঝারি অ্যাপে উৎপাদনশীলতা দেয়, বিনিময়ে লুকোনো query আর বাঁধা object-এর দাম নেয়। দাম জেনে ব্যবহার করলে সেটি যুক্তিসঙ্গত পছন্দ।

প্র ০৩Mass assignment কেন একটি পরিচিত দুর্বলতা হয়ে উঠল? Rails-এর একটি ঘটনা থেকে Laravel-এর $fillable পর্যন্ত শিক্ষাটি কী?

২০১২ সালের মার্চে Egor Homakov নামের একজন নিরাপত্তা গবেষক দেখান যে GitHub, যা Rails-এ চলত, mass assignment-এর ফাঁক দিয়ে তাঁকে এমন একটি repository-তে লেখার অধিকার দিচ্ছে যা তাঁর নয়। ব্যাপকভাবে প্রকাশিত বর্ণনা অনুযায়ী তিনি একটি form-এ বাড়তি একটি field পাঠিয়ে নিজের public key অন্য একটি repository-র মালিকানায় যুক্ত করেছিলেন। তিনি আগে Rails-এর issue tracker-এ ডিফল্ট আচরণ নিয়ে সতর্ক করেছিলেন, আর বিষয়টি গুরুত্ব না পাওয়ায় এভাবে দেখিয়েছিলেন বলে জানা যায়। GitHub দ্রুত ফাঁকটি বন্ধ করে।

সমস্যার মূল ছিল সুবিধাজনক ডিফল্ট: Model.new(params) সব key বসিয়ে দিত, আর কোন field বদলানো যাবে তার তালিকা লেখা ছিল ঐচ্ছিক। Developer-রা মনে রাখতেন form-এ কী আছে, কিন্তু আক্রমণকারী form মানেন না, তিনি সরাসরি HTTP request বানান। ঘটনার পরে Rails 4 “strong parameters” আনে, যেখানে controller-এ স্পষ্ট করে কোন field গ্রহণযোগ্য তা বলতে হয়। পর্ব ৭১-এ Rails-এর params.expect দেখবেন, যা সেই ধারার আধুনিক রূপ।

Laravel-এর উত্তর দুই স্তরের। Model-এ $fillable (বা $guarded), যা Eloquent-এর শুরুর দিক থেকে আছে, আর request-এ FormRequest-এর validated(). এই পর্বের run-এ দুটিই কাজ করেছে: fill() থেকে is_admin বাদ, আর API-তে পাঠানো id: 1 হয়ে গেল নতুন id ৯. Express-এ (পর্ব ২৭) Zod-এর schema-ও একই কাজ করে, অজানা key বাদ দিয়ে।

শিক্ষাটি framework ছাড়িয়ে যায়: client থেকে আসা object কখনো সরাসরি ডেটাবেসের object নয়। মাঝখানে একটি allow-list থাকতে হবে, আর সেটি হতে হবে ডিফল্ট, ঐচ্ছিক নয়। OWASP-এর API নিরাপত্তার তালিকাতেও property-স্তরের এই ধরনের দুর্বলতা আলাদা করে আছে।

মূল উপলব্ধি: “আমার form-এ তো ওই field নেই” কোনো পাহারা নয়। কোন field ঢুকবে, তা server-এ স্পষ্ট তালিকায় লেখা থাকতে হবে।

প্র ০৪বাংলাদেশের একটি ছোট দোকানের জন্য Laravel অ্যাপ কোথায় চালাবেন: cPanel-এর shared hosting, না একটি ছোট VPS? সিদ্ধান্তের প্রশ্নগুলো কী?

প্রথমে মানতে হবে, PHP-র বড় সুবিধাই এখানে। স্থানীয় অনেক hosting প্রতিষ্ঠান cPanel-সহ shared hosting দেয়, যেখানে PHP, MySQL, email আর SSL প্রায়ই প্যানেল থেকেই চালু করা যায়, বিল দেওয়া যায় স্থানীয় পদ্ধতিতে, আর সহায়তা পাওয়া যায় বাংলায়। একজন freelancer বা ছোট এজেন্সির কাছে এটি বড় ব্যাপার। দাম আর সুবিধা প্রতিষ্ঠান ভেদে আলাদা, তাই এখানে কোনো নির্দিষ্ট নাম বা দাম বলা হচ্ছে না।

তারপর প্রশ্নগুলো এক এক করে। প্রথম, PHP version: Laravel 13 চায় 8.3 বা নতুন; প্যানেলে সেটি আছে কি না। দ্বিতীয়, document root: domain-কে project-এর public/-এ দেখানো যায় কি না। না গেলে অনেকে public/-এর ভেতরের জিনিস public_html-এ কপি করে index.php-এর path বদলান, যা চলে কিন্তু ভুলের সুযোগ বেশি। তৃতীয়, SSH আর Composer: না থাকলে vendor/ সহ পুরো project upload করতে হয়, যা ধীর আর ঝামেলার। চতুর্থ, queue: সারাক্ষণ চলা process নিষেধ হলে cron দিয়ে queue:work --stop-when-empty, অর্থাৎ email এক মিনিট পর্যন্ত দেরিতে যেতে পারে। পঞ্চম, CPU আর মেমরির সীমা: ঈদের অফারে ভিড় হলে shared server-এ আপনার অ্যাপ থামিয়ে দেওয়া হতে পারে।

VPS এসব সীমা তুলে দেয়: নিজের PHP, Redis, Supervisor দিয়ে worker, nginx-এর পুরো নিয়ন্ত্রণ। কিন্তু দায়িত্বও আসে: OS হালনাগাদ, firewall, SSH-এর নিরাপত্তা, backup, আর রাত ২টায় server বন্ধ হলে আপনাকেই উঠতে হবে। এর মাঝামাঝি আছে managed platform, যারা VPS-এ Laravel চালানোর কাজ সহজ করে, তবে তার খরচ আর নির্ভরতা আছে।

একটি যুক্তিসঙ্গত পথ: শুরুতে কম ভিড় আর queue-র কম প্রয়োজন থাকলে ভালো একটি shared hosting, document root ঠিক করে আর APP_DEBUG=false দিয়ে। যখন queue-র দেরি, ভিড় বা version-এর সীমা সমস্যা হয়ে দাঁড়ায়, তখন VPS. আর যেটাই হোক, ডেটাবেসের নিয়মিত backup আর তা ফিরিয়ে আনার অনুশীলন।

মূল উপলব্ধি: প্রশ্নটি “কোনটি ভালো” নয়, “আমার অ্যাপের কোন অংশ কোন সীমায় আটকাবে, আর server চালানোর দায়িত্ব কে নেবে”।

প্র ০৫Server-এ HTML বানানো (Blade, Livewire) আর আলাদা React SPA: ২০২০-এর দশকে কেন অনেক দল আবার server-এর দিকে ফিরছে, আর Inertia এই দুইয়ের মাঝে কোথায়?

২০১০-এর দশকে একটি সাধারণ নকশা ছিল: backend শুধু JSON API, আর frontend একটি আলাদা SPA. সুবিধা ছিল পরিষ্কার ভাগ, আর একই API mobile অ্যাপও ব্যবহার করতে পারে। কিন্তু দাম ছিল অনেক: দুটি project, দুটি routing, দুই জায়গায় validation, auth-এর token বা cookie-র জটিলতা, আর প্রথম পাতা দেখাতে বড় JavaScript bundle. পর্ব ০৩-এ এই trade-off দেখেছেন।

এরপর কয়েকটি ধারা প্রায় একসাথে এলো যারা server-এ HTML ফেরত আনছে। Rails-এর Hotwire, Phoenix-এর LiveView, Laravel-এর Livewire, আর React-এর নিজের দিক থেকে Server Component (পর্ব ৫২)। মূল ভাবনা একই: অনেক অ্যাপের বেশিরভাগ পাতা আসলে ফর্ম আর তালিকা, যার জন্য browser-এ পুরো অ্যাপ চালানোর দরকার নেই। Livewire বিশেষ করে সেইসব দলের জন্য, যাঁরা PHP-তে দক্ষ আর JavaScript কম লিখতে চান।

Inertia একটি চতুর মাঝামাঝি পথ। পাতা আঁকে React, Vue বা Svelte, তাই interactive UI-র পুরো শক্তি থাকে। কিন্তু routing, controller, auth আর validation থাকে Laravel-এ, ঠিক Blade অ্যাপের মতো। Controller JSON API বানায় না, পাতার নাম আর props পাঠায়। ফলে আলাদা API ডিজাইন বা token-এর ঝামেলা নেই, session cookie-ই যথেষ্ট। Laravel 13-এর React, Vue আর Svelte starter kit এই পথেই বানানো, যা দেখায় framework-টি নিজে কোন দিকে ঝুঁকছে।

তবে সীমাও আছে। Mobile অ্যাপ বা বাইরের partner-এর জন্য আলাদা API লাগলে Inertia-র পাশাপাশি routes/api.php আর Sanctum লাগবে। Livewire-এ প্রতিটি ক্লিক server পর্যন্ত যায়, তাই ধীর নেটওয়ার্কে (মোবাইল ডেটায় গ্রামের দিকে) অভিজ্ঞতা খারাপ হতে পারে, আর offline প্রায় অসম্ভব। কোনটি বেছে নেবেন, তা নির্ভর করে দলের দক্ষতা, ব্যবহারকারীর নেটওয়ার্ক আর API-র প্রয়োজনের ওপর।

মূল উপলব্ধি: “Server বনাম client” একটি সিদ্ধান্ত, ফ্যাশন নয়। Blade, Livewire, Inertia আর আলাদা SPA একই বর্ণালীর চারটি বিন্দু, প্রতিটির নিজস্ব খরচ।

অনুশীলন

হিসাব, command লেখা, নকশা বিচার আর ফল আগে থেকে বলা, চার রকম কাজ আছে।

  1. Numerical: বাজারঘরের তালিকার পাতায় ২০টি পণ্য দেখানো হয়। প্রতিটি পণ্যের একটি বিভাগ আর একটি brand আছে (দুটি belongsTo সম্পর্ক)। ধরুন প্রতিটি query-তে গড়ে ২ ms লাগে। (ক) কোনো with() ছাড়া পাতায় কয়টি query, আর মোট কত সময়? (খ) with(['category', 'brand']) দিলে? (গ) পাতায় ১০০টি পণ্য হলে দুটির সংখ্যা কত? (ঘ) প্রতি সেকেন্ডে ৫০টি পাতার request এলে ডেটাবেস সেকেন্ডে কয়টি query পায়, দুই ক্ষেত্রে?

    (ক) ১টি পণ্যের তালিকা + ২০টি বিভাগ + ২০টি brand = ৪১টি query, ৪১ × ২ = ৮২ ms.

    (খ) ১টি পণ্য + ১টি বিভাগ (where id in …) + ১টি brand = ৩টি, ৩ × ২ = ৬ ms.

    (গ) Lazy: ১ + ১০০ + ১০০ = ২০১টি. Eager: এখনো ৩টি. সূত্র: lazy = ১ + N × (সম্পর্কের সংখ্যা), eager = ১ + সম্পর্কের সংখ্যা।

    (ঘ) ২০টির পাতায় lazy: ৫০ × ৪১ = ২,০৫০টি query/সেকেন্ড. Eager: ৫০ × ৩ = ১৫০টি. প্রায় ১৪ গুণ কম চাপ, একই পাতা।

  2. Command লেখা: বাজারঘরে ক্রেতার review যোগ করতে চান। artisan-এর কোন command-গুলো চালাবেন যাতে তৈরি হয়: Review model, তার migration, factory আর seeder, একটি API controller, নতুন review-এর জন্য FormRequest, আর একটি feature test? তারপর কোন command-এ শুধু নতুন migration চলবে (পুরোনো ডেটা না মুছে), আর কোন command-এ নিশ্চিত হবেন নতুন route তৈরি হয়েছে?
    php artisan make:model Review -mfs
    php artisan make:controller ReviewController --api
    php artisan make:request StoreReviewRequest
    php artisan make:test ReviewApiTest
    php artisan migrate                      # শুধু বাকি migration, ডেটা থাকে
    php artisan route:list --path=reviews

    -mfs মানে migration, factory, seeder. --api এমন resource controller বানায় যেখানে HTML form-এর create আর edit method নেই। migrate:fresh এখানে ভুল উত্তর, কারণ সেটি সব table মুছে দেয়। Route যোগ করতে routes/api.php-এ Route::apiResource('reviews', ReviewController::class); লিখতে হবে, তারপর route:list-এ দেখবেন।

  3. নকশা বিচার: একজন freelancer বাজারঘরের admin অংশ এভাবে বানিয়েছেন: (১) Product model-এ protected $guarded = [];, (২) controller-এ Product::create($request->all());, (৩) পণ্যের পাতায় {!! $product->description !!}, যেখানে description যেকোনো বিক্রেতা লিখতে পারেন, (৪) পুরো project public_html-এ, (৫) .env-এ APP_DEBUG=true, (৬) তালিকার পাতায় Product::all() আর লুপে $p->category->name. প্রতিটির সমস্যা আর সমাধান লিখুন।

    (১) আর (২) মিলে mass assignment পুরো খোলা: id, price বা অন্য যেকোনো column বসানো যায়। সমাধান: সংকীর্ণ $fillable, FormRequest, আর create($request->validated()).

    (৩) Stored XSS: একজন বিক্রেতা script রাখলে প্রতিটি দর্শকের browser-এ চলবে। সমাধান: {{ }}; formatting লাগলে sanitizer আর CSP.

    (৪) .env আর log-এর মতো ফাইল browser থেকে পৌঁছানোর ঝুঁকি। সমাধান: project public_html-এর বাইরে, document root হবে public/.

    (৫) যেকোনো error-এ server-এর path, package আর stack trace ফাঁস, এই পর্বের 404-এর run-এর মতো। সমাধান: APP_ENV=production, APP_DEBUG=false.

    (৬) N+1: N পণ্যে N+1 query. সমাধান: Product::with('category'), আর development-এ preventLazyLoading. সাথে all()-এর বদলে paginate(), নইলে হাজার পণ্য একবারে আসবে।

  4. ফল আগে থেকে বলুন: একটি Laravel অ্যাপের app time zone UTC. (ক) Category model-এ $fillable = ['name']. (new Category)->fill(['name' => 'Snacks', 'id' => 50, 'slug' => 'x'])->getAttributes()-কে json_encode করলে কী ছাপা হবে? (খ) Blade-এ <b>{{ $t }}</b>, যেখানে $t = 'চা & বিস্কুট <নতুন>', কোন HTML বের হবে? (গ) Schedule::command('report')->dailyAt('21:30')->timezone('Asia/Dhaka') হলে schedule:list-এ cron expression কী দেখাবে?

    (ক) {"name":"Snacks"}. শুধু $fillable-এর key থাকে, id আর slug চুপচাপ বাদ। আসল run-এ is_admin-এর ক্ষেত্রেও তাই হয়েছে।

    (খ) <b>চা &amp; বিস্কুট &lt;নতুন&gt;</b>. বাইরের <b> template-এর অংশ, তাই অক্ষত। ভেতরের মানের &, <, > escape হয়, বাংলা অক্ষর যেমন ছিল তেমন।

    (গ) ঢাকার ২১:৩০ = UTC-র ১৫:৩০, তাই 30 15 * * *. আসল run-এ ০৯:০০ দেখিয়েছিল 0 3 * * *, একই ৬ ঘণ্টার পার্থক্য।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমLaravel-এ POST /api/products request-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Web server request-টি public/index.php-এ পাঠায়
  2. Global middleware-এর স্তর পেরোয়
  3. Router routes/api.php-এর route মেলায়
  4. FormRequest নিয়ম যাচাই করে
  5. Controller validated() ডেটা দিয়ে Eloquent-এ create করে
  6. 201 আর নতুন পণ্যের JSON ফেরত যায়

একটি Laravel অ্যাপে ৬টি পণ্য আছে, প্রতিটির একটি বিভাগ। foreach (Product::all() as $p) { echo $p->category->name; } চালালে মোট কয়টি SQL query যাবে?

  • ১টি
  • ২টি
  • ৭টি
  • ৪টি

Blade template-এ <p>{{ $review }}</p> লেখা, আর $review-এর মান <script>steal()</script>. Browser-এ কী ঘটবে?

  • Script চলবে
  • Script লেখা হিসেবে দেখাবে, চলবে না
  • অনুচ্ছেদটি ফাঁকা থাকবে
  • পাতা 500 error দেবে

Eloquent model-এ protected $fillable = ['name', 'price'];. $p->fill(['name' => 'Tea', 'price' => 100, 'is_admin' => true]) চালানোর পরে attribute-এ কী থাকবে?

  • name, price আর is_admin
  • MassAssignmentException, কিছুই বসবে না
  • কিছুই না
  • শুধু name আর price

একটি JSON request-এ (Accept: application/json) Laravel-এর FormRequest validation ব্যর্থ হলে ডিফল্টে কী ফেরত যায়?

  • 422, সাথে message আর field-ভিত্তিক errors
  • 400, কোনো বিস্তারিত ছাড়া
  • 302 redirect আগের পাতায়
  • 500, stack trace-সহ

cPanel-এর shared hosting-এ একটি Laravel অ্যাপ বসাতে হবে। Domain-এর document root কোন ফোল্ডার হওয়া উচিত?

  • Project-এর মূল ফোল্ডার
  • storage/
  • public/
  • resources/

Laravel 13-এর official docs অনুযায়ী এটি চালাতে PHP-র সর্বনিম্ন কোন version লাগে?

  • 8.1
  • 8.3
  • 8.5
  • 7.4

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আধুনিক PHP আর Laravel: type, attribute, Composer আর artisan. Laravel 13 চায় PHP 8.3 বা নতুন; route, controller, Eloquent, Blade, validation, queue, scheduler সব এক বাক্সে।
  2. Eloquent হলো Active Record: save() নিজেই insert বা update লেখে, মান যায় binding-এ। Lazy loading-এ ৬টি পণ্যে ৭টি query, with()-এ ২টি।
  3. দুই স্তরের পাহারা: সংকীর্ণ $fillable আর FormRequest-এর validated(). ভুল ইনপুটে 422, সাথে field ধরে errors.
  4. Blade: {{ }} escape করে, {!! !!} করে না। ব্যবহারকারীর লেখা কখনো raw নয়।
  5. Deploy: document root public/, APP_DEBUG=false, migrate --force, scheduler-এর জন্য একটি cron লাইন। Shared hosting-এ queue আর SSH-এর সীমা দেখে নিন।

পরের পর্বে: পর্ব ৭১-এ Spring Boot আর Rails: enterprise Java আর convention over configuration. একই বাজারঘরের পণ্য, একই N+1, আর validation-এ 400 বনাম 422-এর পার্থক্য দেখবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান