davidhirtz / yii2-skeleton
Extended framework and admin panel based on Yii 2.0 framework
Package info
github.com/davidhirtz/yii2-skeleton
Type:yii2-extension
pkg:composer/davidhirtz/yii2-skeleton
Requires
- php: ^8.3
- ext-intl: *
- ext-json: *
- ext-openssl: *
- ext-simplexml: *
- ext-xmlwriter: *
- davidhirtz/yii2-datetime-behavior: ^1.1
- guzzlehttp/guzzle: ^7.9
- jfcherng/php-diff: ^6.9
- mikehaertl/php-shellcommand: ^1.7
- robthree/twofactorauth: ^3.0
- seld/cli-prompt: ^1.0
- sentry/sentry: ^4.15
- symfony/mailer: ^7.4 || ^8.0
- symfony/polyfill-mbstring: ^1.31
- yiisoft/yii2: ^2.0
Requires (Dev)
- components/jquery: ^3.7
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
- symfony/browser-kit: ^7.4
- symfony/css-selector: ^7.4
- symfony/http-client: ^7.4 || ^8.0
- symfony/resend-mailer: ^7.4 || ^8.0
- yiisoft/yii2-debug: ^2.1
Suggests
- components/jquery: The Yii debugger's panels register their grid filters as jQuery, which the skeleton does not ship
Provides
Conflicts
None
Replaces
None
- dev-main / 3.x-dev
- 3.4.0
- 3.3.0
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.0
- v2.x-dev
- 2.6.9
- 2.6.8
- 2.6.7
- 2.6.6
- 2.6.5
- 2.6.4
- 2.6.3
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.8
- 2.5.7
- 2.5.6
- 2.5.5
- 2.5.4
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.10
- 2.4.9
- 2.4.8
- 2.4.7
- 2.4.6
- 2.4.5
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.20
- 2.3.19
- 2.3.18
- 2.3.17
- 2.3.16
- 2.3.15
- 2.3.14
- 2.3.13
- 2.3.12
- 2.3.11
- 2.3.10
- 2.3.9
- 2.3.8
- 2.3.7
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.7
- 2.2.6
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.23
- 2.1.22
- 2.1.21
- 2.1.20
- 2.1.19
- 2.1.18
- 2.1.17
- v2.1.16
- v2.1.15
- v2.1.14
- v2.1.13
- v2.1.12
- v2.1.11
- v2.1.10
- v2.1.9
- v2.1.8
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.14
- v2.0.13
- v2.0.12
- v2.0.11
- v2.0.10
- v2.0.9
- v2.0.8
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.x-dev
- 1.9.6
- 1.9.5
- v1.9.4
- v1.9.3
- v1.9.2
- v1.9.1
- v1.9.0
- v1.8.3
- v1.8.2
- v1.8.1
- v1.8.0
- v1.7.5
- v1.7.4
- v1.7.3
- v1.7.2
- v1.7.1
- v1.7.0
- v1.6.8
- v1.6.7
- v1.6.6
- v1.6.5
- v1.6.4
- v1.6.3
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.22
- v1.5.21
- v1.5.20
- v1.5.19
- v1.5.18
- v1.5.17
- v1.5.16
- v1.5.15
- v1.5.14
- v1.5.13
- v1.5.12
- v1.5.11
- v1.5.10
- v1.5.9
- v1.5.8
- v1.5.7
- v1.5.6
- v1.5.5
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.18
- v1.4.17
- v1.4.16
- v1.4.15
- v1.4.14
- v1.4.13
- v1.4.12
- v1.4.11
- v1.4.10
- v1.4.9
- v1.4.8
- v1.4.7
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.18
- v1.3.17
- v1.3.16
- v1.3.15
- v1.3.14
- v1.3.13
- v1.3.12
- v1.3.11
- v1.3.10
- v1.3.9
- v1.3.8
- v1.3.7
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
This package is auto-updated.
Last update: 2026-09-28 15:09:40 UTC
README
The core of a Yii 2 based admin platform: two application classes, a database layer (Db\ActiveRecord,
Db\ActiveQuery, migrations with backups), users with roles, two-factor authentication and rate-limited login, an admin
module with a navbar, aside, grids, forms and a fulltext search, an HTML and widget layer built on htmx 4, translated
and custom attributes, redirects, a trail, a sitemap and a console. Every other davidhirtz/yii2-* bundle requires it.
It depends on yiisoft/yii2, symfony/mailer, sentry/sentry, robthree/twofactorauth and
tinymce/tinymce, and needs PHP 8.3 with intl, openssl, simplexml and xmlwriter. Image processing lives in
davidhirtz/yii2-media.
Installation
composer require davidhirtz/yii2-skeleton
The skeleton has no extra.bootstrap: it is the application. The two entry scripts build it from a configuration
array; Base\Traits\ApplicationTrait::preInitInternal() merges the core components, aliases and the admin module into
it and then reads config/params.php and config/db.php from basePath.
// web/index.php (new Hirtz\Skeleton\Web\Application(require __DIR__ . '/../config/web.php'))->run(); // yii (console entry point, executable) exit((new Hirtz\Skeleton\Console\Application(require __DIR__ . '/config/console.php'))->run());
Then, from the project root:
./yii params # writes cookieValidationKey and passwordPepper into config/params.php ./yii migrate/config # writes config/db.php ./yii migrate # applies every registered migration, backing up first ./yii user/create # the first account, prompted or with --name --email --password ./yii search/rebuild # after adding a searchable model
Web\Application serves web/ (@webroot), routes to App\Controllers, and turns on the debug module under YII_DEBUG.
Console\Application routes to App\Commands, drops the user and session components, and registers the commands
below. Both throw from current() when reached under the wrong SAPI; Web\User::current() and Web\Request::current()
answer null there instead.
Project layout
| Path | Alias | Holds |
|---|---|---|
app/Controllers/ |
@app/Controllers |
App\Controllers\*, the web controllers (site/index is SiteController::actionIndex()) |
app/Commands/ |
@app/Commands |
App\Commands\*, console commands |
app/Migrations/ |
@App/Migrations (registered during a migration run only) |
App\Migrations\M..., created with ./yii migrate/create Name |
app/ |
@app |
everything else under App\: models, a project admin module (App\Modules\Admin\Module extends Base\Module, views under @views/admin) |
resources/views/ |
@views |
views and layouts; @views/<controller id>/<view>.php |
messages/ |
@messages |
messages/<language>/app.php, the project's app category |
config/params.php, config/db.php |
read by the application; both gitignored | |
runtime/, web/ |
@runtime, @webroot |
logs, backups, uploads temp, maintenance stub; published assets, attachments/ |
Configuration
config/params.php:
| Key | Default | Meaning |
|---|---|---|
cookieValidationKey |
required | Yii's request key; ./yii params generates it |
passwordPepper |
none | appended to every password before hashing; ./yii params/pepper |
secretKey |
cookieValidationKey |
encrypts 2FA secrets and signs tokens |
adminAlias |
admin |
the URL prefix of the admin module |
allowedHosts |
none | comma-separated host names a request may carry (fnmatch() patterns: www.example.com, *.example.com); any other gets a 400 before routing, local hosts always pass. Without it, or a pinned urlManager.hostInfo, links built from the request (password resets) name whatever host it claimed, and the admin says so |
email |
hostmaster@<server name> |
the sender of every mail |
mailerDsn |
sendmail://default |
the Symfony mailer transport; any installed bridge's scheme (resend+api://KEY@default with symfony/resend-mailer and symfony/http-client); native://default where the host's sendmail has no -bs mode |
cookieDomain |
none | the Domain of the session and auth cookies |
cacheKeyPrefix |
none | keyPrefix of the cache component |
sentryDsn |
none | adds Log\SentryTarget under components.log.targets.sentry |
twoFactorAuthenticationIssuer |
the application name | shown in the authenticator app |
registryUrl, registryKey |
none | where ./yii registry/push reports the installation |
modules.admin (Modules\Admin\Module):
| Property | Default | Meaning |
|---|---|---|
enableSearch |
true |
the navbar search, the search actions and the index writes |
languages |
null (the i18n languages) |
the languages the admin is offered in; one language hides the picker |
languageSessionKey |
language |
session key of the picked admin language |
asideCookieName, asideCookieSecure |
_aside, null |
the collapsed-aside cookie |
trailLifetime, userLoginLifetime |
false |
seconds trail/clear and user-login/clear keep rows for |
components.user (Web\User): enableLogin, enableSignup (false), enablePasswordReset, enableUnconfirmedEmailLogin,
enableTwoFactorAuthentication, enableUserEnumerationProtection (all true unless noted), loginAttemptLimit (10, 0 off)
and loginAttemptDuration (900 s) counted per email and IP in the cache, cookieLifetime (30 days), cookieSecure (null
derives from the request), disableRbacForGuests, disableRbacForOwner. The identity cookie is _auth, the session cookie _session.
Other components the skeleton configures: request (Web\Request, environments maps host patterns to local and stage,
trustedHosts must be set behind a proxy), urlManager (Web\UrlManager: i18nUrl, defaultLanguage, draftSubdomain,
redirectMap), i18n (I18n\I18N::$languages), db (Db\Connection: backupOnMigration, backupPath, maxBackups),
session (Web\DbSession), search (Search\Search::$models, $driver), sitemap (Sitemap\Sitemap::$sitemaps, urls,
views, useSitemapIndex), upload (Upload\Upload: path, maxSize, enableStreamUploads, uploadLimit),
view (Web\View::$titleTemplate), log (targets file and sentry).
Content Security Policy
The admin sends a strict policy, the contentSecurityPolicy component (Web\ContentSecurityPolicy):
script-src 'self' 'strict-dynamic' 'nonce-…'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'. Every script
Web\View renders carries the nonce (registerJs(), registerJsFile(), asset bundles, registerJsModule()); a script
added by hand has to as well, $this->renderScript($js) or nonce="<?= $this->nonce ?>". An inline event handler
(onclick) or a javascript: URL is refused. 'strict-dynamic' trusts whatever a trusted script loads itself, so a
bundle or project widens only the other directives, from its Bootstrap or the component's configuration:
Hirtz\Skeleton\Web\Application::current()->getContentSecurityPolicy() ->addSource('frame-src', 'https://www.youtube-nocookie.com');
A frontend page sends Web\Controller::$contentSecurityPolicy,
frame-ancestors 'self'; object-src 'none'; base-uri 'self', which restricts no script and so survives a page cached for
every visitor. A controller whose pages are never cached as a whole can send the strict policy instead
($strictContentSecurityPolicy = true); a cached page cannot, since every visitor would get the nonce it was cached with.
Strict-Transport-Security is the web server's to send, for every response of the host. A project whose server does
not sets Web\Controller::$strictTransportSecurity = 'max-age=31536000'; both at once is a duplicate header scanners reject.
A model is configured through the container rather than subclassed; a type's name is a Yii::t() result, hence the closure:
'container' => [ 'definitions' => [ Hirtz\Skeleton\Models\User::class => [ 'customAttributes' => [ Hirtz\Skeleton\Models\CustomAttributes\TextCustomAttribute::make('company')->max(100), ], ], App\Models\Product::class => [ 'i18nAttributes' => ['name', 'slug'], 'types' => fn (): array => [ Hirtz\Skeleton\Models\Types\Type::make(1)->name(Yii::t('app', 'PRODUCT_TYPE_PHYSICAL')), ], ], ], ],
Console commands
| Command | Purpose |
|---|---|
migrate, migrate/create, migrate/config, migrate/backup, migrate/restore |
migrations with a backup first; --skipBackup, --dbFile, --upgradeFile |
params, params/cookie, params/pepper, params/create, params/update, params/delete |
maintain config/params.php |
user/create, user/password <email> |
create an account, set a password (--password or YII_USER_PASSWORD) |
upgrade/passwords |
mail a reset link to every user without a password |
search/rebuild, search/clear |
the fulltext index, optionally --models=Entry,Category |
trail/clear, trail/optimize, trail/update-models |
trail retention and class renames |
user-login/clear, user-token/clear, upload/clear |
login history, expired tokens, abandoned uploads |
redirect/clean |
delete redirect loops and shorten chains; --hosts, --dryRun |
maintenance/enable, maintenance/disable, maintenance |
the pre-rendered maintenance page (runtime/maintenance.php) |
registry/push, registry/show |
report the installation to a version registry; --url, --strict |
asset/clear, email/test <address>, message |
published assets, the mailer, message extraction |
The admin module
Modules\Admin\Module is mounted as admin under params.adminAlias. Access is RBAC: one permission per managed model,
named after it (user, redirect, authUpdate, trailIndex, system), and three flat roles, admin (everything),
manager (everything but system and tenant) and, from the cms bundle, author. A role lists permissions, never
another role. A project adds a permission with Db\Traits\MigrationTrait::addPermission() and an AUTH_<NAME>_DESCRIPTION
message key, guards a controller with AccessControl naming Model::AUTH_<MODEL>, and never passes a record to can().
Extension points: a bundle module implementing Modules\Admin\ModuleInterface contributes aside(Nav) items and
dashboard(Dashboard) panels; any widget is changed from the outside through Widgets\Widget::EVENT_CONFIGURE
(Helpers\EventHelper::on(NavBar::class, Widget::EVENT_CONFIGURE, fn (NavBar $navBar) => ...)), any web controller
through Web\Controller::EVENT_CONFIGURE; Modules\Admin\Controllers\DashboardController::addRoles() widens the
dashboard's access rule. Models opt into features by interface plus trait: AdminModelInterface, TypeAttributeInterface,
StatusAttributeInterface, CustomAttributeInterface, TranslationInterface, TrailModelInterface, SearchableInterface.