yiisoft/yii2
Yii 2 is a fast, secure, and high-performance PHP framework for building modern web applications. It offers powerful caching, MVC architecture, robust security, and a rich ecosystem of extensions and tools for rapid development.
Yii2 is a full-stack framework, not a drop-in library — "installing the package" means bootstrapping an application skeleton around it. The core repo (yiisoft/yii2) is a read-only subsplit of the framework; Gii, Debug, Bootstrap, and mailer are separate packages pulled in by the app template.
Minimal steps
composer create-project --prefer-dist yiisoft/yii2-app-basic myapp
cd myapp
php yii serve # http://localhost:8080
For API-first work use the advanced template instead (yiisoft/yii2-app-advanced), which gives you frontend/, backend/, common/, and a shared environments/ overlay for per-target config.
Where to look first (in this order)
| Path | What it is | Laravel equivalent |
|---|---|---|
web/index.php |
Entry point; sets YII_DEBUG, YII_ENV, loads config |
public/index.php + bootstrap/app.php |
config/web.php |
The whole app: components, modules, URL rules, DI | config/*.php + AppServiceProvider |
config/db.php |
PDO DSN/credentials | .env + config/database.php |
yii (root script) |
Console entry point, uses config/console.php |
artisan |
controllers/SiteController.php |
Reference controller | app/Http/Controllers |
models/, views/ |
ActiveRecord + PHP/Twig templates | Eloquent + Blade |
First use case: ship one CRUD slice end-to-end
config/web.php:
if (YII_ENV_DEV) {
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = ['class' => \yii\gii\Module::class];
}
php yii migrate/create create_post → edit migrations/ → php yii migrate./gii, generate a Model from the table, then a CRUD from that model.PostController and views/post/_form.php. That generated code is the idiomatic pattern for the rest of the codebase — copy its style rather than importing Laravel habits.The single fastest way to get productive is to generate two or three CRUDs with Gii and read every line it emits: access control via behaviors(), Yii::$app->request->post() binding, ActiveForm, GridView, and flash messages all show up there.
Controllers and actions
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::class,
'rules' => [
['allow' => true, 'actions' => ['index', 'view'], 'roles' => ['?']],
['allow' => true, 'roles' => ['@']],
],
],
'verbs' => [
'class' => \yii\filters\VerbFilter::class,
'actions' => ['delete' => ['POST']],
],
];
}
Behaviors are Yii's middleware/trait hybrid — they attach to the component and hook its events (beforeAction, afterAction). Prefer them over rewriting base controller classes; they're composable per-controller and are how auth, caching, and rate limiting are normally wired.
ActiveRecord
// Relations drive everything: with() for eager loading, indexBy for keyed results
$posts = Post::find()
->with(['author', 'comments'])
->where(['status' => Post::STATUS_PUBLISHED])
->orderBy(['created_at' => SORT_DESC])
->indexBy('id')
->limit(20)
->all();
// Bulk ops DO NOT run validation, behaviors, or events
Post::updateAll(['status' => Post::STATUS_ARCHIVED], ['<', 'created_at', $cutoff]);
Drop in TimestampBehavior and BlameableBehavior on nearly every model — they replace the created_at/updated_at/created_by boilerplate you'd otherwise write per table:
public function behaviors()
{
return [
\yii\behaviors\TimestampBehavior::class,
[
'class' => \yii\behaviors\BlameableBehavior::class,
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['created_by'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_by'],
],
],
];
}
Forms: separate the write model from the table
The biggest structural shift from Laravel form requests is that Yii expects a yii\base\Model subclass for anything with non-1:1 input (multi-step wizards, search filters, signup + profile in one submit):
class SignupForm extends Model
{
public $username, $email, $password;
public function rules()
{
return [
[['username', 'email', 'password'], 'required'],
['email', 'email'],
['username', 'unique', 'targetClass' => User::class, 'targetAttribute' => 'username'],
['password', 'string', 'min' => 8],
];
}
public function signup()
{
if (!$this->validate()) {
return null;
}
$user = new User(['username' => $this->username, 'email' => $this->email]);
$user->setPassword($this->password);
return $user->save() ? $user : null;
}
}
Search/filter forms extend yii\base\Model and are fed to ActiveDataProvider, which is what powers GridView and pagination — this is the pattern in every Gii-generated search() model.
Data providers and grids
$dataProvider = new ActiveDataProvider([
'query' => Post::find()->with('author'),
'pagination' => ['pageSize' => 20],
'sort' => ['defaultOrder' => ['created_at' => SORT_DESC]],
]);
return $this->render('index', ['dataProvider' => $dataProvider, 'searchModel' => $searchModel]);
REST APIs: extend yii\rest\ActiveController, set $modelClass, and you get index/view/create/update/delete with content negotiation, pagination headers, and HTTP caching for free.
class PostController extends \yii\rest\ActiveController
{
public $modelClass = 'app\models\Post';
public $serializer = [
'class' => \yii\rest\Serializer::class,
'collectionEnvelope' => 'items',
];
}
Wire it in config/web.php via urlManager rules with 'parseRequest' => false on the controller and 'pluralize' => false on the rule if you want explicit paths. Add yii\filters\auth\HttpBearerAuth as a behavior for token auth.
Migrations and console work
php yii migrate/create add_index_to_post
php yii migrate --interactive=0 # CI / deploy scripts
php yii migrate/down 1
Anything scheduled, batch, or long-running belongs in a console controller under commands/ and gets called as php yii report/daily. In cron, always pass --interactive=0 and set YII_ENV=prod. For real background work, pull yiisoft/yii2-queue (DB or Redis driver) rather than shelling out to exec().
Config, DI, and environments
// config/web.php — components are lazily built and act as singletons
'components' => [
'cache' => ['class' => \yii\caching\ApcCache::class],
'db' => require __DIR__ . '/db.php',
'mailer' => ['class' => \yii\symfonymailer\Mailer::class, 'useFileTransport' => YII_ENV_DEV],
],
Bind interfaces in the bootstrap section, not in bootstrap files:
Yii::$container->set(\app\contracts\PaymentGateway::class, \app\services\StripeGateway::class);
Use config/web-local.php (basic template) or environments/ (advanced) for per-machine overrides, and commit only the non-local files. Wrap environment conditionals around YII_ENV_DEV rather than hardcoding hostnames.
The framework is in bugfix-only maintenance. Releases continue (recent tags address PHP 8.3/8.4 compatibility and security patches), but do not architect around new first-party features arriving. Pin yiisoft/yii2 to a caret constraint, run composer outdated deliberately, and treat major extension abandonment as a planning risk — yii2-swiftmailer is already superseded by yii2-symfonymailer. Verify the exact PHP range in the tag's composer.json before upgrading your runtime; the framework's own support matrix lags new PHP releases by a minor version or so.
Yii::$app is a service locator, not a container-first design. You will see Yii::$app->db, Yii::$app->user, Yii::$app->request everywhere. Do not fight it by injecting everything; instead isolate it behind your own service classes if you want testability. Yii::$container exists and works, but it only auto-resolves constructor dependencies — it does not rewrite the framework's internal Yii::$app lookups.
PRG and validation. $model->save() returns false on validation failure with errors on the model; $model->load(Yii::$app->request->post()) only populates safe attributes (those with a rules() entry). If a field silently refuses to save, it's almost always a missing rule — not a DB issue.
Routing 404s on a fresh install are almost always the web server. urlManager with 'enablePrettyUrl' => true, 'showScriptName' => false needs an nginx try_files $uri $uri/ /index.php?$args; or the Apache .htaccess that ships with the template. php yii serve works out of the box, which makes this a deploy-time trap.
Gii and Debug must be env-gated. Both are registered on bootstrap in the stock config; if you copy config/web.php to production without the YII_ENV_DEV guard you have shipped a code generator to the internet. Add an IP allowlist to the Debug module as well — it captures request bodies and SQL.
CSRF breaks JSON APIs. yii\web\Controller::$enableCsrfValidation is on by default; REST clients without a token get 400s. Disable it only on the API controller (or switch to cookie-based CSRF with X-CSRF-Token and HttpBearerAuth). Read payloads with Yii::$app->request->getBodyParams(), not ->post(), so JSON and form-encoded both work.
N+1 is invisible and expensive. $post->author->name inside a GridView loop fires a query per row. Always ->with(['author']) on the ActiveDataProvider query, and add ->asArray() on read-only listings for a measurable memory win.
updateAll(), deleteAll(), and createCommand() bypass the ORM layer entirely — no validation, no behaviors, no events, no afterSave. Fine for backfills; a bug if you rely on TimestampBehavior.
Migrations are not reversible by default. safeUp()/safeDown() are the ones that behave transactionally on MySQL. Use them for anything touching more than one table, and never edit a migration that has already run in shared environments — write a new one.
Config merging is array-based and shallow-ish. Later entries in required config files overwrite whole keys. Debug print_r of the merged result (var_dump(Yii::$app->params)) rather than assuming deep merge semantics, especially when a module overrides a component.
Debugging toolkit. Yii::debug($msg, $category) writes to the debug panel, Yii::error() to runtime/logs/app.log via yii\log\FileTarget. Set 'levels' and 'categories' on the log target or the file grows unbounded — a classic post-launch surprise. Enable yii\debug\Module in dev and read the "DB" and "Timing" panels; they replace most ad-hoc dd() habits.
Testing is Codeception-first. The app templates ship with Codeception suites (tests/unit, tests/functional) that bootstrap the Yii application for you. Pure PHPUnit requires you to require the Yii autoloader and construct an application instance manually — doable but you'll be reimplementing the fixture layer that yii2-codeception / yii2-faker already provide. Fixtures via yii\test\ActiveFixture with load()/unload() are how DB tests stay isolated.
Extension ecosystem conventions. Community packages are named vendor/yii2-* and are configured as components (e.g. 'gridview' => ['class' => 'kartik\grid\Module']). Before writing UI, check kartik-v/yii2-grid, yii2-widget-select2, yii2-date-range, and yii2-queue — they're the de-facto standards and save days. Conversely, check the last-commit date; a large share of Yii2 extensions are unmaintained and will pin you to older dependency trees via composer.json conflicts.
Asset bundles and the @web alias. Asset publishing copies to web/assets/<hash> via assetManager. In containers or read-only filesystems, set 'assetManager' => ['basePath' => '@webroot/assets', 'baseUrl' => '@web/assets', 'linkAssets' => true] and make the directory writable — otherwise styles silently 404 in production while working locally.
How can I help you explore Laravel packages today?