yiisoft/yii2-dev
Yii 2 is a fast, secure, and efficient PHP framework for building modern web applications. It ships with sensible defaults, is flexible and easy to customize, and provides a solid foundation for development.
Frame the decision before you type a command. yiisoft/yii2-dev is the monorepo distribution of the Yii 2 full-stack framework — not a Composer package you require into an existing Laravel app. There is no service provider, no facade, no auto-discovery, and nothing in the Illuminate\ namespace is implemented. In practice there are only two honest entry points:
If you need the framework as a dependency, the package name is yiisoft/yii2, not yiisoft/yii2-dev.
Scaffold a throwaway app to learn the shape of it.
composer create-project --prefer-dist yiisoft/yii2-app-basic yii-sandbox
cd yii-sandbox
php yii serve # built-in server on :8080
The advanced template (yiisoft/yii2-app-advanced) splits frontend/backend/console and is the better reference for anything multi-app or team-sized.
Where to look first, in this order:
| Priority | Path | Why |
|---|---|---|
| 1 | config/web.php |
The whole app is this array. Components, routes, DI definitions, bootstrap. Read it end to end once. |
| 2 | web/index.php |
Four lines. That is the entire bootstrap. |
| 3 | controllers/, models/, views/ |
Conventional MVC; nothing hides in a service container you can't see. |
| 4 | yii (console script) |
Your CLI entry point for migrations, Gii, cache, RBAC, queue. |
| 5 | Definitive Guide → "Application Structure" | Do not start at chapter 1 and read linearly. |
| 6 | API reference | Your PHPDoc substitute; the source itself is readable. |
Laravel → Yii 2 concept map. Keep this visible for the first week.
| Laravel | Yii 2 |
|---|---|
Illuminate\Support\Facades\* |
Yii::$app-><component> (service locator, not facades) |
Service container bind()/singleton() |
Yii::$container->set(), plus components in config |
Eloquent Model + Builder |
yii\base\Model (forms) + yii\db\ActiveRecord + ActiveQuery |
routes/web.php |
urlManager.rules in config, or pretty URLs via Controller::actions() |
| Middleware | behaviors() on controllers (AccessControl, VerbFilter, PageCache) |
| Blade | PHP view files (views/site/index.php) + layout via $this->title / layout property |
| Artisan | php yii <controller>/<action> |
| Migrations | yii\db\Migration, php yii migrate/create |
| Form Requests | yii\base\Model subclasses with rules() and load() |
php artisan tinker |
No first-class REPL. Use yii\helpers\VarDumper::dump() in a console controller. |
First use case — generate a CRUD, then read what it generated. With a table in place:
php yii gii/model --tableName=post --modelClass=Post
php yii gii/crud --modelClass=app\\models\\Post --controllerClass=app\\controllers\\PostController
Gii writes idiomatic Yii for your project. The generated PostController, PostSearch, and _form.php are the fastest possible tour of behaviors, GridView filtering, ActiveForm, and ActiveQuery. Read them before hand-writing anything.
First day checklist for an inherited codebase: find config/web.php and config/console.php, then grep for Yii::$container->set (DI overrides), Event::on (global event hooks), and bootstrap in config (things that run before every request — this is where surprises live). Confirm the lockfile resolves yiisoft/yii2 to ≥ 2.0.55 (see Gotchas).
Configuration is arrays, not classes. Every component is configured declaratively and lazily instantiated on first access:
// config/web.php
return [
'id' => 'app',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'components' => [
'db' => [
'class' => yii\db\Connection::class,
'dsn' => 'pgsql:host=localhost;dbname=app',
'username' => 'app',
'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8',
],
'cache' => ['class' => yii\caching\RedisCache::class],
'user' => ['identityClass' => app\models\User::class, 'enableAutoLogin' => true],
],
'params' => require __DIR__ . '/params.php',
];
Override per environment by require-ing and merging a web-local.php — this is Yii's .env equivalent, and it's why params.php exists.
Controllers: behaviors are your middleware. Verb filtering and access control attach to actions by ID, not by route prefix:
public function behaviors(): array
{
return [
'verbs' => ['class' => yii\filters\VerbFilter::class,
'actions' => ['delete' => ['POST']]],
'access' => ['class' => yii\filters\AccessControl::class,
'rules' => [
['allow' => true, 'actions' => ['index', 'view'], 'roles' => ['?']],
['allow' => true, 'roles' => ['@']],
]],
];
}
CSRF is on by default via yii\web\Request::enableCsrfValidation and ActiveForm/Html::beginForm() emit the token. Do not hand-roll <form> tags or you lose it.
ActiveRecord is not Eloquent — the query object is explicit. find() returns an ActiveQuery; nothing executes until a terminal method:
$posts = Post::find()
->where(['status' => Post::STATUS_PUBLISHED])
->andWhere(['>=', 'published_at', $since])
->with(['author', 'tags']) // eager loading, same N+1 fix as with() in Eloquent
->orderBy(['published_at' => SORT_DESC])
->limit(20)
->all(); // ->one(), ->count(), ->exists(), ->asArray()->all()
$post = Post::findOne($id);
$post->title = 'New';
if (!$post->save()) { // validate() runs first; returns bool
$errors = $post->getErrors();
}
Relations are declared as methods returning ActiveQuery (hasMany, hasOne, viaTable), and mass assignment is gated by rules()/scenarios, not $fillable — the equivalent safeguard is load():
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
Wrap multi-model writes in Yii::$app->db->transaction(function () { ... }).
Migrations live in @app/migrations by default and have up()/down(). Use safeUp()/safeDown() for DDL that isn't transactional on your engine. php yii migrate/create create_post_table scaffolds it; php yii migrate/up --interactive=0 is the CI form.
Views compose through widgets, not components. yii\grid\GridView, yii\widgets\ActiveForm, yii\widgets\Pjax, yii\helpers\Html/Url. Layouts are set with public $layout = 'main' on the controller, and $this->title, $this->registerMetaTag() fill the layout.
RBAC is built in and file-or-DB backed. Yii::$app->authManager->createRole('editor'), ->assign($role, $userId), then Yii::$app->user->can('post.update'). For anything under 50 permissions, seed it from a console command rather than clicking through the UI.
Console commands are controllers. php yii report/daily maps to app\commands\ReportController::actionDaily(). This is where you put cron jobs, imports, and backfills — not in web controllers guarded by an IP check.
Integrating with a Laravel estate. Do not co-resident the two frameworks in one process. Yii's every component reaches for Yii::$app, so instantiating one pulls in the bootstrap, and you end up with two containers, two error handlers, two session implementations, and competing global state. The clean seams are:
yii2-queue on a shared Redis/AMQP broker. This is the only pattern I would defend in review.users, Yii owns legacy_*. Migrations run from exactly one side.user component validates tokens rather than sharing Laravel sessions.Autoloading both trees works — yii\ and Illuminate\ don't collide — but "it autoloads" is not an integration.
Pin to ≥ 2.0.55 for CVE-2026-39850. Internal variables in View::renderPhpFile() and ErrorHandler::renderFile() were not isolated and could be displaced by parameter collisions, letting an attacker influence included file paths. If you render user-influenced templates or dynamic error pages, this is remotely relevant. Check the actual resolved version, not the constraint:
composer show yiisoft/yii2 --format=json | grep -i '"version"'
require yiisoft/yii2-dev is a mistake. It's the monorepo scaffolding package. Nothing will break immediately and everything will break eventually.
Yii::$app is global static state, and it is the framework's defining constraint. You cannot unit-test a Yii component in isolation without a bootstrapped application. Test with Codeception's yii2 module (functional/acceptance suites) or accept integration-level tests. This is the single largest day-to-day tax relative to Laravel, and there's no clean workaround — the extension point is Yii::$container->set() for swapping implementations, not constructor injection.
Debugging kit. Yii::debug($msg, 'category'), log targets in the log component, yii\helpers\VarDumper::dump($x, 10, true) for var_dump that survives arrays and objects, and yii2-debug's toolbar in the browser — install it in dev only, and confirm it's excluded from require in production.
Extension points, in order of preference. (1) Yii::$container->set('yii\mail\MailerInterface', MyMailer::class) for DI overrides. (2) Behaviors (yii\base\Behavior) for reusable per-class augmentation — this is the idiomatic replacement for traits, because behaviors are configurable. (3) Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, ...) for cross-cutting hooks, registered in bootstrap config. (4) Subclassing a component and reassigning its class in config. Prefer 1 and 4; events get invisible fast.
No $appends / accessor magic. Eloquent's getFullNameAttribute() and $appends have no equivalent. Yii uses getFullName() (callable as $model->fullName via __get) and fields()/extraFields() to control array/JSON serialization. If you're building an API, define fields() explicitly or toArray() will leak columns.
Asset bundles are the most common "works locally, 404 in prod" cause. assetManager->forceCopy is for dev; production needs the publish path writable and appendTimestamp set for cache busting. Symptom: CSS/JS loads in the dev server and 404s behind nginx.
Recent releases changed behavior in ways that break CI. 2.0.54 raised the PHP minimum from 7.3 to 7.4 and made the fixture loader throw instead of silently skipping missing fixtures — a previously-passing suite can go red on upgrade. It also added ArrayDataProvider support for path keys and BaseStringHelper::convertIniSizeToBytes(). Read framework/CHANGELOG.md before bumping; the release notes are short but the behavioral changes are real.
PHP 7.4 framework vs. newer templates. The framework supports 7.4+, but the refreshed basic/advanced templates target newer PHP. Don't assume the template's composer.json platform constraints describe what the framework supports — or vice versa. Static analysis annotations (PHPStan/Psalm generics, conditional types) have improved substantially across 2.0.53–2.0.55 but are explicitly work in progress; expect gaps and keep a baseline.
Be honest with your team about the trajectory. 14.3k stars, BSD-3-Clause, a Definitive Guide plus PDF plus a 15-minute docs mirror, working CI and a documented private security-reporting process — the maturity signals are genuinely strong. But the recent release cadence is PHP-version compatibility, annotation work, obsolete-code removal, and bug fixes, and the README carries Yii 3 branding. That is a maintenance-mode profile with a second migration horizon behind it. If you're adopting deliberately, ask why not Yii 3, and put an explicit review date on the decision.
Talent and tooling fit. The Yii talent pool is materially smaller than Laravel's, and your observability, CI, and static-analysis templates are almost certainly Laravel-shaped. Budget for the gap in onboarding time rather than discovering it during an incident.
How can I help you explore Laravel packages today?