Laravel

Balíček dresscode/rules-laravel nese migrace frameworku Laravel od verze 6 po současnou, tedy pro kód psaný pro Laravel 5.8 a novější. Je v něm 179 položek: helpery str_slug() a array_get() na Str::slug() a Arr::get(), přejmenovaný middleware VerifyCsrfToken, Request::get() na input(), schéma bez Doctrine DBAL, sloupce migrací, které verze 11 zrušila. Přes sto z nich DressCode rovnou opraví, zbytek ohlásí i s tím, co napsat místo toho. A k tomu nabídne, jak Laravel 13 píše modely, příkazy a joby atributy.

Instalace

composer require --dev dresscode/rules-laravel

A do konfigurace typy a skupinu deprecations, jak popisuje Aktualizace knihoven:

types: phpstan

groups:
	- deprecations

Balíček žádné pravidlo nezapne sám. Data se uplatní jen tehdy, když projekt má laravel/framework, a jen ty verze, na které projekt dosáhne.

Larastan v projektu nevadí, naopak: ať ho máte v phpstan.neon, nebo ho zaregistroval phpstan/extension-installer, DressCode spustí aplikaci stejně jako PHPStan a fasádu Request::get() čte jako metodu třídy, která za ní stojí. Selže-li při tom bootstrap/app.php, vypíše Larastan sám hlášku „Application bootstrap failed“ i s výjimkou, na které start aplikace selhal, a celý běh ukončí. Pozor v CI: běh skončí nenulovým exit kódem, a když běží v jednom procesu, je to 1, stejně jako když DressCode v kódu něco najde. Alias z config/app.php psaný bez jmenného prostoru, \Request::get(), nezná žádná statická analýza, a ten zůstane nepovšimnutý.

Co se opraví

Tohle je middleware, provider a controller psaný pro Laravel 10, v projektu na Laravelu 13:

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;  // Class Illuminate\Foundation\Http\Middleware\VerifyCsrfToken is replaced by Illuminate\Foundation\Http\Middleware\PreventRequestForgery

class VerifyCsrfToken extends Middleware  // Class Illuminate\Foundation\Http\Middleware\VerifyCsrfToken is replaced by Illuminate\Foundation\Http\Middleware\PreventRequestForgery
{
	protected $except = ['stripe/*'];
}


class AppServiceProvider extends ServiceProvider
{
	public function boot(): void
	{
		Schema::useNativeSchemaOperationsIfPossible();  // Static method Illuminate\Support\Facades\Schema::useNativeSchemaOperationsIfPossible() is forbidden: there is no replacement, the schema is always changed natively; drop the call
		URL::forceRootUrl(config('app.url'));  // Static method Illuminate\Support\Facades\URL::forceRootUrl() is replaced by URL::useOrigin()
	}
}


class ReportController
{
	public function show(Request $request): array
	{
		$limit = Limit::perMinute(60);
		$limit->decayMinutes = 5;  // Property Illuminate\Cache\RateLimiting\Limit::$decayMinutes is replaced by $this->decaySeconds = $value * 60
		return [$request->get('day'), Schema::getAllTables()];  // Method Illuminate\Http\Request::get() is replaced by Request::input()
		                                                        // Static method Illuminate\Support\Facades\Schema::getAllTables() is forbidden: call getTables(), which returns arrays with the name and schema of each table, not rows of the database
	}
}

Po dresscode fix:

use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;

class VerifyCsrfToken extends Middleware
{
	protected $except = ['stripe/*'];
}


class AppServiceProvider extends ServiceProvider
{
	public function boot(): void
	{
		Schema::useNativeSchemaOperationsIfPossible();
		URL::useOrigin(config('app.url'));
	}
}


class ReportController
{
	public function show(Request $request): array
	{
		$limit = Limit::perMinute(60);
		$limit->decaySeconds = 5 * 60;
		return [$request->input('day'), Schema::getAllTables()];
	}
}

Minuty se převedly na sekundy, které Laravel 11 do vlastnosti ukládá. Schema::getAllTables() zůstalo: getTables() vrací pole se jménem a schématem každé tabulky, ne řádky databáze, a co s nimi váš kód dělá, musíte rozhodnout vy.

Příklady toho, co balíček opravuje, podle druhu změny:

  • helpery Laravelu 5.8: str_slug(), str_limit(), array_get(), studly_case() a dalších 35 na metody Str a Arr se stejnými argumenty, i s importem,
  • přejmenované třídy: VerifyCsrfToken a ValidateCsrfToken na PreventRequestForgery, trait HasVersion7Uuids na HasUuids, fasáda Input na Request, Resource na JsonResource, testovací TestResponse a Assert do jmenného prostoru Illuminate\Testing; přepíše se import, extends, ::class i withoutMiddleware(),
  • přejmenované členy: Request::get() na input() i přes fasádu, Lang::trans() a transChoice() na get() a choice(), dispatchNow() jobu a funkce dispatch_now() na dispatchSync() a dispatch_sync(), getRouteMiddleware() kernelu na getMiddlewareAliases(), validateCsrfTokens() na preventRequestForgery(), forceRootUrl() na useOrigin(), metody kolekcí containsOneItem(), containsManyItems(), reduceWithKeys(), reduceMany() a until() na hasSole(), hasMany(), reduce(), reduceSpread() a takeUntil(), Relation::getBaseQuery() na toBase(), PendingMail::sendNow() na send(), vlastnost QueueBusy::$connection na $connectionName,
  • volání jiného tvaru: $limit->decayMinutes na sekundy, $event->exceptionOccurred na $event->exception !== null, redirectTo() výjimky AuthenticationException s aktuálním požadavkem, Redirect::home() na route('home'), assertDeleted() testů na assertModelMissing() nebo assertDatabaseMissing(), assertTimesSent($count, $notification) na assertSentTimes($notification, $count), addHidden() a addVisible() modelu na makeHidden() a makeVisible(), sloupce double('x', 8, 2), unsignedDecimal() a unsignedDouble() migrací, jak je píše Laravel 11.

Úplný výčet je v datech balíčku, v souboru upgrading/framework.neon.

Helper volaný bez zpětného lomítka uvnitř jmenného prostoru, jak je to v aplikaci obvyklé, DressCode přepíše jen s --fix-risky: stejnojmennou funkci by mohl deklarovat ten prostor. str_contains(), array_first() a array_last() data nechávají být, protože dnes jsou to funkce PHP.

Request::get() a input() nejsou úplně totéž: get() čte napřed atributy routy, pak query a nakonec tělo požadavku, input() atributy nečte, tělo má před query a klíč s tečkou bere jako cestu do pole. Laravel sám u zastaralého get() radí input() a DressCode ho píše také; hodnotu, kterou nastavuje middleware jako atribut routy, čtěte přes $request->attributes->get().

Co framework změnil v signaturách, metodu, kterou přidal kontrakt, a návratový typ, který musí deklarovat potomek, data nepotřebuje: ohlásí a zapíše to no-unimplemented-abstract-method a override-signature z téže skupiny, z deklarací nainstalované verze. Dopsání návratového typu je riziková oprava (risky fix): tělo metody může vracet něco jiného, a proto se udělá jen s --fix-risky nebo s pravidlem v klíči fixRisky.

Co zůstane na vás

Kde náhrada není, vrací něco jiného, nebo se liší podle databáze, DressCode místo ohlásí a řekne, co napsat. Třeba Doctrine DBAL, se kterým Laravel 11 skončil, nebo prostorové sloupce migrací:

Method Illuminate\Database\Connection::getDoctrineSchemaManager() is forbidden: read the schema by the schema builder, such as Schema::getTables(), getColumns() or getIndexes()
Method Illuminate\Database\Schema\Blueprint::point() is forbidden: call geometry() or geography() with the subtype, such as geography($column, 'point'), whose type differs by the database

Stejně se ohlásí i další místa, například tato:

  • pošta: přechod ze Swift Maileru na Symfony Mailer (getSwiftMessage(), withSwiftMessage(), TransportManager) a ovladače Mandrill a SparkPost, které framework už nemá,
  • testy: factories modelů psané před Laravelem 8 i s helperem factory() a trait MocksApplicationServices,
  • fronta: retryAfter(), $retryAfter a $timeoutAt jobu, místo kterých fronta čte backoff(), $backoff a $retryUntil,
  • databáze: konstruktory gramatik a Blueprint, které Laravel 12 chce se spojením, withTablePrefix(), sloupec float() s parametry $total a $places a trait DetectsDeadlocks,
  • ostatní: helper elixir(), validace hesla v PasswordBroker, ovladač Rackspace souborového systému a trait DetectsApplicationNamespace.

Úplný výčet i s tím, co napsat místo toho, je opět v datech balíčku.

Co framework jen označil jako zastaralé a náhradu k tomu napsal, v datech často není; s typy to ohlásí, a kde anotace náhradu řekne, i přepíše no-deprecated-members. Změny chování a konfigurace data nezachytí vůbec, ty najdete v průvodci upgradem Laravelu.

Atributy Laravelu 13

Laravel 13 čte z atributů třídy to, co dřív četl z vlastností: #[Table('posts')] místo protected $table = 'posts', #[Tries(3)] místo public $tries = 3. Vlastnosti fungují dál, a proto je tahle část nabídka, ne oprava, v samostatné skupině modernization:

groups:
	- modernization
class Post extends Model
{
	protected $table = 'blog_posts';  // Property Illuminate\Database\Eloquent\Model::$table is replaced by the attribute #[Illuminate\Database\Eloquent\Attributes\Table]

	public $timestamps = false;  // Property Illuminate\Database\Eloquent\Model::$timestamps is replaced by the attribute #[Illuminate\Database\Eloquent\Attributes\WithoutTimestamps]

	protected $fillable = ['title', 'body'];  // Property Illuminate\Database\Eloquent\Model::$fillable is replaced by the attribute #[Illuminate\Database\Eloquent\Attributes\Fillable]

	protected $casts = ['published_at' => 'datetime'];  // Property Illuminate\Database\Eloquent\Model::$casts is replaced by the method casts()


	public function scopePublished(Builder $query): void  // Method scopePublished() is replaced by the method published() with the attribute #[Illuminate\Database\Eloquent\Attributes\Scope]
	{
		$query->whereNotNull('published_at');
	}
}

Po dresscode fix --fix-risky:

#[WithoutTimestamps]
#[Table(name: 'blog_posts')]
#[Fillable(['title', 'body'])]
class Post extends Model
{
	protected function casts(): array
	{
		return ['published_at' => 'datetime'];
	}


	#[Scope]
	protected function published(Builder $query): void
	{
		$query->whereNotNull('published_at');
	}
}

Atributy přinášejí data pravidla attribute-for-member pro modely, příkazy, form requesty, API resources a joby, listenery, mailables a notifikace ve frontě; vlastnosti téže třídy atributu, $table, $primaryKey a $keyType, se spojí do jednoho #[Table]. Třídu, která vlastnost sama čte, abstraktní třídu a metodu getRouteKeyName() s logikou pravidlo ohlásí a nechá. Metodu casts() píše laravel/casts-method-for-casts-property a scope s atributem laravel/scope-attribute-for-scope-prefix. Ta druhá oprava je riziková: volání scopePublished() starým jménem v jiném souboru nevidí, a proto se udělá jen s --fix-risky. Metodu píše chráněnou, protože statické volání Post::published() k ní jinak nedojde.

Co balíček pokrývá

co verze
helpery str_* a array_* 6
zastaralé a odstraněné API frameworku 6 až 13
atributy místo vlastností 13
casts() místo $casts 11
#[Scope] místo předpony scope 12.4

Balíček začíná Laravelem 6, takže převede kód psaný pro Laravel 5.8. Aplikaci na starší verzi převedete po jedné hlavní verzi, jak popisuje Aktualizace knihoven, ale pro verze do 5.8 data balíček nemá.