This commit is contained in:
2026-05-13 17:11:09 +02:00
commit ea63897455
2785 changed files with 359868 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
---
name: cpad-plugin-development
description: 'Use when creating, reviewing, or refactoring cPad plugins. Trigger for plugin module structure, ServiceProvider registration, route layout, admin page headers, setup controllers, DataTables usage, Blade components, translations, migrations, models, and plugin-specific conventions for packages under cPad\Plugins\*.'
license: MIT
metadata:
author: copilot
---
# cPad Plugin Development
Use this skill when building new cPad plugins or updating existing ones. Follow the patterns already used by the plugin you are touching, especially the Blocks module.
## Core Principle
Prefer the established cPad plugin conventions over generic Laravel defaults when both are plausible. A plugin should feel native to the admin panel, not like a standalone Laravel package.
If a pattern already exists in the target plugin or a sibling plugin, keep it. Use this skill to standardize the parts that are repeated across plugins, not to invent new architecture.
## Plugin Structure
A typical plugin should have this shape:
- `composer.json` for package metadata and dependencies
- `manifest.json` for plugin metadata, permissions, version, and entry point
- `ServiceProvider.php` for bootstrapping
- `Controllers/` for admin actions and setup flows
- `Controllers/Traits/` for reusable controller logic
- `Models/` for Eloquent models and revision models
- `Migrations/` for plugin-owned schema
- `Routes/routes.php` for admin routes
- `Resources/views/` for Blade views, forms, setup screens, and components
- `Components/` for Blade components exposed to frontend templates
- `Services/` for business logic that should not live in controllers
Keep plugin code namespaced under `cPad\Plugins\<PluginName>\`.
## Service Provider Rules
Use the service provider to wire the plugin into cPad:
- Load migrations from the plugin `Migrations/` directory
- Load views with a plugin namespace like `plugin.<slug>`
- Register Blade components when the plugin exposes reusable frontend markup
- Register the route file from `register()`
- Add the plugin menu entry in the admin panel if the plugin has a visible admin area
Keep registration logic minimal and push behavior into controllers, services, or views.
## Routing Rules
Use a grouped route file with the admin middleware and the cPad webroot prefix.
Preferred shape:
- Middleware: `admin`
- Prefix: `config('cp.webroot', 'cp')`
- Route names: `admin.plugin.<slug>.*`
- Setup routes under a dedicated `setup` prefix
- Use POST for mutating actions and GET for page rendering
Keep route names predictable so Blade views and JavaScript can reference them consistently.
## Admin Header Pattern
Use a shared header partial for plugin pages.
The Blocks module shows a good pattern:
- Extend the admin layout
- Include the plugin header partial inside the content section
- Render a page header component with slots for logo, main route, add route, middle actions, and append actions
- Use `showSaveBtn` and `hideAddBtn` flags where a page needs to alter the header controls
- Keep `header.blade.php` as the shared admin header for the plugin, not a page-specific header
- Reuse the same header partial in create, edit, layout, setup, and list pages when the navigation/actions are shared
For page wrappers:
- `create.blade.php` and `edit.blade.php` should usually be thin wrappers
- Both should extend the admin layout, include the shared header, and then include the shared form partial
- The only difference is usually the form action route and, for edit pages, the bound model data
- Keep form fields in a shared `form.blade.php` or segmented partials so create/edit stay in sync
Recommended header behavior:
- Keep the header partial small and reusable
- Put navigation actions in the header, not scattered across page bodies
- Expose setup, layout, or diagnostic actions as middle/header buttons when they are part of the plugin workflow
## Views and Page Layout
For admin pages:
- Extend `admin::layout.default`
- Wrap content in `<x-page-layout>` or `<x-card>` where appropriate
- Use plugin view namespaces such as `plugin.block::main`
- Use `@section('header-addon')` for page-specific CSS
- Use `@section('footer-addon')` for page-specific JavaScript
For forms:
- Split large forms into partials like settings, translations, revisions, or layout tabs
- Prefer small includes for repeated form groups
- Keep Blade views focused on markup and binding, not business logic
## DataTables Rule
Blocks includes DataTables assets, but the table itself is still rendered server-side. That means the important rule is:
- Do not add DataTables by default just because it exists in the admin theme
- Use it only when you need client-side search, sort, or paging
- If you include the DataTables assets, initialize the table explicitly in the footer script
- If a table is simple, a plain Blade table is usually better and easier to maintain
When DataTables is used:
- Load CSS in `header-addon`
- Load JS in `footer-addon`
- Keep markup semantic and compatible with the admin table classes
- Avoid duplicating behavior already handled by server-side filters
## Setup Flow
Use a dedicated setup controller for plugin configuration.
Rules:
- Restrict setup actions with `Security::check(...)`
- Store plugin settings through the cPad config service, usually as JSON
- Keep long-lived configuration separate from page-specific form state
- If the plugin has editable layout fragments such as header or footer HTML, store them as separate config keys
- Provide a setup screen with tabs or sections when the configuration is more than a few fields
The Blocks module pattern is useful here: one config payload for plugin options and a separate layout payload for header/footer HTML.
## Model Rules
Follow the existing database shape instead of forcing conventional Laravel naming when the plugin schema is already established.
Common patterns in Blocks:
- Custom primary keys like `block_id`
- Explicit table names when they do not follow Laravel defaults
- `fillable` defined for all mass-assigned fields
- Cast JSON columns to `array`
- Cast booleans explicitly
- Use local scopes for repeated filters such as active records or group filters
If the plugin stores language-specific content in JSON, expose a helper method or accessor for the current locale rather than repeating array lookups everywhere.
## Migration Rules
Plugin migrations should be easy to read and easy to reverse.
- Use a plugin-specific prefix in migration filenames
- Create plugin-owned tables only
- Add indexes where the plugin filters or sorts frequently
- Include foreign keys for revision tables or parent-child tables
- Keep `down()` reversible unless the change is intentionally one-way
For revision-style tables:
- Store the original record ID
- Keep the revision table aligned with the main tables key fields
- Use cascading delete only when revision history should disappear with the source record
## Controller Rules
Keep controllers thin.
- Put data prep in private helpers or services
- Use traits only when the same logic is reused across controller actions
- Validate input before touching the database
- Use transactions for multi-step writes or revision snapshots
- Return redirects for admin form submissions and arrays/JSON only for AJAX endpoints
Blocks shows a practical controller split:
- CRUD and page rendering in the main controller
- setup logic in a dedicated controller
- editor integration in a separate controller
- shared mutation logic in a trait
## Revisions and Auditing
If the plugin needs change history:
- Snapshot the previous record before updating
- Normalize data before comparing it so cosmetic ordering does not create false diffs
- Store timestamps on the revision record
- Keep the diff logic in one place
## Frontend Components
For reusable frontend output:
- Expose a Blade component with a small constructor API
- Keep the component read-only
- Resolve content by keycode or slug and optional store/language context
- Return raw HTML only when the plugin intentionally manages HTML content
If debug mode is useful, gate it behind a config or environment check and keep the debug overlay unobtrusive.
## Blade and Content Safety
Be careful with encoded content.
- Decode stored HTML only at the boundary where it needs to be displayed or edited
- Avoid double-encoding content when round-tripping through the editor
- Preserve raw HTML when the plugin is designed to store HTML fragments
- If content is user-authored, validate and sanitize deliberately instead of assuming Blade escaping will handle it
## Naming Conventions
Use predictable names:
- View namespace: `plugin.<slug>::...`
- Route name prefix: `admin.plugin.<slug>.`
- Config keys: `plugin.<slug>.config`, `plugin.<slug>.layout`
- Plugin class namespace: `cPad\Plugins\<PluginName>\...`
- Admin menu label and plugin title should match the human-facing module name
## What to Check Before Writing a New Plugin
Before starting a new cPad plugin, confirm:
- What the plugins main content model is
- Whether it needs revisions or history
- Whether it needs setup/configuration screens
- Whether it needs header/footer HTML injection
- Whether it needs store, language, or site scoping
- Whether it needs a reusable Blade component
- Whether the admin list can stay server-rendered or needs DataTables
## Output Expectations
When asked to create a new plugin, produce:
- A clear folder structure
- A service provider that registers the plugin cleanly
- Routes grouped by admin prefix
- Admin pages with a shared header and consistent layout
- A setup screen if the plugin has configuration
- Model and migration conventions that match the plugins schema
## Blocks Module Notes
The Blocks plugin is the canonical example in this repo for:
- A custom admin header partial
- A setup controller with config persistence
- Translation tabs and editor-based content entry
- Optional store scoping
- Revision tracking
- A Blade component for frontend rendering
Use it as the reference implementation when a future plugin has similar needs.
## Starter Templates
Use these as the default starting point for a new plugin and then adapt them to the plugin's domain.
### Service Provider
```php
<?php
namespace cPad\Plugins\Example;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Klevze\ControlPanel\Framework\Core\Menu;
class ServiceProvider extends LaravelServiceProvider
{
public function boot(Menu $menu): void
{
$this->loadMigrationsFrom(__DIR__ . '/Migrations');
$this->loadViewsFrom(__DIR__ . '/Resources/views', 'plugin.example');
Blade::component('example', \cPad\Plugins\Example\Components\ExampleComponent::class);
$menu->addItem(trans('admin.CONTENT'), trans('admin.EXAMPLE'), 'fa fa-th-large', 'admin.plugin.example.main');
}
public function register(): void
{
require __DIR__ . '/Routes/routes.php';
}
}
```
### Routes
```php
<?php
use Illuminate\Support\Facades\Route;
use cPad\Plugins\Example\Controllers\ExampleController;
use cPad\Plugins\Example\Controllers\SetupController;
Route::group(['middleware' => 'admin', 'as' => 'admin.plugin.example.', 'prefix' => config('cp.webroot', 'cp') . '/content/example'], function () {
Route::get('/', [ExampleController::class, 'main'])->name('main');
Route::get('create', [ExampleController::class, 'create'])->name('create');
Route::get('edit/{id}', [ExampleController::class, 'edit'])->name('edit');
Route::post('insert', [ExampleController::class, 'insert'])->name('insert');
Route::post('update/{id}', [ExampleController::class, 'update'])->name('update');
Route::group(['as' => 'setup.', 'prefix' => 'setup'], function () {
Route::get('/', [SetupController::class, 'setup'])->name('main');
Route::post('update', [SetupController::class, 'update'])->name('update');
});
});
```
### Shared Header
```blade
<x-page-header :sticky="true">
<x-slot name="logo">
{{ config('cp.admin_path') }}/images/icons/example.png
</x-slot>
<x-slot name="mainRoute">
{{ route('admin.plugin.example.main') }}
</x-slot>
@unless(isset($hideAddBtn))
<x-slot name="addRoute">
{{ route('admin.plugin.example.create') }}
</x-slot>
@endunless
@if(isset($showSaveBtn))
<x-slot name="append">
<button type="submit" class="btn btn-flat bg-indigo saveForm">
<i class="fa fa-save fa-fw"></i>
{{ trans('admin.SAVE') }}
</button>
</x-slot>
@endif
<x-slot name="title">
{{ trans('admin.EXAMPLE') }}
</x-slot>
</x-page-header>
```
### Create and Edit Wrappers
```blade
@extends('admin::layout.default')
@section('content')
@include('plugin.example::header', ['showSaveBtn' => true, 'hideAddBtn' => true])
@include('plugin.example::form', ['urlRoute' => route('admin.plugin.example.insert')])
@endsection
```
```blade
@extends('admin::layout.default')
@section('content')
@include('plugin.example::header', ['showSaveBtn' => true, 'hideAddBtn' => true])
@include('plugin.example::form', ['urlRoute' => route('admin.plugin.example.update', $table->id)])
@endsection
```

View File

@@ -0,0 +1,190 @@
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
author: laravel
---
# Laravel Best Practices
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
## Consistency First
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
## Quick Reference
### 1. Database Performance → `rules/db-performance.md`
- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
### 3. Security → `rules/security.md`
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
### 4. Caching → `rules/caching.md`
- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production
### 5. Eloquent Patterns → `rules/eloquent.md`
- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
### 6. Validation & Forms → `rules/validation.md`
- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`
### 7. Configuration → `rules/config.md`
- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text
### 8. Testing Patterns → `rules/testing.md`
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios
### 10. Routing & Controllers → `rules/routing.md`
- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation
### 11. HTTP Client → `rules/http-client.md`
- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails
### 13. Error Handling → `rules/error-handling.md`
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes
### 14. Task Scheduling → `rules/scheduling.md`
- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration
### 15. Architecture → `rules/architecture.md`
- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
### 16. Migrations → `rules/migrations.md`
- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML
### 17. Collections → `rules/collections.md`
- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections
### 18. Blade & Views → `rules/blade-views.md`
- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props
### 19. Conventions & Style → `rules/style.md`
- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files
## How to Apply
Always use a sub-agent to read rule files and explore this skill's content.
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version

View File

@@ -0,0 +1,106 @@
# Advanced Query Patterns
## Use `addSelect()` Subqueries for Single Values from Has-Many
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
```php
public function scopeWithLastLoginAt($query): void
{
$query->addSelect([
'last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->withCasts(['last_login_at' => 'datetime']);
}
```
## Create Dynamic Relationships via Subquery FK
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
```php
public function lastLogin(): BelongsTo
{
return $this->belongsTo(Login::class);
}
public function scopeWithLastLogin($query): void
{
$query->addSelect([
'last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}
```
## Use Conditional Aggregates Instead of Multiple Count Queries
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
```php
$statuses = Feature::toBase()
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
->first();
```
## Use `setRelation()` to Prevent Circular N+1
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
```php
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
```
## Prefer `whereIn` + Subquery Over `whereHas`
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
Incorrect (correlated EXISTS re-executes per row):
```php
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
```
Correct (index-friendly subquery, no PHP memory overhead):
```php
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
```
## Sometimes Two Simple Queries Beat One Complex Query
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
## Use Compound Indexes Matching `orderBy` Column Order
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
```php
// Migration
$table->index(['last_name', 'first_name']);
// Query — column order must match the index
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
```
## Use Correlated Subqueries for Has-Many Ordering
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
```php
public function scopeOrderByLastLogin($query): void
{
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
```

View File

@@ -0,0 +1,202 @@
# Architecture Best Practices
## Single-Purpose Action Classes
Extract discrete business operations into invokable Action classes.
```php
class CreateOrderAction
{
public function __construct(private InventoryService $inventory) {}
public function execute(array $data): Order
{
$order = Order::create($data);
$this->inventory->reserve($order);
return $order;
}
}
```
## Use Dependency Injection
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
Incorrect:
```php
class OrderController extends Controller
{
public function store(StoreOrderRequest $request)
{
$service = app(OrderService::class);
return $service->create($request->validated());
}
}
```
Correct:
```php
class OrderController extends Controller
{
public function __construct(private OrderService $service) {}
public function store(StoreOrderRequest $request)
{
return $this->service->create($request->validated());
}
}
```
## Code to Interfaces
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
Incorrect (concrete dependency):
```php
class OrderService
{
public function __construct(private StripeGateway $gateway) {}
}
```
Correct (interface dependency):
```php
interface PaymentGateway
{
public function charge(int $amount, string $customerId): PaymentResult;
}
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
}
```
Bind in a service provider:
```php
$this->app->bind(PaymentGateway::class, StripeGateway::class);
```
## Default Sort by Descending
When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
Incorrect:
```php
$posts = Post::paginate();
```
Correct:
```php
$posts = Post::latest()->paginate();
```
## Use Atomic Locks for Race Conditions
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
```php
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
$order->process();
});
// Or at query level
$product = Product::where('id', $id)->lockForUpdate()->first();
```
## Use `mb_*` String Functions
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
Incorrect:
```php
strlen('José'); // 5 (bytes, not characters)
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
```
Correct:
```php
mb_strlen('José'); // 4 (characters)
mb_strtolower('MÜNCHEN'); // 'münchen'
// Prefer Laravel's Str helpers when available
Str::length('José'); // 4
Str::lower('MÜNCHEN'); // 'münchen'
```
## Use `defer()` for Post-Response Work
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
Incorrect (job overhead for trivial work):
```php
dispatch(new LogPageView($page));
```
Correct (runs after response, same process):
```php
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
```
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
## Use `Context` for Request-Scoped Data
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
```php
// In middleware
Context::add('tenant_id', $request->header('X-Tenant-ID'));
// Anywhere later — controllers, jobs, log context
$tenantId = Context::get('tenant_id');
```
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
## Use `Concurrency::run()` for Parallel Execution
Run independent operations in parallel using child processes — no async libraries needed.
```php
use Illuminate\Support\Facades\Concurrency;
[$users, $orders] = Concurrency::run([
fn () => User::count(),
fn () => Order::where('status', 'pending')->count(),
]);
```
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
## Convention Over Configuration
Follow Laravel conventions. Don't override defaults unnecessarily.
Incorrect:
```php
class Customer extends Model
{
protected $table = 'Customer';
protected $primaryKey = 'customer_id';
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
}
}
```
Correct:
```php
class Customer extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
```

View File

@@ -0,0 +1,36 @@
# Blade & Views Best Practices
## Use `$attributes->merge()` in Component Templates
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
```blade
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
```
## Use `@pushOnce` for Per-Component Scripts
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
## Prefer Blade Components Over `@include`
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
## Use View Composers for Shared View Data
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
A single view can return either the full page or just a fragment, keeping routing clean.
```php
return view('dashboard', compact('users'))
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
```
## Use `@aware` for Deeply Nested Component Props
Avoids re-passing parent props through every level of nested components.

View File

@@ -0,0 +1,70 @@
# Caching Best Practices
## Use `Cache::remember()` Instead of Manual Get/Put
Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
Incorrect:
```php
$val = Cache::get('stats');
if (! $val) {
$val = $this->computeStats();
Cache::put('stats', $val, 60);
}
```
Correct:
```php
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
```
## Use `Cache::flexible()` for Stale-While-Revalidate
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
## Use Cache Tags to Invalidate Related Groups
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
```php
Cache::tags(['user-1'])->flush();
```
## Use `Cache::add()` for Atomic Conditional Writes
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
Correct: `Cache::add('lock', true, 10);`
## Use `once()` for Per-Request Memoization
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
```php
public function roles(): Collection
{
return once(fn () => $this->loadRoles());
}
```
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
## Configure Failover Cache Stores in Production
If Redis goes down, the app falls back to a secondary store automatically.
```php
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
```

View File

@@ -0,0 +1,44 @@
# Collection Best Practices
## Use Higher-Order Messages for Simple Operations
Incorrect:
```php
$users->each(function (User $user) {
$user->markAsVip();
});
```
Correct: `$users->each->markAsVip();`
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
## Choose `cursor()` vs. `lazy()` Correctly
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
## Use `lazyById()` When Updating Records While Iterating
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
## Use `toQuery()` for Bulk Operations on Collections
Avoids manual `whereIn` construction.
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
Correct: `$users->toQuery()->update([...]);`
## Use `#[CollectedBy]` for Custom Collection Classes
More declarative than overriding `newCollection()`.
```php
#[CollectedBy(UserCollection::class)]
class User extends Model {}
```

View File

@@ -0,0 +1,73 @@
# Configuration Best Practices
## `env()` Only in Config Files
Direct `env()` calls may return `null` when config is cached.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'key' => env('API_KEY'),
// Application code
$key = config('services.key');
```
## Use Encrypted Env or External Secrets
Never store production secrets in plain `.env` files in version control.
Incorrect:
```bash
# .env committed to repo or shared in Slack
STRIPE_SECRET=sk_live_abc123
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
```
Correct:
```bash
php artisan env:encrypt --env=production --readable
php artisan env:decrypt --env=production
```
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
## Use `App::environment()` for Environment Checks
Incorrect:
```php
if (env('APP_ENV') === 'production') {
```
Correct:
```php
if (app()->isProduction()) {
// or
if (App::environment('production')) {
```
## Use Constants and Language Files
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
```php
// Incorrect
return $this->type === 'normal';
// Correct
return $this->type === self::TYPE_NORMAL;
```
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
```php
// Only when lang files already exist in the project
return back()->with('message', __('app.article_added'));
```

View File

@@ -0,0 +1,192 @@
# Database Performance Best Practices
## Always Eager Load Relationships
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
Incorrect (N+1 — executes 1 + N queries):
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Correct (2 queries total):
```php
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Constrain eager loads to select only needed columns (always include the foreign key):
```php
$users = User::with(['posts' => function ($query) {
$query->select('id', 'user_id', 'title')
->where('published', true)
->latest()
->limit(10);
}])->get();
```
## Prevent Lazy Loading in Development
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
```php
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
```
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
## Select Only Needed Columns
Avoid `SELECT *` — especially when tables have large text or JSON columns.
Incorrect:
```php
$posts = Post::with('author')->get();
```
Correct:
```php
$posts = Post::select('id', 'title', 'user_id', 'created_at')
->with(['author:id,name,avatar'])
->get();
```
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
## Chunk Large Datasets
Never load thousands of records at once. Use chunking for batch processing.
Incorrect:
```php
$users = User::all();
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
```
Correct:
```php
User::where('subscribed', true)->chunk(200, function ($users) {
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
});
```
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
```php
User::where('active', false)->chunkById(200, function ($users) {
$users->each->delete();
});
```
## Add Database Indexes
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index()->constrained();
$table->string('status')->index();
$table->timestamps();
$table->index(['status', 'created_at']);
});
```
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
## Use `withCount()` for Counting Relations
Never load entire collections just to count them.
Incorrect:
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
```
Correct:
```php
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
```
Conditional counting:
```php
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => function ($query) {
$query->where('approved', true);
},
])->get();
```
## Use `cursor()` for Memory-Efficient Iteration
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
Incorrect:
```php
$users = User::where('active', true)->get();
```
Correct:
```php
foreach (User::where('active', true)->cursor() as $user) {
ProcessUser::dispatch($user->id);
}
```
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
## No Queries in Blade Templates
Never execute queries in Blade templates. Pass data from controllers.
Incorrect:
```blade
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
```
Correct:
```php
// Controller
$users = User::with('profile')->get();
return view('users.index', compact('users'));
```
```blade
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
```

View File

@@ -0,0 +1,148 @@
# Eloquent Best Practices
## Use Correct Relationship Types
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
```php
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
```
## Use Local Scopes for Reusable Queries
Extract reusable query constraints into local scopes to avoid duplication.
Incorrect:
```php
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
$articles = Article::whereHas('user', function ($q) {
$q->where('verified', true)->whereNotNull('activated_at');
})->get();
```
Correct:
```php
public function scopeActive(Builder $query): Builder
{
return $query->where('verified', true)->whereNotNull('activated_at');
}
// Usage
$active = User::active()->get();
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
```
## Apply Global Scopes Sparingly
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
Incorrect (global scope for a conditional filter):
```php
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('published', true);
}
}
// Now admin panels, reports, and background jobs all silently skip drafts
```
Correct (local scope you opt into):
```php
public function scopePublished(Builder $query): Builder
{
return $query->where('published', true);
}
Post::published()->paginate(); // Explicit
Post::paginate(); // Admin sees all
```
## Define Attribute Casts
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
```php
protected function casts(): array
{
return [
'is_active' => 'boolean',
'metadata' => 'array',
'total' => 'decimal:2',
];
}
```
## Cast Date Columns Properly
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
Incorrect:
```blade
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
```
Correct:
```php
protected function casts(): array
{
return [
'ordered_at' => 'datetime',
];
}
```
```blade
{{ $order->ordered_at->toDateString() }}
{{ $order->ordered_at->format('m-d') }}
```
## Use `whereBelongsTo()` for Relationship Queries
Cleaner than manually specifying foreign keys.
Incorrect:
```php
Post::where('user_id', $user->id)->get();
```
Correct:
```php
Post::whereBelongsTo($user)->get();
Post::whereBelongsTo($user, 'author')->get();
```
## Avoid Hardcoded Table Names in Queries
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
Incorrect:
```php
DB::table('users')->where('active', true)->get();
$query->join('companies', 'companies.id', '=', 'users.company_id');
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
```
Correct — reference the model's table:
```php
DB::table((new User)->getTable())->where('active', true)->get();
// Even better — use Eloquent or the query builder instead of raw SQL
User::where('active', true)->get();
Order::where('status', 'pending')->get();
```
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.

View File

@@ -0,0 +1,72 @@
# Error Handling Best Practices
## Exception Reporting and Rendering
There are two valid approaches — choose one and apply it consistently across the project.
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
```php
class InvalidOrderException extends Exception
{
public function report(): void { /* custom reporting */ }
public function render(Request $request): Response
{
return response()->view('errors.invalid-order', status: 422);
}
}
```
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
```php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
$exceptions->render(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', status: 422);
});
})
```
Check the existing codebase and follow whichever pattern is already established.
## Use `ShouldntReport` for Exceptions That Should Never Log
More discoverable than listing classes in `dontReport()`.
```php
class PodcastProcessingException extends Exception implements ShouldntReport {}
```
## Throttle High-Volume Exceptions
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
## Enable `dontReportDuplicates()`
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
## Force JSON Error Rendering for API Routes
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
```php
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
return $request->is('api/*') || $request->expectsJson();
});
```
## Add Context to Exception Classes
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
```php
class InvalidOrderException extends Exception
{
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
```

View File

@@ -0,0 +1,52 @@
# Events & Notifications Best Practices
## Rely on Event Discovery
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
## Run `event:cache` in Production Deploy
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
## Use `ShouldDispatchAfterCommit` Inside Transactions
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
```php
class OrderShipped implements ShouldDispatchAfterCommit {}
```
## Always Queue Notifications
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
```php
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
}
```
## Use `afterCommit()` on Notifications in Transactions
Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits.
```php
$user->notify((new InvoicePaid($invoice))->afterCommit());
```
## Route Notification Channels to Dedicated Queues
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
## Use On-Demand Notifications for Non-User Recipients
Avoid creating dummy models to send notifications to arbitrary addresses.
```php
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
```
## Implement `HasLocalePreference` on Notifiable Models
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.

View File

@@ -0,0 +1,160 @@
# HTTP Client Best Practices
## Always Set Explicit Timeouts
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
Incorrect:
```php
$response = Http::get('https://api.example.com/users');
```
Correct:
```php
$response = Http::timeout(5)
->connectTimeout(3)
->get('https://api.example.com/users');
```
For service-specific clients, define timeouts in a macro:
```php
Http::macro('github', function () {
return Http::baseUrl('https://api.github.com')
->timeout(10)
->connectTimeout(3)
->withToken(config('services.github.token'));
});
$response = Http::github()->get('/repos/laravel/framework');
```
## Use Retry with Backoff for External APIs
External APIs have transient failures. Use `retry()` with increasing delays.
Incorrect:
```php
$response = Http::post('https://api.stripe.com/v1/charges', $data);
if ($response->failed()) {
throw new PaymentFailedException('Charge failed');
}
```
Correct:
```php
$response = Http::retry([100, 500, 1000])
->timeout(10)
->post('https://api.stripe.com/v1/charges', $data);
```
Only retry on specific errors:
```php
$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
return $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError());
})->post('https://api.example.com/data');
```
## Handle Errors Explicitly
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
Incorrect:
```php
$response = Http::get('https://api.example.com/users/1');
$user = $response->json(); // Could be an error body
```
Correct:
```php
$response = Http::timeout(5)
->get('https://api.example.com/users/1')
->throw();
$user = $response->json();
```
For graceful degradation:
```php
$response = Http::get('https://api.example.com/users/1');
if ($response->successful()) {
return $response->json();
}
if ($response->notFound()) {
return null;
}
$response->throw();
```
## Use Request Pooling for Concurrent Requests
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
Incorrect:
```php
$users = Http::get('https://api.example.com/users')->json();
$posts = Http::get('https://api.example.com/posts')->json();
$comments = Http::get('https://api.example.com/comments')->json();
```
Correct:
```php
use Illuminate\Http\Client\Pool;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('users')->get('https://api.example.com/users'),
$pool->as('posts')->get('https://api.example.com/posts'),
$pool->as('comments')->get('https://api.example.com/comments'),
]);
$users = $responses['users']->json();
$posts = $responses['posts']->json();
```
## Fake HTTP Calls in Tests
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
Incorrect:
```php
it('syncs user from API', function () {
$service = new UserSyncService;
$service->sync(1); // Hits the real API
});
```
Correct:
```php
it('syncs user from API', function () {
Http::preventStrayRequests();
Http::fake([
'api.example.com/users/1' => Http::response([
'name' => 'John Doe',
'email' => 'john@example.com',
]),
]);
$service = new UserSyncService;
$service->sync(1);
Http::assertSent(function (Request $request) {
return $request->url() === 'https://api.example.com/users/1';
});
});
```
Test failure scenarios too:
```php
Http::fake([
'api.example.com/*' => Http::failedConnection(),
]);
```

View File

@@ -0,0 +1,27 @@
# Mail Best Practices
## Implement `ShouldQueue` on the Mailable Class
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
## Use `afterCommit()` on Mailables Inside Transactions
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
Correct: `Mail::assertQueued(OrderShipped::class);`
## Use Markdown Mailables for Transactional Emails
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
## Separate Content Tests from Sending Tests
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
Don't mix them — it conflates concerns and makes tests brittle.

View File

@@ -0,0 +1,121 @@
# Migration Best Practices
## Generate Migrations with Artisan
Always use `php artisan make:migration` for consistent naming and timestamps.
Incorrect (manually created file):
```php
// database/migrations/posts_migration.php ← wrong naming, no timestamp
```
Correct (Artisan-generated):
```bash
php artisan make:migration create_posts_table
php artisan make:migration add_slug_to_posts_table
```
## Use `constrained()` for Foreign Keys
Automatic naming and referential integrity.
```php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
// Non-standard names
$table->foreignId('author_id')->constrained('users');
```
## Never Modify Deployed Migrations
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
Incorrect (editing a deployed migration):
```php
// 2024_01_01_create_posts_table.php — already in production
$table->string('slug')->unique(); // ← added after deployment
```
Correct (new migration to alter):
```php
// 2024_03_15_add_slug_to_posts_table.php
Schema::table('posts', function (Blueprint $table) {
$table->string('slug')->unique()->after('title');
});
```
## Add Indexes in the Migration
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->index();
$table->string('status')->index();
$table->timestamp('shipped_at')->nullable()->index();
$table->timestamps();
});
```
## Mirror Defaults in Model `$attributes`
When a column has a database default, mirror it in the model so new instances have correct values before saving.
```php
// Migration
$table->string('status')->default('pending');
// Model
protected $attributes = [
'status' => 'pending',
];
```
## Write Reversible `down()` Methods by Default
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
```php
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
```
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
## Keep Migrations Focused
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
Incorrect (partial failure creates unrecoverable state):
```php
public function up(): void
{
Schema::create('settings', function (Blueprint $table) { ... });
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
}
```
Correct (separate migrations):
```php
// Migration 1: create_settings_table
Schema::create('settings', function (Blueprint $table) { ... });
// Migration 2: seed_default_settings
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
```

View File

@@ -0,0 +1,144 @@
# Queue & Job Best Practices
## Set `retry_after` Greater Than `timeout`
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
Incorrect (`retry_after``timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 90 ← job retried while still running!
```
Correct (`retry_after` > `timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
```
## Use Exponential Backoff
Use progressively longer delays between retries to avoid hammering failing services.
Incorrect (fixed retry interval):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
// Default: retries immediately, overwhelming the API
}
```
Correct (exponential backoff):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
public $backoff = [1, 5, 10];
}
```
## Implement `ShouldBeUnique`
Prevent duplicate job processing.
```php
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->order->id;
}
public $uniqueFor = 3600;
}
```
## Always Implement `failed()`
Handle errors explicitly — don't rely on silent failure.
```php
public function failed(?Throwable $exception): void
{
$this->podcast->update(['status' => 'failed']);
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
}
```
## Rate Limit External API Calls in Jobs
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
```php
public function middleware(): array
{
return [new RateLimited('external-api')];
}
```
## Batch Related Jobs
Use `Bus::batch()` when jobs should succeed or fail together.
```php
Bus::batch([
new ImportCsvChunk($chunk1),
new ImportCsvChunk($chunk2),
])
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
->dispatch();
```
## `retryUntil()` Needs `$tries = 0`
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
```php
public $tries = 0;
public function retryUntil(): \DateTimeInterface
{
return now()->addHours(4);
}
```
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
```php
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// Lock releases when processing begins, not when it finishes
}
```
## Use Horizon for Complex Queue Scenarios
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
```php
// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
],
```

View File

@@ -0,0 +1,99 @@
# Routing & Controllers Best Practices
## Use Implicit Route Model Binding
Let Laravel resolve models automatically from route parameters.
Incorrect:
```php
public function show(int $id)
{
$post = Post::findOrFail($id);
}
```
Correct:
```php
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
```
## Use Scoped Bindings for Nested Resources
Enforce parent-child relationships automatically.
```php
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
// $post is automatically scoped to $user
})->scopeBindings();
```
## Use Resource Controllers
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
```php
Route::resource('posts', PostController::class);
// In routes/api.php — the /api prefix is applied automatically
Route::apiResource('posts', Api\PostController::class);
```
## Keep Controllers Thin
Aim for under 10 lines per method. Extract business logic to action or service classes.
Incorrect:
```php
public function store(Request $request)
{
$validated = $request->validate([...]);
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images'));
}
$post = Post::create($validated);
$post->tags()->sync($validated['tags']);
event(new PostCreated($post));
return redirect()->route('posts.show', $post);
}
```
Correct:
```php
public function store(StorePostRequest $request, CreatePostAction $create)
{
$post = $create->execute($request->validated());
return redirect()->route('posts.show', $post);
}
```
## Type-Hint Form Requests
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
Incorrect:
```php
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'max:255'],
'body' => ['required'],
]);
Post::create($validated);
return redirect()->route('posts.index');
}
```
Correct:
```php
public function store(StorePostRequest $request): RedirectResponse
{
Post::create($request->validated());
return redirect()->route('posts.index');
}
```

View File

@@ -0,0 +1,39 @@
# Task Scheduling Best Practices
## Use `withoutOverlapping()` on Variable-Duration Tasks
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
## Use `onOneServer()` on Multi-Server Deployments
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
## Use `runInBackground()` for Concurrent Long Tasks
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
## Use `environments()` to Restrict Tasks
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
```php
Schedule::command('billing:charge')->monthly()->environments(['production']);
```
## Use `takeUntilTimeout()` for Time-Bounded Processing
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
## Use Schedule Groups for Shared Configuration
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
```php
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
```

View File

@@ -0,0 +1,198 @@
# Security Best Practices
## Mass Assignment Protection
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
Incorrect:
```php
class User extends Model
{
protected $guarded = []; // All fields are mass assignable
}
```
Correct:
```php
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
```
Never use `$guarded = []` on models that accept user input.
## Authorize Every Action
Use policies or gates in controllers. Never skip authorization.
Incorrect:
```php
public function update(UpdatePostRequest $request, Post $post)
{
$post->update($request->validated());
}
```
Correct:
```php
public function update(UpdatePostRequest $request, Post $post)
{
Gate::authorize('update', $post);
$post->update($request->validated());
}
```
Or via Form Request:
```php
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
```
## Prevent SQL Injection
Always use parameter binding. Never interpolate user input into queries.
Incorrect:
```php
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
```
Correct:
```php
User::where('name', $request->name)->get();
// Raw expressions with bindings
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
```
## Escape Output to Prevent XSS
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
Incorrect:
```blade
{!! $user->bio !!}
```
Correct:
```blade
{{ $user->bio }}
```
## CSRF Protection
Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied.
Incorrect:
```blade
<form method="POST" action="/posts">
<input type="text" name="title">
</form>
```
Correct:
```blade
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
</form>
```
## Rate Limit Auth and API Routes
Apply `throttle` middleware to authentication and API routes.
```php
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Route::post('/login', LoginController::class)->middleware('throttle:login');
```
## Validate File Uploads
Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames.
```php
public function rules(): array
{
return [
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
];
}
```
Store with generated filenames:
```php
$path = $request->file('avatar')->store('avatars', 'public');
```
## Keep Secrets Out of Code
Never commit `.env`. Access secrets via `config()` only.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'api_key' => env('API_KEY'),
// In application code
$key = config('services.api_key');
```
## Audit Dependencies
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
```bash
composer audit
```
## Encrypt Sensitive Database Fields
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
Incorrect:
```php
class Integration extends Model
{
protected function casts(): array
{
return [
'api_key' => 'string',
];
}
}
```
Correct:
```php
class Integration extends Model
{
protected $hidden = ['api_key', 'api_secret'];
protected function casts(): array
{
return [
'api_key' => 'encrypted',
'api_secret' => 'encrypted',
];
}
}
```

View File

@@ -0,0 +1,125 @@
# Conventions & Style
## Follow Laravel Naming Conventions
| What | Convention | Good | Bad |
|------|-----------|------|-----|
| Controller | singular | `ArticleController` | `ArticlesController` |
| Model | singular | `User` | `Users` |
| Table | plural, snake_case | `article_comments` | `articleComments` |
| Pivot table | singular alphabetical | `article_user` | `user_article` |
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
| Route | plural | `articles/1` | `article/1` |
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
| Method | camelCase | `getAll` | `get_all` |
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
| Collection | descriptive, plural | `$activeUsers` | `$data` |
| Object | descriptive, singular | `$activeUser` | `$users` |
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
| Enum | singular | `UserType` | `UserTypes` |
## Prefer Shorter Readable Syntax
| Verbose | Shorter |
|---------|---------|
| `Session::get('cart')` | `session('cart')` |
| `$request->session()->get('cart')` | `session('cart')` |
| `$request->input('name')` | `$request->name` |
| `return Redirect::back()` | `return back()` |
| `Carbon::now()` | `now()` |
| `App::make('Class')` | `app('Class')` |
| `->where('column', '=', 1)` | `->where('column', 1)` |
| `->orderBy('created_at', 'desc')` | `->latest()` |
| `->orderBy('created_at', 'asc')` | `->oldest()` |
| `->first()->name` | `->value('name')` |
## Use Laravel String & Array Helpers
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
Strings — use `Str` and fluent `Str::of()` over raw PHP:
```php
// Incorrect
$slug = strtolower(str_replace(' ', '-', $title));
$short = substr($text, 0, 100) . '...';
$class = substr(strrchr('App\Models\User', '\'), 1);
// Correct
$slug = Str::slug($title);
$short = Str::limit($text, 100);
$class = class_basename('App\Models\User');
```
Fluent strings — chain operations for complex transformations:
```php
// Incorrect
$result = strtolower(trim(str_replace('_', '-', $input)));
// Correct
$result = Str::of($input)->trim()->replace('_', '-')->lower();
```
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
Arrays — use `Arr` over raw PHP:
```php
// Incorrect
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
// Correct
$name = Arr::get($array, 'user.name', 'default');
```
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
Numbers — use `Number` for display formatting:
```php
Number::format(1000000); // "1,000,000"
Number::currency(1500, 'USD'); // "$1,500.00"
Number::abbreviate(1000000); // "1M"
Number::fileSize(1024 * 1024); // "1 MB"
Number::percentage(75.5); // "75.5%"
```
URIs — use `Uri` for URL manipulation:
```php
$uri = Uri::of('https://example.com/search')
->withQuery(['q' => 'laravel', 'page' => 1]);
```
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
Use `search-docs` for the full list of available methods — these helpers are extensive.
## No Inline JS/CSS in Blade
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
Incorrect:
```blade
let article = `{{ json_encode($article) }}`;
```
Correct:
```blade
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
```
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
## No Unnecessary Comments
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
Incorrect:
```php
// Check if there are any joins
if (count((array) $builder->getQuery()->joins) > 0)
```
Correct:
```php
if ($this->hasJoins())
```

View File

@@ -0,0 +1,43 @@
# Testing Best Practices
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date.
## Use Model Assertions Over Raw Database Assertions
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
Correct: `$this->assertModelExists($user);`
More expressive, type-safe, and fails with clearer messages.
## Use Factory States and Sequences
Named states make tests self-documenting. Sequences eliminate repetitive setup.
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
Correct: `User::factory()->unverified()->create();`
## Use `Exceptions::fake()` to Assert Exception Reporting
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
## Call `Event::fake()` After Factory Setup
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
Incorrect: `Event::fake(); $user = User::factory()->create();`
Correct: `$user = User::factory()->create(); Event::fake();`
## Use `recycle()` to Share Relationship Instances Across Factories
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
```php
Ticket::factory()
->recycle(Airline::factory()->create())
->create();
```

View File

@@ -0,0 +1,75 @@
# Validation & Forms Best Practices
## Use Form Request Classes
Extract validation from controllers into dedicated Form Request classes.
Incorrect:
```php
public function store(Request $request)
{
$request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
}
```
Correct:
```php
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
```
## Array vs. String Notation for Rules
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
```php
// Preferred for new code
'email' => ['required', 'email', Rule::unique('users')],
// Follow existing convention if the project uses string notation
'email' => 'required|email|unique:users',
```
## Always Use `validated()`
Get only validated data. Never use `$request->all()` for mass operations.
Incorrect:
```php
Post::create($request->all());
```
Correct:
```php
Post::create($request->validated());
```
## Use `Rule::when()` for Conditional Validation
```php
'company_name' => [
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
],
```
## Use the `after()` Method for Custom Validation
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
```php
public function after(): array
{
return [
function (Validator $validator) {
if ($this->quantity > Product::find($this->product_id)?->stock) {
$validator->errors()->add('quantity', 'Not enough stock.');
}
},
];
}
```

View File

@@ -0,0 +1,159 @@
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

View File

@@ -0,0 +1,119 @@
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
author: laravel
---
# Tailwind CSS Development
## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
## Basic Usage
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
## Tailwind CSS v4 Specifics
- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
### CSS-First Configuration
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<!-- CSS-First Config -->
```css
@theme {
--color-brand: oklch(0.72 0.11 178);
}
```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```
### Replaced Utilities
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
## Spacing
Use `gap` utilities instead of margins for spacing between siblings:
<!-- Gap Utilities -->
```html
<div class="flex gap-8">
<div>Item 1</div>
<div>Item 2</div>
</div>
```
## Dark Mode
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content adapts to color scheme
</div>
```
## Common Patterns
### Flexbox Layout
<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
<div>Left content</div>
<div>Right content</div>
</div>
```
### Grid Layout
<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
```
## Common Pitfalls
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

8
.env.cpad Normal file
View File

@@ -0,0 +1,8 @@
# cPad Configuration
# Template: custom
CPAD_DEBUG=false
CPAD_CACHE_ENABLED=true
CPAD_LOG_LEVEL=WARNING
CPAD_SECURITY_LEVEL=MAXIMUM
CPAD_BACKUP_ENABLED=true

65
.env.example Normal file
View File

@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=aritmija
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gemini/settings.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
}
}
}

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db

167
GEMINI.md Normal file
View File

@@ -0,0 +1,167 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4
- laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
- vue (VUE) - v3
- tailwindcss (TAILWINDCSS) - v4
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
## Tools
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
- Use `database-schema` to inspect table structure before writing migrations or models.
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
## Searching Documentation (IMPORTANT)
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
- Pass a `packages` array to scope results when you know which packages are relevant.
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
### Search Syntax
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
## Artisan
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
- To check environment variables, read the `.env` file directly.
## Tinker
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
=== deployments rules ===
# Deployment
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
=== tests rules ===
# Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
## APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
</laravel-boost-guidelines>

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use cPad\Plugins\Pages\Services\Page;
class MainController extends Controller
{
protected $lang;
public function __construct(protected Page $page)
{
$this->lang = app()->getLocale();
}
public function Main(Request $request)
{
$page = $this->page->getByKeycode('PAGE-HOME', $this->lang);
if (is_null($page)) {
abort(404);
}
$seo = $this->pageSeo($page, 'Home');
return view('pages.index', [
'page' => 'home',
'assetBase' => asset('assets'),
'page_title' => $seo['title'],
'seo' => $seo,
'title' => $seo['title'],
]);
}
public function Page(Request $request, string $lang, int $id)
{
$validator = Validator::make([
'iso' => $lang,
'id' => $id,
], [
'iso' => 'string|required',
'id' => 'integer|required|min:1',
]);
if ($validator->fails()) {
abort(404);
}
$content = $this->page->load($id, $lang);
if (is_null($content)) {
abort(404);
}
$slider = null;
$seo = $this->pageSeo($content, 'Page');
return view('pages.page', [
'page' => Str::slug($seo['title'] ?? 'page'),
'content' => $content,
'slider' => $slider,
'page_title' => $seo['title'],
'seo' => $seo,
'assetBase' => asset('assets'),
'title' => $seo['title'],
]);
}
public function Slug(Request $request, string $lang, string $slug)
{
$slug = Str::slug($slug);
$lang = Str::slug($lang);
#$content = $this->page->slug($slug, $lang);
$content = $this->page->getBySlug($slug, $lang);
if (is_null($content)) {
abort(404);
}
$seo = $this->pageSeo($content, $slug);
return view('pages.page', [
'page' => $slug,
'content' => $content,
'slider' => null,
'page_title' => $seo['title'],
'seo' => $seo,
'assetBase' => asset('assets'),
'title' => $seo['title'],
]);
}
private function pageSeo(object $page, ?string $fallbackTitle = null): array
{
$meta = data_get($page, 'meta') ?? [];
$og = data_get($page, 'og') ?? [];
$translation = data_get($page, 'translation');
$content = data_get($page, 'content');
$title = (string) (data_get($meta, 'title')
?: data_get($translation, 'page_title')
?: data_get($content, 'headline')
?: $fallbackTitle
?: 'Page');
$description = (string) (data_get($meta, 'description')
?: data_get($translation, 'meta_description')
?: '');
return [
'title' => $title,
'meta_description' => $description,
'meta_keywords' => (string) (data_get($meta, 'keywords') ?: ''),
'meta_author' => (string) (data_get($meta, 'author') ?: ''),
'meta_publisher' => (string) (data_get($meta, 'publisher') ?: ''),
'meta_copyright' => (string) (data_get($meta, 'copyright') ?: ''),
'meta_refresh' => data_get($meta, 'refresh'),
'og_title' => (string) (data_get($og, 'title') ?: $title),
'og_description' => (string) (data_get($og, 'description') ?: $description),
'og_image' => (string) (data_get($og, 'image') ?: ''),
'og_type' => (string) (data_get($og, 'type') ?: 'website'),
];
}
}

View File

@@ -0,0 +1,774 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreContactMessageRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Klevze\ControlPanel\Facades\ActiveLanguage;
use cPad\Plugins\Pages\Services\Page as CmsPage;
class PageController extends Controller
{
public function home(): View
{
$frontpageId = DB::table('contents')
->where('page_type', 'frontpage')
->value('content_id');
$frontpage = $frontpageId
? app(CmsPage::class)->load((int) $frontpageId, app()->getLocale())
: null;
$seo = $this->pageSeo($frontpage, 'Home');
return $this->render('pages.home', $seo['title'], 'home', [
'seo' => $seo,
]);
}
public function about(): View
{
return $this->render('pages.about', 'About Us', 'about');
}
public function work(): View
{
$categoryMap = $this->projectCategoryMap();
return $this->render('pages.work', 'Works', 'work', [
'artworks' => $this->portfolioProjects($categoryMap),
'categories' => array_values($categoryMap),
]);
}
public function project(string $locale, ?int $projectId = null, ?string $slug = null): View
{
$project = $this->findPublicProject($projectId);
abort_if(!$project, 404);
$payload = $this->projectRendererPayload($project);
$headline = (string) ($project->headline ?: 'Single Project');
$metaTitle = (string) ($project->meta_title ?: $project->page_title ?: $headline);
return $this->render('pages.project', $metaTitle, 'work', [
'project' => $this->normalizeProjectForBlade($payload),
'nextProject' => $this->nextPublicProject((int) $project->project_id),
'seo' => [
'title' => $metaTitle,
'meta_description' => (string) ($project->meta_description ?? ''),
'og_title' => (string) ($project->og_title ?: $metaTitle),
'og_description' => (string) ($project->og_description ?: $project->meta_description ?? ''),
'og_image' => $this->projectCoverUrl($project->picture_catalog ?? null)
?? $this->projectCoverUrl($project->picture_cover ?? null)
?? '',
'og_url' => request()->url(),
],
]);
}
public function contact(): View
{
return $this->render('pages.contact', 'Contact Us', 'contact');
}
public function terms(): View
{
return $this->render('pages.terms', 'Terms', 'terms');
}
public function thankyou(): View
{
return $this->render('pages.thankyou', 'Thank you!', 'contact');
}
public function submitContact(StoreContactMessageRequest $request): RedirectResponse
{
Log::info('Website contact form submission', $request->validated());
return redirect()->route('thankyou');
}
private function render(string $view, string $title, string $page, array $data = []): View
{
return view($view, array_merge($data, [
'activeLanguages' => ActiveLanguage::getList('fp'),
'assetBase' => asset('assets'),
'page' => $page,
'title' => $title,
]));
}
private function pageSeo(?object $page, ?string $fallbackTitle = null): array
{
$meta = data_get($page, 'meta') ?? [];
$og = data_get($page, 'og') ?? [];
$translation = data_get($page, 'translation');
$content = data_get($page, 'content');
$title = (string) (data_get($meta, 'title')
?: data_get($translation, 'page_title')
?: data_get($content, 'headline')
?: $fallbackTitle
?: 'Home');
$description = (string) (data_get($meta, 'description')
?: data_get($translation, 'meta_description')
?: '');
return [
'title' => $title,
'meta_description' => $description,
'meta_keywords' => (string) (data_get($meta, 'keywords') ?: ''),
'meta_author' => (string) (data_get($meta, 'author') ?: ''),
'meta_publisher' => (string) (data_get($meta, 'publisher') ?: ''),
'meta_copyright' => (string) (data_get($meta, 'copyright') ?: ''),
'meta_refresh' => data_get($meta, 'refresh'),
'og_title' => (string) (data_get($og, 'title') ?: $title),
'og_description' => (string) (data_get($og, 'description') ?: $description),
'og_image' => (string) (data_get($og, 'image') ?: ''),
'og_type' => (string) (data_get($og, 'type') ?: 'website'),
];
}
private function portfolioProjects(array $categoryMap): array
{
return $this->applyPublicProjectOrder($this->publicProjectsQuery())
->get()
->values()
->map(function ($project, int $index) use ($categoryMap) {
$categoryIds = collect($this->decodeCategoryIds($project->categories ?? null))
->filter(fn ($id) => is_numeric($id) && (int) $id > 0)
->map(fn ($id) => (int) $id)
->unique()
->values();
$filters = $categoryIds
->map(fn (int $id) => $categoryMap[$id]['filter'] ?? null)
->filter()
->values()
->all();
$headline = (string) ($project->headline ?: $project->name ?: 'Untitled project');
return [
'id' => (int) $project->project_id,
'sort_order' => (int) ($project->num ?? 0),
'title' => $headline,
'subline' => (string) ($project->subline ?: ''),
'subtitle' => '',
'thumbnail' => $this->projectThumbnailForGrid($project, $index),
'image' => $this->projectCoverForGrid($project, $index),
'filters' => $filters,
'group' => $this->defaultProjectGroup(),
'url' => route('project', [
'locale' => app()->getLocale(),
'projectId' => $project->project_id,
'slug' => Str::slug($headline),
]),
];
})
->all();
}
private function projectCategoryMap(): array
{
$locale = app()->getLocale();
$fallbackLocale = ActiveLanguage::getDefaultLanguage('fp') ?: config('app.fallback_locale');
return DB::table('projects_categories as c')
->leftJoin('projects_categories_description as cd', function ($join) use ($locale) {
$join->on('c.category_id', '=', 'cd.category_id')
->where('cd.iso', '=', $locale);
})
->leftJoin('projects_categories_description as fallback_cd', function ($join) use ($fallbackLocale) {
$join->on('c.category_id', '=', 'fallback_cd.category_id')
->where('fallback_cd.iso', '=', $fallbackLocale);
})
->where('c.parent_id', 0)
->where('c.active', 1)
->orderBy('c.num')
->select('c.category_id', DB::raw('COALESCE(cd.title, fallback_cd.title) as title'))
->get()
->mapWithKeys(function ($category) {
$title = (string) ($category->title ?? '');
if ($title === '') {
return [];
}
return [(int) $category->category_id => [
'id' => (int) $category->category_id,
'title' => $title,
'filter' => (string) Str::of($title)->lower()->replaceMatches('/[^a-z0-9]+/', ''),
]];
})
->all();
}
private function findPublicProject(?int $projectId = null): ?object
{
$query = $this->applyPublicProjectOrder($this->publicProjectsQuery());
if ($projectId !== null) {
$query->where('p.project_id', $projectId);
}
return $query->first();
}
private function nextPublicProject(int $currentProjectId): ?array
{
$current = $this->findPublicProject($currentProjectId);
if (!$current) {
return null;
}
$next = $this->applyPublicProjectOrder(
$this->publicProjectsQuery()
->where(function ($query) use ($current, $currentProjectId) {
$query->where('p.num', '>', (int) ($current->num ?? 0))
->orWhere(function ($nestedQuery) use ($current, $currentProjectId) {
$nestedQuery->where('p.num', '=', (int) ($current->num ?? 0))
->where('p.project_id', '<', $currentProjectId);
});
})
)->first();
if (!$next) {
$next = $this->applyPublicProjectOrder(
$this->publicProjectsQuery()
->where('p.project_id', '<>', $currentProjectId)
)->first();
}
if (!$next) {
return null;
}
$headline = (string) ($next->headline ?: $next->name ?: 'Next project');
return [
'title' => $headline,
'url' => route('project', [
'locale' => app()->getLocale(),
'projectId' => $next->project_id,
'slug' => Str::slug($headline),
]),
];
}
private function publicProjectsQuery()
{
$locale = app()->getLocale();
$fallbackLocale = ActiveLanguage::getDefaultLanguage('fp') ?: config('app.fallback_locale');
return DB::table('projects as p')
->leftJoin('projects_description as pd', function ($join) use ($locale) {
$join->on('p.project_id', '=', 'pd.project_id')
->where('pd.iso', '=', $locale);
})
->leftJoin('projects_description as fallback_pd', function ($join) use ($fallbackLocale) {
$join->on('p.project_id', '=', 'fallback_pd.project_id')
->where('fallback_pd.iso', '=', $fallbackLocale);
})
->where('p.active', 1)
->select([
'p.project_id',
'p.num',
'p.categories',
'p.picture_cover',
'p.picture_catalog',
'p.picture_catalog_full',
'p.video',
'p.structure',
DB::raw('COALESCE(pd.headline, fallback_pd.headline) as headline'),
DB::raw('COALESCE(pd.subline, fallback_pd.subline) as subline'),
DB::raw('COALESCE(pd.name, fallback_pd.name) as name'),
DB::raw('COALESCE(pd.year, fallback_pd.year) as year'),
DB::raw('COALESCE(pd.preview, fallback_pd.preview) as preview'),
DB::raw('COALESCE(pd.content, fallback_pd.content) as content'),
DB::raw('COALESCE(pd.tag1, fallback_pd.tag1) as tag1'),
DB::raw('COALESCE(pd.tag2, fallback_pd.tag2) as tag2'),
DB::raw('COALESCE(pd.tag3, fallback_pd.tag3) as tag3'),
DB::raw('COALESCE(pd.youtube, fallback_pd.youtube) as youtube'),
DB::raw('COALESCE(pd.page_title, fallback_pd.page_title) as page_title'),
DB::raw('COALESCE(pd.meta_title, fallback_pd.meta_title) as meta_title'),
DB::raw('COALESCE(pd.meta_description, fallback_pd.meta_description) as meta_description'),
DB::raw('COALESCE(pd.og_title, fallback_pd.og_title) as og_title'),
DB::raw('COALESCE(pd.og_description, fallback_pd.og_description) as og_description'),
]);
}
private function applyPublicProjectOrder($query)
{
return $query
->orderBy('p.num')
->orderByDesc('p.project_id');
}
private function normalizeProjectForBlade(array $payload): array
{
$structure = $payload['structure'] ?? [];
$legacy = $payload['legacy'] ?? [];
$lang = $payload['defaultLanguage'] ?? app()->getLocale();
$hasModernStructure = !empty($structure) && (
array_key_exists('header', $structure) ||
array_key_exists('heroMedia', $structure) ||
array_key_exists('contentBlocks', $structure) ||
array_key_exists('blocks', $structure)
);
if ($hasModernStructure) {
$legacyAwards = $this->bladeNormalizeHtmlList((array) ($legacy['awarded'] ?? []));
$structureAwards = $this->bladeNormalizeHtmlList((array) ($structure['metadata']['awarded'] ?? []));
return [
'headline' => $this->bladeFirstFilledHtml($structure['header']['headline'] ?? null, $legacy['headline'] ?? null),
'subline' => $this->bladeFirstFilledHtml($structure['header']['subline'] ?? null, $legacy['subline'] ?? null),
'hero' => $this->bladeHeroSlot($structure['heroMedia'] ?? null),
'year' => $this->bladeFirstFilledText($structure['metadata']['year'] ?? null, $legacy['year'] ?? null),
'clientName' => $this->bladeFirstFilledHtml($structure['metadata']['clientName'] ?? null, $legacy['clientName'] ?? null),
'awarded' => !empty($structureAwards) ? $structureAwards : $legacyAwards,
'description'=> $this->bladeFirstFilledHtml($legacy['description'] ?? null, $structure['metadata']['description'] ?? null),
'blocks' => $this->bladeNormalizeBlocks(
$structure['contentBlocks'] ?? $structure['blocks'] ?? [],
$lang
),
];
}
// Fall back to legacy flat fields
$heroMedia = null;
if (!empty($legacy['youtube'])) {
$heroMedia = ['type' => 'youtube', 'url' => $legacy['youtube'], 'poster' => ''];
} elseif (!empty($legacy['video'])) {
$heroMedia = ['type' => 'video', 'url' => $legacy['video'], 'poster' => ''];
} elseif (!empty($legacy['pictureCatalogFull'])) {
$heroMedia = ['type' => 'image', 'url' => $legacy['pictureCatalogFull'], 'poster' => ''];
} elseif (!empty($legacy['pictureCover'])) {
$heroMedia = ['type' => 'image', 'url' => $legacy['pictureCover'], 'poster' => ''];
}
return [
'headline' => $this->bladeNormalizeHtml((string) ($legacy['headline'] ?? '')),
'subline' => $this->bladeNormalizeHtml((string) ($legacy['subline'] ?? '')),
'hero' => $this->bladeHeroSlot($heroMedia),
'year' => $this->bladeNormalizeText((string) ($legacy['year'] ?? '')),
'clientName' => $this->bladeNormalizeHtml((string) ($legacy['clientName'] ?? '')),
'awarded' => array_map($this->bladeNormalizeHtml(...), (array) ($legacy['awarded'] ?? [])),
'description' => $this->bladeNormalizeHtml((string) ($legacy['description'] ?? '')),
'blocks' => [],
];
}
private function bladeHeroSlot(?array $heroMedia): ?array
{
if (!$heroMedia) {
return null;
}
$type = $heroMedia['type'] ?? 'image';
$url = $heroMedia['url'] ?? '';
if (!$url) {
return null;
}
if ($type === 'image') {
return [
'type' => 'image',
'text' => '',
'image' => ['url' => $url, 'alt' => ''],
'media' => null,
];
}
return [
'type' => 'video',
'text' => '',
'image' => ['url' => $heroMedia['poster'] ?? '', 'alt' => ''],
'media' => $this->bladeNormalizeMedia($heroMedia),
];
}
private function bladeNormalizeBlocks(array $blocks, string $lang): array
{
$result = [];
foreach ($blocks as $block) {
$type = $block['type'] ?? '';
// Legacy type migrations
if ($type === 'FullWidthText') {
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'text',
'text' => $this->bladeResolveText($block['content'] ?? [], $lang),
'image' => null,
'media' => null,
],
];
continue;
}
if ($type === 'FullWidthImage') {
$img = $block['image'] ?? [];
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'image',
'text' => '',
'image' => ['url' => $img['url'] ?? '', 'alt' => $this->bladeResolveText($img['alt'] ?? [], $lang)],
'media' => null,
],
];
continue;
}
if ($type === 'Video') {
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'video',
'text' => '',
'image' => null,
'media' => $this->bladeNormalizeMedia($block['media'] ?? null),
],
];
continue;
}
if ($type === 'TwoColumnImages') {
$images = $block['images'] ?? [];
$result[] = [
'type' => 'TwoColumns',
'hidden' => (bool) ($block['hidden'] ?? false),
'left' => $this->bladeImageSlot($images[0] ?? [], $lang),
'right' => $this->bladeImageSlot($images[1] ?? [], $lang),
];
continue;
}
if ($type === 'TwoColumns') {
$result[] = [
'type' => 'TwoColumns',
'hidden' => (bool) ($block['hidden'] ?? false),
'left' => $this->bladeNormalizeSlot($block['left'] ?? [], $lang),
'right' => $this->bladeNormalizeSlot($block['right'] ?? [], $lang),
];
continue;
}
// FullWidth (default)
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => $this->bladeNormalizeSlot($block['slot'] ?? [], $lang),
];
}
return $result;
}
private function bladeNormalizeSlot(array $slot, string $lang): array
{
return [
'type' => $slot['type'] ?? 'text',
'text' => $this->bladeResolveText($slot['content'] ?? [], $lang),
'image' => [
'url' => $slot['image']['url'] ?? '',
'alt' => $this->bladeResolveText($slot['image']['alt'] ?? [], $lang),
],
'media' => $this->bladeNormalizeMedia($slot['media'] ?? null),
];
}
private function bladeImageSlot(array $image, string $lang): array
{
return [
'type' => 'image',
'text' => '',
'image' => ['url' => $image['url'] ?? '', 'alt' => $this->bladeResolveText($image['alt'] ?? [], $lang)],
'media' => null,
];
}
private function bladeNormalizeMedia(?array $media): ?array
{
if (!$media) {
return null;
}
$type = $media['type'] ?? 'image';
$url = $media['url'] ?? '';
if (!$url) {
return null;
}
return [
'type' => $type,
'url' => $url,
'poster' => $media['poster'] ?? '',
'autoplay' => ($media['autoplay'] ?? false) === true,
'loop' => ($media['loop'] ?? false) === true,
'muted' => ($media['muted'] ?? false) === true,
'embedUrl' => $this->bladeEmbedUrl($type, $media),
];
}
private function bladeResolveText(mixed $content, string $lang): string
{
if (is_string($content)) {
return $this->bladeNormalizeHtml($content);
}
if (!is_array($content) || empty($content)) {
return '';
}
$values = array_values($content);
return $this->bladeNormalizeHtml((string) ($content[$lang] ?? $values[0] ?? ''));
}
private function bladeNormalizeHtml(string $value): string
{
$normalized = $value;
for ($i = 0; $i < 3; $i++) {
$decoded = html_entity_decode($normalized, ENT_QUOTES | ENT_HTML5, 'UTF-8');
if ($decoded === $normalized) {
break;
}
$normalized = $decoded;
}
return $normalized;
}
private function bladeFirstFilledHtml(mixed ...$values): string
{
foreach ($values as $value) {
$normalized = $this->bladeNormalizeHtml((string) ($value ?? ''));
if (trim(strip_tags($normalized)) !== '') {
return $normalized;
}
}
return '';
}
private function bladeNormalizeText(string $value): string
{
return trim(strip_tags($this->bladeNormalizeHtml($value)));
}
private function bladeFirstFilledText(mixed ...$values): string
{
foreach ($values as $value) {
$normalized = $this->bladeNormalizeText((string) ($value ?? ''));
if ($normalized !== '') {
return $normalized;
}
}
return '';
}
private function bladeNormalizeHtmlList(array $values): array
{
$normalized = [];
foreach ($values as $value) {
$item = $this->bladeNormalizeHtml((string) ($value ?? ''));
if (trim(strip_tags($item)) !== '') {
$normalized[] = $item;
}
}
return $normalized;
}
private function bladeEmbedUrl(string $type, array|string $media): string
{
$mediaObject = is_array($media) ? $media : ['url' => $media];
$url = (string) ($mediaObject['url'] ?? '');
if ($type === 'youtube') {
preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $url, $m);
return isset($m[1])
? 'https://www.youtube.com/embed/' . $m[1]
. '?autoplay=' . (($mediaObject['autoplay'] ?? false) ? '1' : '0')
. '&mute=' . (($mediaObject['muted'] ?? false) ? '1' : '0')
. '&loop=' . (($mediaObject['loop'] ?? false) ? '1' : '0')
. ((($mediaObject['loop'] ?? false) ? '&playlist=' . $m[1] : ''))
: '';
}
if ($type === 'bunny') {
// Normalize /play/ → /embed/ regardless of what was stored
$url = preg_replace('#player\.mediadelivery\.net/play/#', 'player.mediadelivery.net/embed/', $url);
$params = http_build_query([
'autoplay' => ($mediaObject['autoplay'] ?? false) ? 'true' : 'false',
'loop' => ($mediaObject['loop'] ?? false) ? 'true' : 'false',
'muted' => ($mediaObject['muted'] ?? false) ? 'true' : 'false',
'preload' => 'true',
'responsive' => 'true',
]);
return $url . '?' . $params;
}
// frameio, video — url is used as-is (embed src or file src)
return $url;
}
private function projectRendererPayload(object $project): array
{
return [
'structure' => $this->decodeStructure($project->structure ?? null),
'defaultLanguage' => app()->getLocale(),
'legacy' => [
'headline' => (string) ($project->headline ?? ''),
'subline' => (string) ($project->subline ?? ''),
'year' => (string) ($project->year ?? ''),
'clientName' => (string) ($project->name ?? ''),
'description' => (string) ($project->preview ?: $project->content ?: ''),
'awarded' => array_values(array_filter([
$project->tag1 ?? null,
$project->tag2 ?? null,
$project->tag3 ?? null,
])),
'youtube' => (string) ($project->youtube ?? ''),
'video' => $this->projectVideoUrl($project->video ?? null),
'pictureCover' => $this->projectCoverUrl($project->picture_cover ?? null),
'pictureCatalog' => $this->projectCoverUrl($project->picture_catalog ?? null),
'pictureCatalogFull' => $this->projectCoverUrl($project->picture_catalog_full ?? null),
'gallery' => $this->projectGallery((int) $project->project_id),
],
];
}
private function projectGallery(int $projectId): array
{
return DB::table('projects_gallery')
->where('project_id', $projectId)
->orderBy('num')
->get()
->map(function ($photo) {
return [
'url' => asset('files/projects/gallery/' . (int) floor($photo->project_id / 100) . '/' . $photo->project_id . '/' . $photo->diskname),
'alt' => (string) ($photo->name ?? ''),
];
})
->values()
->all();
}
private function decodeCategoryIds(?string $categories): array
{
$decoded = json_decode($categories ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
private function decodeStructure(?string $structure): array
{
$normalized = html_entity_decode((string) ($structure ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$decoded = json_decode($normalized, true);
return is_array($decoded) ? $decoded : [];
}
private function projectCoverForGrid(object $project, int $index): string
{
return $this->projectCoverUrl($project->picture_cover ?? null)
?? $this->projectCoverUrl($project->picture_catalog ?? null)
?? asset('assets/img/portfolio-' . (($index % 6) + 1) . '.jpg');
}
private function projectThumbnailForGrid(object $project, int $index): array
{
$structure = $this->decodeStructure($project->structure ?? null);
$rawThumbnailMedia = is_array($structure['thumbnailMedia'] ?? null) ? $structure['thumbnailMedia'] : null;
$thumbnailMedia = $this->bladeNormalizeMedia(
$rawThumbnailMedia
);
if (($thumbnailMedia['type'] ?? null) === 'bunny' && !empty($thumbnailMedia['embedUrl'])) {
return [
'type' => 'bunny',
'url' => '',
'embedUrl' => $thumbnailMedia['embedUrl'],
];
}
$heroMedia = $this->bladeNormalizeMedia(
is_array($structure['heroMedia'] ?? null) ? $structure['heroMedia'] : null
);
$prefersBunnyThumbnail = ($rawThumbnailMedia['type'] ?? null) === 'bunny';
if ($prefersBunnyThumbnail && ($heroMedia['type'] ?? null) === 'bunny' && !empty($heroMedia['embedUrl'])) {
return [
'type' => 'bunny',
'url' => '',
'embedUrl' => $heroMedia['embedUrl'],
];
}
return [
'type' => 'image',
'url' => $this->projectCoverForGrid($project, $index),
'embedUrl' => '',
];
}
private function projectCoverUrl(?string $filename): ?string
{
if (!$filename) {
return null;
}
return asset('files/projects/cover/' . $filename);
}
private function projectVideoUrl(?string $filename): ?string
{
if (!$filename) {
return null;
}
return asset('files/projects/video/' . $filename);
}
private function defaultProjectGroup(): array
{
return [
'name' => 'Aritmija',
'label' => 'Uploaded by',
'icon' => 'fas fa-layer-group',
'color' => '#4050FF',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreContactMessageRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'message' => ['required', 'string', 'max:2000'],
];
}
}

32
app/Models/User.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More