Repository: yiisoft/yii2 Branch: master Commit: f59774fd3b65 Files: 2161 Total size: 18.8 MB Directory structure: gitextract_0uomsu8b/ ├── .appveyor.yml ├── .codecov.yml ├── .dockerignore ├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── actions/ │ │ ├── php-setup/ │ │ │ └── action.yml │ │ └── phpunit/ │ │ └── action.yml │ └── workflows/ │ ├── build.yml │ ├── ci-mariadb.yml │ ├── ci-mssql.yml │ ├── ci-mysql.yml │ ├── ci-node.yml │ ├── ci-oracle.yml │ ├── ci-pgsql.yml │ ├── ci-sqlite.yml │ ├── linter.yaml │ └── static.yml ├── .gitignore ├── .gitlab-ci.yml ├── .well-known/ │ └── funding-manifest-urls ├── Dockerfile ├── LICENSE.md ├── README.md ├── ROADMAP.md ├── build/ │ ├── .htaccess │ ├── build │ ├── build.bat │ ├── build.xml │ └── controllers/ │ ├── ClassmapController.php │ ├── DevController.php │ ├── MimeTypeController.php │ ├── PhpDocController.php │ ├── ReleaseController.php │ ├── TranslationController.php │ ├── Utf8Controller.php │ └── views/ │ └── translation/ │ └── report_html.php ├── code-of-conduct.md ├── composer.json ├── contrib/ │ └── completion/ │ ├── bash/ │ │ └── yii │ └── zsh/ │ └── _yii ├── docs/ │ ├── documentation_style_guide.md │ ├── guide/ │ │ ├── README.md │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── glossary.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-json.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-form-javascript.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-formatting.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-filtering-collections.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-best-practices.md │ │ ├── security-cryptography.md │ │ ├── security-overview.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-fixtures.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── tutorial-console.md │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-docker.md │ │ ├── tutorial-i18n.md │ │ ├── tutorial-mailing.md │ │ ├── tutorial-performance-tuning.md │ │ ├── tutorial-shared-hosting.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ ├── tutorial-yii-as-micro-framework.md │ │ └── tutorial-yii-integration.md │ ├── guide-ar/ │ │ ├── README.md │ │ ├── intro-yii.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ └── start-workflow.md │ ├── guide-de/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ └── translators.json │ ├── guide-es/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── glossary.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-multiple-models.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-pagination.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-fixtures.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── translators.json │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-mailing.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ └── tutorial-yii-integration.md │ ├── guide-fr/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── advanced-app-configs.graphml │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-formatting.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-best-practices.md │ │ ├── security-cryptography.md │ │ ├── security-overview.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── translators.json │ │ └── tutorial-i18n.md │ ├── guide-id/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── images/ │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-overview.md │ │ └── translators.json │ ├── guide-it/ │ │ ├── README.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ └── start-installation.md │ ├── guide-ja/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── glossary.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-json.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── advanced-app-configs.graphml │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-form-javascript.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-formatting.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-filtering-collections.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-best-practices.md │ │ ├── security-cryptography.md │ │ ├── security-overview.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-fixtures.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── translators.json │ │ ├── tutorial-console.md │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-docker.md │ │ ├── tutorial-i18n.md │ │ ├── tutorial-mailing.md │ │ ├── tutorial-performance-tuning.md │ │ ├── tutorial-shared-hosting.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ ├── tutorial-yii-as-micro-framework.md │ │ └── tutorial-yii-integration.md │ ├── guide-pl/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── db-active-record.md │ │ ├── db-migrations.md │ │ ├── glossary.md │ │ ├── helper-overview.md │ │ ├── images/ │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-form-javascript.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-pagination.md │ │ ├── rest-error-handling.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-overview.md │ │ ├── security-overview.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-overview.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── translators.json │ │ ├── tutorial-mailing.md │ │ ├── tutorial-shared-hosting.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ └── tutorial-yii-as-micro-framework.md │ ├── guide-pt-BR/ │ │ ├── README.md │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── advanced-app-configs.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-data-providers.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-overview.md │ │ ├── translators.json │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-docker.md │ │ ├── tutorial-shared-hosting.md │ │ └── tutorial-yii-integration.md │ ├── guide-ru/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── glossary.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-json.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-form-javascript.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-formatting.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-filtering-collections.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-best-practices.md │ │ ├── security-cryptography.md │ │ ├── security-overview.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-fixtures.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── translators.json │ │ ├── tutorial-console.md │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-docker.md │ │ ├── tutorial-i18n.md │ │ ├── tutorial-mailing.md │ │ ├── tutorial-performance-tuning.md │ │ ├── tutorial-shared-hosting.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ ├── tutorial-yii-as-micro-framework.md │ │ └── tutorial-yii-integration.md │ ├── guide-tr/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── intro-yii.md │ │ ├── start-prerequisites.md │ │ └── translators.json │ ├── guide-uk/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-fragment.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── images/ │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── runtime-sessions-cookies.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-models.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── tutorial-console.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ └── tutorial-yii-integration.md │ ├── guide-uz/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-page.md │ │ ├── images/ │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-workflow.md │ │ └── structure-controllers.md │ ├── guide-vi/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── images/ │ │ │ ├── advanced-app-configs.graphml │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ └── structure-views.md │ ├── guide-zh-CN/ │ │ ├── README.md │ │ ├── blocktypes.json │ │ ├── caching-data.md │ │ ├── caching-fragment.md │ │ ├── caching-http.md │ │ ├── caching-overview.md │ │ ├── caching-page.md │ │ ├── concept-aliases.md │ │ ├── concept-autoloading.md │ │ ├── concept-behaviors.md │ │ ├── concept-components.md │ │ ├── concept-configurations.md │ │ ├── concept-di-container.md │ │ ├── concept-events.md │ │ ├── concept-properties.md │ │ ├── concept-service-locator.md │ │ ├── db-active-record.md │ │ ├── db-dao.md │ │ ├── db-migrations.md │ │ ├── db-query-builder.md │ │ ├── documentation_style_guide.md │ │ ├── glossary.md │ │ ├── helper-array.md │ │ ├── helper-html.md │ │ ├── helper-overview.md │ │ ├── helper-url.md │ │ ├── images/ │ │ │ ├── advanced-app-configs.graphml │ │ │ ├── application-lifecycle.graphml │ │ │ ├── application-structure.graphml │ │ │ ├── rbac-access-check-1.graphml │ │ │ ├── rbac-access-check-2.graphml │ │ │ ├── rbac-access-check-3.graphml │ │ │ ├── rbac-hierarchy-1.graphml │ │ │ ├── rbac-hierarchy-2.graphml │ │ │ └── request-lifecycle.graphml │ │ ├── input-file-upload.md │ │ ├── input-form-javascript.md │ │ ├── input-forms.md │ │ ├── input-multiple-models.md │ │ ├── input-tabular-input.md │ │ ├── input-validation.md │ │ ├── intro-upgrade-from-v1.md │ │ ├── intro-yii.md │ │ ├── output-client-scripts.md │ │ ├── output-data-providers.md │ │ ├── output-data-widgets.md │ │ ├── output-formatting.md │ │ ├── output-pagination.md │ │ ├── output-sorting.md │ │ ├── output-theming.md │ │ ├── rest-authentication.md │ │ ├── rest-controllers.md │ │ ├── rest-error-handling.md │ │ ├── rest-quick-start.md │ │ ├── rest-rate-limiting.md │ │ ├── rest-resources.md │ │ ├── rest-response-formatting.md │ │ ├── rest-routing.md │ │ ├── rest-versioning.md │ │ ├── runtime-bootstrapping.md │ │ ├── runtime-handling-errors.md │ │ ├── runtime-logging.md │ │ ├── runtime-overview.md │ │ ├── runtime-requests.md │ │ ├── runtime-responses.md │ │ ├── runtime-routing.md │ │ ├── runtime-sessions-cookies.md │ │ ├── security-authentication.md │ │ ├── security-authorization.md │ │ ├── security-best-practices.md │ │ ├── security-cryptography.md │ │ ├── security-overview.md │ │ ├── security-passwords.md │ │ ├── start-databases.md │ │ ├── start-forms.md │ │ ├── start-gii.md │ │ ├── start-hello.md │ │ ├── start-installation.md │ │ ├── start-looking-ahead.md │ │ ├── start-prerequisites.md │ │ ├── start-workflow.md │ │ ├── structure-application-components.md │ │ ├── structure-applications.md │ │ ├── structure-assets.md │ │ ├── structure-controllers.md │ │ ├── structure-entry-scripts.md │ │ ├── structure-extensions.md │ │ ├── structure-filters.md │ │ ├── structure-models.md │ │ ├── structure-modules.md │ │ ├── structure-overview.md │ │ ├── structure-views.md │ │ ├── structure-widgets.md │ │ ├── test-acceptance.md │ │ ├── test-environment-setup.md │ │ ├── test-fixtures.md │ │ ├── test-functional.md │ │ ├── test-overview.md │ │ ├── test-unit.md │ │ ├── translators.json │ │ ├── tutorial-console.md │ │ ├── tutorial-core-validators.md │ │ ├── tutorial-docker.md │ │ ├── tutorial-i18n.md │ │ ├── tutorial-mailing.md │ │ ├── tutorial-performance-tuning.md │ │ ├── tutorial-shared-hosting.md │ │ ├── tutorial-start-from-scratch.md │ │ ├── tutorial-template-engines.md │ │ ├── tutorial-yii-as-micro-framework.md │ │ └── tutorial-yii-integration.md │ ├── internals/ │ │ ├── README.md │ │ ├── automation.md │ │ ├── bc.md │ │ ├── core-code-style.md │ │ ├── design-decisions.md │ │ ├── exception_hierarchy.vsd │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── project-organization.md │ │ ├── pull-request-qa.md │ │ ├── release.md │ │ ├── report-an-issue.md │ │ ├── schema-builder-patterns.xlsx │ │ ├── translation-status.md │ │ ├── translation-teams.md │ │ ├── translation-workflow.md │ │ ├── versions.md │ │ └── view-code-style.md │ ├── internals-es/ │ │ └── translation-workflow.md │ ├── internals-fa/ │ │ └── core-code-style.md │ ├── internals-ja/ │ │ ├── README.md │ │ ├── automation.md │ │ ├── bc.md │ │ ├── core-code-style.md │ │ ├── design-decisions.md │ │ ├── exception_hierarchy.vsd │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── project-organization.md │ │ ├── pull-request-qa.md │ │ ├── release.md │ │ ├── report-an-issue.md │ │ ├── schema-builder-patterns.xlsx │ │ ├── translation-status.md │ │ ├── translation-teams.md │ │ ├── translation-workflow.md │ │ ├── versions.md │ │ └── view-code-style.md │ ├── internals-pl/ │ │ ├── README.md │ │ ├── automation.md │ │ ├── bc.md │ │ ├── core-code-style.md │ │ ├── design-decisions.md │ │ ├── exception_hierarchy.vsd │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── project-organization.md │ │ ├── pull-request-qa.md │ │ ├── release.md │ │ ├── report-an-issue.md │ │ ├── schema-builder-patterns.xlsx │ │ ├── translation-status.md │ │ ├── translation-teams.md │ │ ├── translation-workflow.md │ │ ├── versions.md │ │ └── view-code-style.md │ ├── internals-pt-BR/ │ │ └── translation-workflow.md │ ├── internals-ru/ │ │ ├── README.md │ │ ├── automation.md │ │ ├── bc.md │ │ ├── blocktypes.json │ │ ├── core-code-style.md │ │ ├── design-decisions.md │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── project-organization.md │ │ ├── pull-request-qa.md │ │ ├── release.md │ │ ├── report-an-issue.md │ │ ├── translation-status.md │ │ ├── translation-teams.md │ │ ├── translation-workflow.md │ │ ├── versions.md │ │ └── view-code-style.md │ ├── internals-sr-Latn/ │ │ ├── automation.md │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── report-an-issue.md │ │ └── translation-workflow.md │ ├── internals-uk/ │ │ ├── automation.md │ │ ├── core-code-style.md │ │ ├── design-decisions.md │ │ ├── getting-started.md │ │ ├── git-workflow.md │ │ ├── report-an-issue.md │ │ ├── translation-workflow.md │ │ ├── versions.md │ │ └── view-code-style.md │ └── internals-uz/ │ └── translation-workflow.md ├── eslint.config.js ├── framework/ │ ├── .github/ │ │ ├── CONTRIBUTING.md │ │ ├── FUNDING.yml │ │ ├── PULL_REQUEST_TEMPLATE.md │ │ └── SECURITY.md │ ├── .gitignore │ ├── .htaccess │ ├── .meta-storm/ │ │ ├── active-record.meta-storm.xml │ │ ├── array.meta-storm.xml │ │ ├── controller.meta-storm.xml │ │ ├── db.meta-storm.xml │ │ ├── html.meta-storm.xml │ │ ├── model.meta-storm.xml │ │ ├── view.meta-storm.xml │ │ └── widgets.meta-storm.xml │ ├── .phpstorm.meta.php │ ├── BaseYii.php │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── UPGRADE.md │ ├── Yii.php │ ├── assets/ │ │ ├── yii.activeForm.js │ │ ├── yii.captcha.js │ │ ├── yii.gridView.js │ │ ├── yii.js │ │ └── yii.validation.js │ ├── base/ │ │ ├── Action.php │ │ ├── ActionEvent.php │ │ ├── ActionFilter.php │ │ ├── Application.php │ │ ├── ArrayAccessTrait.php │ │ ├── Arrayable.php │ │ ├── ArrayableTrait.php │ │ ├── BaseObject.php │ │ ├── Behavior.php │ │ ├── BootstrapInterface.php │ │ ├── Component.php │ │ ├── Configurable.php │ │ ├── Controller.php │ │ ├── DynamicContentAwareInterface.php │ │ ├── DynamicContentAwareTrait.php │ │ ├── DynamicModel.php │ │ ├── ErrorException.php │ │ ├── ErrorHandler.php │ │ ├── Event.php │ │ ├── Exception.php │ │ ├── ExitException.php │ │ ├── InlineAction.php │ │ ├── InvalidArgumentException.php │ │ ├── InvalidCallException.php │ │ ├── InvalidConfigException.php │ │ ├── InvalidParamException.php │ │ ├── InvalidRouteException.php │ │ ├── InvalidValueException.php │ │ ├── Model.php │ │ ├── ModelEvent.php │ │ ├── Module.php │ │ ├── NotSupportedException.php │ │ ├── Request.php │ │ ├── Response.php │ │ ├── Security.php │ │ ├── StaticInstanceInterface.php │ │ ├── StaticInstanceTrait.php │ │ ├── Theme.php │ │ ├── UnknownClassException.php │ │ ├── UnknownMethodException.php │ │ ├── UnknownPropertyException.php │ │ ├── UserException.php │ │ ├── View.php │ │ ├── ViewContextInterface.php │ │ ├── ViewEvent.php │ │ ├── ViewNotFoundException.php │ │ ├── ViewRenderer.php │ │ ├── Widget.php │ │ ├── WidgetEvent.php │ │ └── package.json │ ├── behaviors/ │ │ ├── AttributeBehavior.php │ │ ├── AttributeTypecastBehavior.php │ │ ├── AttributesBehavior.php │ │ ├── BlameableBehavior.php │ │ ├── CacheableWidgetBehavior.php │ │ ├── OptimisticLockBehavior.php │ │ ├── SluggableBehavior.php │ │ └── TimestampBehavior.php │ ├── caching/ │ │ ├── ApcCache.php │ │ ├── ArrayCache.php │ │ ├── Cache.php │ │ ├── CacheInterface.php │ │ ├── CallbackDependency.php │ │ ├── ChainedDependency.php │ │ ├── DbCache.php │ │ ├── DbDependency.php │ │ ├── DbQueryDependency.php │ │ ├── Dependency.php │ │ ├── DummyCache.php │ │ ├── ExpressionDependency.php │ │ ├── FileCache.php │ │ ├── FileDependency.php │ │ ├── MemCache.php │ │ ├── MemCacheServer.php │ │ ├── TagDependency.php │ │ ├── WinCache.php │ │ └── migrations/ │ │ ├── m150909_153426_cache_init.php │ │ ├── schema-mssql.sql │ │ ├── schema-mysql.sql │ │ ├── schema-oci.sql │ │ ├── schema-pgsql.sql │ │ └── schema-sqlite.sql │ ├── captcha/ │ │ ├── Captcha.php │ │ ├── CaptchaAction.php │ │ ├── CaptchaAsset.php │ │ ├── CaptchaValidator.php │ │ └── SpicyRice.md │ ├── classes.php │ ├── composer.json │ ├── console/ │ │ ├── Application.php │ │ ├── Controller.php │ │ ├── ErrorHandler.php │ │ ├── Exception.php │ │ ├── ExitCode.php │ │ ├── Markdown.php │ │ ├── Request.php │ │ ├── Response.php │ │ ├── UnknownCommandException.php │ │ ├── controllers/ │ │ │ ├── AssetController.php │ │ │ ├── BaseMigrateController.php │ │ │ ├── CacheController.php │ │ │ ├── FixtureController.php │ │ │ ├── HelpController.php │ │ │ ├── MessageController.php │ │ │ ├── MigrateController.php │ │ │ └── ServeController.php │ │ ├── runtime/ │ │ │ └── .gitignore │ │ └── widgets/ │ │ └── Table.php │ ├── data/ │ │ ├── ActiveDataFilter.php │ │ ├── ActiveDataProvider.php │ │ ├── ArrayDataProvider.php │ │ ├── BaseDataProvider.php │ │ ├── DataFilter.php │ │ ├── DataProviderInterface.php │ │ ├── Pagination.php │ │ ├── Sort.php │ │ └── SqlDataProvider.php │ ├── db/ │ │ ├── ActiveQuery.php │ │ ├── ActiveQueryInterface.php │ │ ├── ActiveQueryTrait.php │ │ ├── ActiveRecord.php │ │ ├── ActiveRecordInterface.php │ │ ├── ActiveRelationTrait.php │ │ ├── AfterSaveEvent.php │ │ ├── ArrayExpression.php │ │ ├── BaseActiveRecord.php │ │ ├── BatchQueryResult.php │ │ ├── CheckConstraint.php │ │ ├── ColumnSchema.php │ │ ├── ColumnSchemaBuilder.php │ │ ├── Command.php │ │ ├── Connection.php │ │ ├── Constraint.php │ │ ├── ConstraintFinderInterface.php │ │ ├── ConstraintFinderTrait.php │ │ ├── DataReader.php │ │ ├── DefaultValueConstraint.php │ │ ├── Exception.php │ │ ├── Expression.php │ │ ├── ExpressionBuilder.php │ │ ├── ExpressionBuilderInterface.php │ │ ├── ExpressionBuilderTrait.php │ │ ├── ExpressionInterface.php │ │ ├── ForeignKeyConstraint.php │ │ ├── IndexConstraint.php │ │ ├── IntegrityException.php │ │ ├── JsonExpression.php │ │ ├── Migration.php │ │ ├── MigrationInterface.php │ │ ├── PdoValue.php │ │ ├── PdoValueBuilder.php │ │ ├── Query.php │ │ ├── QueryBuilder.php │ │ ├── QueryExpressionBuilder.php │ │ ├── QueryInterface.php │ │ ├── QueryTrait.php │ │ ├── Schema.php │ │ ├── SchemaBuilderTrait.php │ │ ├── SqlToken.php │ │ ├── SqlTokenizer.php │ │ ├── StaleObjectException.php │ │ ├── TableSchema.php │ │ ├── Transaction.php │ │ ├── ViewFinderTrait.php │ │ ├── conditions/ │ │ │ ├── AndCondition.php │ │ │ ├── BetweenColumnsCondition.php │ │ │ ├── BetweenColumnsConditionBuilder.php │ │ │ ├── BetweenCondition.php │ │ │ ├── BetweenConditionBuilder.php │ │ │ ├── ConditionInterface.php │ │ │ ├── ConjunctionCondition.php │ │ │ ├── ConjunctionConditionBuilder.php │ │ │ ├── ExistsCondition.php │ │ │ ├── ExistsConditionBuilder.php │ │ │ ├── HashCondition.php │ │ │ ├── HashConditionBuilder.php │ │ │ ├── InCondition.php │ │ │ ├── InConditionBuilder.php │ │ │ ├── LikeCondition.php │ │ │ ├── LikeConditionBuilder.php │ │ │ ├── NotCondition.php │ │ │ ├── NotConditionBuilder.php │ │ │ ├── OrCondition.php │ │ │ ├── SimpleCondition.php │ │ │ └── SimpleConditionBuilder.php │ │ ├── cubrid/ │ │ │ ├── ColumnSchemaBuilder.php │ │ │ ├── QueryBuilder.php │ │ │ ├── Schema.php │ │ │ └── conditions/ │ │ │ └── LikeConditionBuilder.php │ │ ├── mssql/ │ │ │ ├── ColumnSchema.php │ │ │ ├── ColumnSchemaBuilder.php │ │ │ ├── DBLibPDO.php │ │ │ ├── PDO.php │ │ │ ├── QueryBuilder.php │ │ │ ├── Schema.php │ │ │ ├── SqlsrvPDO.php │ │ │ ├── TableSchema.php │ │ │ └── conditions/ │ │ │ ├── InConditionBuilder.php │ │ │ └── LikeConditionBuilder.php │ │ ├── mysql/ │ │ │ ├── ColumnSchema.php │ │ │ ├── ColumnSchemaBuilder.php │ │ │ ├── JsonExpressionBuilder.php │ │ │ ├── QueryBuilder.php │ │ │ └── Schema.php │ │ ├── oci/ │ │ │ ├── ColumnSchemaBuilder.php │ │ │ ├── Command.php │ │ │ ├── QueryBuilder.php │ │ │ ├── Schema.php │ │ │ └── conditions/ │ │ │ ├── InConditionBuilder.php │ │ │ └── LikeConditionBuilder.php │ │ ├── pgsql/ │ │ │ ├── ArrayExpressionBuilder.php │ │ │ ├── ArrayParser.php │ │ │ ├── ColumnSchema.php │ │ │ ├── JsonExpressionBuilder.php │ │ │ ├── QueryBuilder.php │ │ │ └── Schema.php │ │ └── sqlite/ │ │ ├── ColumnSchemaBuilder.php │ │ ├── Command.php │ │ ├── QueryBuilder.php │ │ ├── Schema.php │ │ ├── SqlTokenizer.php │ │ └── conditions/ │ │ ├── InConditionBuilder.php │ │ └── LikeConditionBuilder.php │ ├── di/ │ │ ├── Container.php │ │ ├── Instance.php │ │ ├── NotInstantiableException.php │ │ └── ServiceLocator.php │ ├── filters/ │ │ ├── AccessControl.php │ │ ├── AccessRule.php │ │ ├── AjaxFilter.php │ │ ├── ContentNegotiator.php │ │ ├── Cors.php │ │ ├── HostControl.php │ │ ├── HttpCache.php │ │ ├── PageCache.php │ │ ├── RateLimitInterface.php │ │ ├── RateLimiter.php │ │ ├── VerbFilter.php │ │ └── auth/ │ │ ├── AuthInterface.php │ │ ├── AuthMethod.php │ │ ├── CompositeAuth.php │ │ ├── HttpBasicAuth.php │ │ ├── HttpBearerAuth.php │ │ ├── HttpHeaderAuth.php │ │ └── QueryParamAuth.php │ ├── grid/ │ │ ├── ActionColumn.php │ │ ├── CheckboxColumn.php │ │ ├── Column.php │ │ ├── DataColumn.php │ │ ├── GridView.php │ │ ├── GridViewAsset.php │ │ ├── RadioButtonColumn.php │ │ └── SerialColumn.php │ ├── helpers/ │ │ ├── ArrayHelper.php │ │ ├── BaseArrayHelper.php │ │ ├── BaseConsole.php │ │ ├── BaseFileHelper.php │ │ ├── BaseFormatConverter.php │ │ ├── BaseHtml.php │ │ ├── BaseHtmlPurifier.php │ │ ├── BaseInflector.php │ │ ├── BaseIpHelper.php │ │ ├── BaseJson.php │ │ ├── BaseMarkdown.php │ │ ├── BaseStringHelper.php │ │ ├── BaseUrl.php │ │ ├── BaseVarDumper.php │ │ ├── Console.php │ │ ├── FileHelper.php │ │ ├── FormatConverter.php │ │ ├── Html.php │ │ ├── HtmlPurifier.php │ │ ├── Inflector.php │ │ ├── IpHelper.php │ │ ├── Json.php │ │ ├── Markdown.php │ │ ├── ReplaceArrayValue.php │ │ ├── StringHelper.php │ │ ├── UnsetArrayValue.php │ │ ├── Url.php │ │ ├── VarDumper.php │ │ ├── mimeAliases.php │ │ ├── mimeExtensions.php │ │ └── mimeTypes.php │ ├── i18n/ │ │ ├── DbMessageSource.php │ │ ├── Formatter.php │ │ ├── GettextFile.php │ │ ├── GettextMessageSource.php │ │ ├── GettextMoFile.php │ │ ├── GettextPoFile.php │ │ ├── I18N.php │ │ ├── Locale.php │ │ ├── MessageFormatter.php │ │ ├── MessageSource.php │ │ ├── MissingTranslationEvent.php │ │ ├── PhpMessageSource.php │ │ └── migrations/ │ │ ├── m150207_210500_i18n_init.php │ │ ├── schema-mssql.sql │ │ ├── schema-mysql.sql │ │ ├── schema-oci.sql │ │ ├── schema-pgsql.sql │ │ └── schema-sqlite.sql │ ├── log/ │ │ ├── DbTarget.php │ │ ├── Dispatcher.php │ │ ├── EmailTarget.php │ │ ├── FileTarget.php │ │ ├── LogRuntimeException.php │ │ ├── Logger.php │ │ ├── SyslogTarget.php │ │ ├── Target.php │ │ └── migrations/ │ │ ├── m141106_185632_log_init.php │ │ ├── schema-mssql.sql │ │ ├── schema-mysql.sql │ │ ├── schema-oci.sql │ │ ├── schema-pgsql.sql │ │ └── schema-sqlite.sql │ ├── mail/ │ │ ├── BaseMailer.php │ │ ├── BaseMessage.php │ │ ├── MailEvent.php │ │ ├── MailerInterface.php │ │ └── MessageInterface.php │ ├── messages/ │ │ ├── af/ │ │ │ └── yii.php │ │ ├── ar/ │ │ │ └── yii.php │ │ ├── az/ │ │ │ └── yii.php │ │ ├── be/ │ │ │ └── yii.php │ │ ├── bg/ │ │ │ └── yii.php │ │ ├── bs/ │ │ │ └── yii.php │ │ ├── ca/ │ │ │ └── yii.php │ │ ├── config.php │ │ ├── cs/ │ │ │ └── yii.php │ │ ├── da/ │ │ │ └── yii.php │ │ ├── de/ │ │ │ └── yii.php │ │ ├── el/ │ │ │ └── yii.php │ │ ├── es/ │ │ │ └── yii.php │ │ ├── et/ │ │ │ └── yii.php │ │ ├── fa/ │ │ │ └── yii.php │ │ ├── fi/ │ │ │ └── yii.php │ │ ├── fr/ │ │ │ └── yii.php │ │ ├── ga/ │ │ │ └── yii.php │ │ ├── he/ │ │ │ └── yii.php │ │ ├── hi/ │ │ │ └── yii.php │ │ ├── hr/ │ │ │ └── yii.php │ │ ├── hu/ │ │ │ └── yii.php │ │ ├── hy/ │ │ │ └── yii.php │ │ ├── id/ │ │ │ └── yii.php │ │ ├── it/ │ │ │ └── yii.php │ │ ├── ja/ │ │ │ └── yii.php │ │ ├── ka/ │ │ │ └── yii.php │ │ ├── kk/ │ │ │ └── yii.php │ │ ├── ko/ │ │ │ └── yii.php │ │ ├── kz/ │ │ │ └── yii.php │ │ ├── lt/ │ │ │ └── yii.php │ │ ├── lv/ │ │ │ └── yii.php │ │ ├── ms/ │ │ │ └── yii.php │ │ ├── mt/ │ │ │ └── yii.php │ │ ├── nb-NO/ │ │ │ └── yii.php │ │ ├── nl/ │ │ │ └── yii.php │ │ ├── pl/ │ │ │ └── yii.php │ │ ├── pt/ │ │ │ └── yii.php │ │ ├── pt-BR/ │ │ │ └── yii.php │ │ ├── ro/ │ │ │ └── yii.php │ │ ├── ru/ │ │ │ └── yii.php │ │ ├── sk/ │ │ │ └── yii.php │ │ ├── sl/ │ │ │ └── yii.php │ │ ├── sr/ │ │ │ └── yii.php │ │ ├── sr-Latn/ │ │ │ └── yii.php │ │ ├── sv/ │ │ │ └── yii.php │ │ ├── tg/ │ │ │ └── yii.php │ │ ├── th/ │ │ │ └── yii.php │ │ ├── tr/ │ │ │ └── yii.php │ │ ├── uk/ │ │ │ └── yii.php │ │ ├── uz/ │ │ │ └── yii.php │ │ ├── uz-Cy/ │ │ │ └── yii.php │ │ ├── vi/ │ │ │ └── yii.php │ │ ├── zh/ │ │ │ └── yii.php │ │ └── zh-TW/ │ │ └── yii.php │ ├── mutex/ │ │ ├── DbMutex.php │ │ ├── FileMutex.php │ │ ├── Mutex.php │ │ ├── MysqlMutex.php │ │ ├── OracleMutex.php │ │ ├── PgsqlMutex.php │ │ └── RetryAcquireTrait.php │ ├── rbac/ │ │ ├── Assignment.php │ │ ├── BaseManager.php │ │ ├── CheckAccessInterface.php │ │ ├── DbManager.php │ │ ├── Item.php │ │ ├── ManagerInterface.php │ │ ├── Permission.php │ │ ├── PhpManager.php │ │ ├── Role.php │ │ ├── Rule.php │ │ └── migrations/ │ │ ├── m140506_102106_rbac_init.php │ │ ├── m170907_052038_rbac_add_index_on_auth_assignment_user_id.php │ │ ├── m180523_151638_rbac_updates_indexes_without_prefix.php │ │ ├── m200409_110543_rbac_update_mssql_trigger.php │ │ ├── schema-mssql.sql │ │ ├── schema-mysql.sql │ │ ├── schema-oci.sql │ │ ├── schema-pgsql.sql │ │ └── schema-sqlite.sql │ ├── requirements/ │ │ ├── YiiRequirementChecker.php │ │ ├── requirements.php │ │ └── views/ │ │ ├── console/ │ │ │ └── index.php │ │ └── web/ │ │ ├── css.php │ │ └── index.php │ ├── rest/ │ │ ├── Action.php │ │ ├── ActiveController.php │ │ ├── Controller.php │ │ ├── CreateAction.php │ │ ├── DeleteAction.php │ │ ├── IndexAction.php │ │ ├── OptionsAction.php │ │ ├── Serializer.php │ │ ├── UpdateAction.php │ │ ├── UrlRule.php │ │ └── ViewAction.php │ ├── test/ │ │ ├── ActiveFixture.php │ │ ├── ArrayFixture.php │ │ ├── BaseActiveFixture.php │ │ ├── DbFixture.php │ │ ├── FileFixtureTrait.php │ │ ├── Fixture.php │ │ ├── FixtureTrait.php │ │ └── InitDbFixture.php │ ├── validators/ │ │ ├── BooleanValidator.php │ │ ├── CompareValidator.php │ │ ├── DateValidator.php │ │ ├── DefaultValueValidator.php │ │ ├── EachValidator.php │ │ ├── EmailValidator.php │ │ ├── ExistValidator.php │ │ ├── FileValidator.php │ │ ├── FilterValidator.php │ │ ├── ImageValidator.php │ │ ├── InlineValidator.php │ │ ├── IpValidator.php │ │ ├── NumberValidator.php │ │ ├── PunycodeAsset.php │ │ ├── RangeValidator.php │ │ ├── RegularExpressionValidator.php │ │ ├── RequiredValidator.php │ │ ├── SafeValidator.php │ │ ├── StringValidator.php │ │ ├── TrimValidator.php │ │ ├── UniqueValidator.php │ │ ├── UrlValidator.php │ │ ├── ValidationAsset.php │ │ └── Validator.php │ ├── views/ │ │ ├── _addColumns.php │ │ ├── _addComments.php │ │ ├── _addForeignKeys.php │ │ ├── _createTable.php │ │ ├── _dropColumns.php │ │ ├── _dropForeignKeys.php │ │ ├── _dropTable.php │ │ ├── _foreignTables.php │ │ ├── addColumnMigration.php │ │ ├── createJunctionMigration.php │ │ ├── createTableMigration.php │ │ ├── dropColumnMigration.php │ │ ├── dropTableMigration.php │ │ ├── errorHandler/ │ │ │ ├── callStackItem.php │ │ │ ├── error.php │ │ │ ├── exception.php │ │ │ └── previousException.php │ │ ├── messageConfig.php │ │ └── migration.php │ ├── web/ │ │ ├── Application.php │ │ ├── AssetBundle.php │ │ ├── AssetConverter.php │ │ ├── AssetConverterInterface.php │ │ ├── AssetManager.php │ │ ├── BadRequestHttpException.php │ │ ├── CacheSession.php │ │ ├── CompositeUrlRule.php │ │ ├── ConflictHttpException.php │ │ ├── Controller.php │ │ ├── Cookie.php │ │ ├── CookieCollection.php │ │ ├── DbSession.php │ │ ├── ErrorAction.php │ │ ├── ErrorHandler.php │ │ ├── ForbiddenHttpException.php │ │ ├── GoneHttpException.php │ │ ├── GroupUrlRule.php │ │ ├── HeaderCollection.php │ │ ├── HeadersAlreadySentException.php │ │ ├── HtmlResponseFormatter.php │ │ ├── HttpException.php │ │ ├── IdentityInterface.php │ │ ├── JqueryAsset.php │ │ ├── JsExpression.php │ │ ├── JsonParser.php │ │ ├── JsonResponseFormatter.php │ │ ├── Link.php │ │ ├── Linkable.php │ │ ├── MethodNotAllowedHttpException.php │ │ ├── MultiFieldSession.php │ │ ├── MultipartFormDataParser.php │ │ ├── NotAcceptableHttpException.php │ │ ├── NotFoundHttpException.php │ │ ├── RangeNotSatisfiableHttpException.php │ │ ├── Request.php │ │ ├── RequestParserInterface.php │ │ ├── Response.php │ │ ├── ResponseFormatterInterface.php │ │ ├── ServerErrorHttpException.php │ │ ├── Session.php │ │ ├── SessionHandler.php │ │ ├── SessionIterator.php │ │ ├── TooManyRequestsHttpException.php │ │ ├── UnauthorizedHttpException.php │ │ ├── UnprocessableEntityHttpException.php │ │ ├── UnsupportedMediaTypeHttpException.php │ │ ├── UploadedFile.php │ │ ├── UrlManager.php │ │ ├── UrlNormalizer.php │ │ ├── UrlNormalizerRedirectException.php │ │ ├── UrlRule.php │ │ ├── UrlRuleInterface.php │ │ ├── User.php │ │ ├── UserEvent.php │ │ ├── View.php │ │ ├── ViewAction.php │ │ ├── XmlResponseFormatter.php │ │ ├── YiiAsset.php │ │ └── migrations/ │ │ ├── m160313_153426_session_init.php │ │ ├── schema-mssql.sql │ │ ├── schema-mysql.sql │ │ ├── schema-oci.sql │ │ ├── schema-pgsql.sql │ │ └── schema-sqlite.sql │ ├── widgets/ │ │ ├── ActiveField.php │ │ ├── ActiveForm.php │ │ ├── ActiveFormAsset.php │ │ ├── BaseListView.php │ │ ├── Block.php │ │ ├── Breadcrumbs.php │ │ ├── ContentDecorator.php │ │ ├── DetailView.php │ │ ├── FragmentCache.php │ │ ├── InputWidget.php │ │ ├── LinkPager.php │ │ ├── LinkSorter.php │ │ ├── ListView.php │ │ ├── MaskedInput.php │ │ ├── MaskedInputAsset.php │ │ ├── Menu.php │ │ ├── Pjax.php │ │ ├── PjaxAsset.php │ │ └── Spaceless.php │ ├── yii │ └── yii.bat ├── package.json ├── phpcs.xml.dist ├── phpstan-7x.dist.neon ├── phpstan-baseline-7x.neon ├── phpstan-baseline.neon ├── phpstan.dist.neon ├── phpunit.xml.dist └── tests/ ├── .env-dist ├── .gitignore ├── .hhconfig ├── Dockerfile.caching ├── Dockerfile.mssql ├── IsOneOfAssert.php ├── README.md ├── ResultPrinter.php ├── TestCase.php ├── assets/ │ └── .gitignore ├── bootstrap.php ├── data/ │ ├── ar/ │ │ ├── ActiveRecord.php │ │ ├── Alpha.php │ │ ├── Animal.php │ │ ├── Beta.php │ │ ├── BitValues.php │ │ ├── Cat.php │ │ ├── Category.php │ │ ├── CroppedType.php │ │ ├── Customer.php │ │ ├── CustomerQuery.php │ │ ├── CustomerWithAlias.php │ │ ├── CustomerWithConstructor.php │ │ ├── DefaultMultiplePk.php │ │ ├── DefaultPk.php │ │ ├── Department.php │ │ ├── Document.php │ │ ├── Dog.php │ │ ├── Dossier.php │ │ ├── Employee.php │ │ ├── EnumTypeInCustomSchema.php │ │ ├── Item.php │ │ ├── NoAutoLabels.php │ │ ├── NullValues.php │ │ ├── Order.php │ │ ├── OrderItem.php │ │ ├── OrderItemWithConstructor.php │ │ ├── OrderItemWithNullFK.php │ │ ├── OrderWithConstructor.php │ │ ├── OrderWithNullFK.php │ │ ├── Profile.php │ │ ├── ProfileWithConstructor.php │ │ ├── Storage.php │ │ ├── TestTrigger.php │ │ ├── TestTriggerAlert.php │ │ └── Type.php │ ├── base/ │ │ ├── ArrayAccessObject.php │ │ ├── CallableClass.php │ │ ├── InvalidRulesModel.php │ │ ├── RulesModel.php │ │ ├── Singer.php │ │ ├── Speaker.php │ │ └── TraversableObject.php │ ├── cache/ │ │ └── MockDependency.php │ ├── codeclimate/ │ │ └── phpmd_ruleset.xml │ ├── config-docker.php │ ├── config.php │ ├── console/ │ │ ├── controllers/ │ │ │ ├── FakeController.php │ │ │ ├── FakeEmptyController.php │ │ │ ├── FakeNoDefaultController.php │ │ │ └── fixtures/ │ │ │ ├── DependentActiveFixture.php │ │ │ ├── FirstFixture.php │ │ │ ├── FirstIndependentActiveFixture.php │ │ │ ├── FixtureStorage.php │ │ │ ├── GlobalFixture.php │ │ │ ├── SecondFixture.php │ │ │ ├── SecondIndependentActiveFixture.php │ │ │ └── subdir/ │ │ │ ├── FirstFixture.php │ │ │ └── SecondFixture.php │ │ └── migrate_create/ │ │ ├── add_columns_fk.php │ │ ├── add_columns_prefix.php │ │ ├── add_columns_test.php │ │ ├── add_two_columns_test.php │ │ ├── create_field_with_colon_default_values.php │ │ ├── create_fields.php │ │ ├── create_fields_with_col_method_after_default_value.php │ │ ├── create_foreign_key.php │ │ ├── create_id_field_not_as_pk.php │ │ ├── create_id_pk.php │ │ ├── create_prefix.php │ │ ├── create_products_from_store_table.php │ │ ├── create_test.php │ │ ├── create_title_pk.php │ │ ├── create_title_with_comma_default_values.php │ │ ├── create_unsigned_big_pk.php │ │ ├── create_unsigned_pk.php │ │ ├── default.php │ │ ├── drop_columns_test.php │ │ ├── drop_fields.php │ │ ├── drop_products_from_store_table.php │ │ ├── drop_test.php │ │ └── junction_test.php │ ├── controllers/ │ │ └── TestController.php │ ├── cubrid.sql │ ├── helpers/ │ │ └── CustomDebugInfo.php │ ├── i18n/ │ │ ├── messages/ │ │ │ ├── de/ │ │ │ │ └── test.php │ │ │ ├── de-DE/ │ │ │ │ └── test.php │ │ │ ├── en-150/ │ │ │ │ └── test.php │ │ │ ├── en-US/ │ │ │ │ └── test.php │ │ │ └── ru/ │ │ │ └── test.php │ │ ├── test.mo │ │ └── test.po │ ├── modules/ │ │ └── magic/ │ │ ├── Module.php │ │ └── controllers/ │ │ ├── ETagController.php │ │ └── subFolder/ │ │ └── SubController.php │ ├── mssql.sql │ ├── mysql.sql │ ├── oci/ │ │ └── optimize_for_tests.sql │ ├── oci.sql │ ├── postgres.sql │ ├── postgres10.sql │ ├── postgres12.sql │ ├── rbac/ │ │ └── UserID.php │ ├── sqlite.sql │ ├── validators/ │ │ ├── TestValidator.php │ │ └── models/ │ │ ├── FakedValidationModel.php │ │ ├── FakedValidationTypedModel.php │ │ ├── ValidatorTestEachAndInlineMethodModel.php │ │ ├── ValidatorTestFunctionModel.php │ │ ├── ValidatorTestMainModel.php │ │ ├── ValidatorTestRefModel.php │ │ └── ValidatorTestTypedPropModel.php │ ├── views/ │ │ ├── error.php │ │ ├── errorHandler.php │ │ ├── errorHandlerForAssetFiles.php │ │ ├── layout.php │ │ ├── pageCacheLayout.php │ │ ├── rawlayout.php │ │ ├── simple.php │ │ └── widgets/ │ │ ├── GridView/ │ │ │ └── gridview.php │ │ └── ListView/ │ │ └── item.php │ └── web/ │ ├── assetSources/ │ │ ├── css/ │ │ │ └── stub.css │ │ └── js/ │ │ └── jquery.js │ ├── assets/ │ │ └── .gitignore │ └── data.txt ├── docker-compose.caching.yml ├── docker-compose.mssql.yml ├── docker-compose.mysql.yml ├── docker-compose.oracle.yml ├── docker-compose.pgsql.yml ├── docker-compose.yml ├── framework/ │ ├── BaseYiiTest.php │ ├── ChangeLogTest.php │ ├── ar/ │ │ └── ActiveRecordTestTrait.php │ ├── base/ │ │ ├── ActionFilterTest.php │ │ ├── ApplicationTest.php │ │ ├── ArrayableTraitTest.php │ │ ├── BaseObjectTest.php │ │ ├── BehaviorTest.php │ │ ├── ComponentTest.php │ │ ├── ControllerTest.php │ │ ├── DynamicModelTest.php │ │ ├── ErrorExceptionTest.php │ │ ├── EventTest.php │ │ ├── ExposedSecurity.php │ │ ├── ModelTest.php │ │ ├── ModuleTest.php │ │ ├── SecurityTest.php │ │ ├── StaticInstanceTraitTest.php │ │ ├── ThemeTest.php │ │ ├── ViewTest.php │ │ ├── WidgetTest.php │ │ ├── fixtures/ │ │ │ └── themes/ │ │ │ ├── basic/ │ │ │ │ └── views/ │ │ │ │ └── site/ │ │ │ │ └── index.php │ │ │ └── christmas/ │ │ │ └── views/ │ │ │ └── site/ │ │ │ ├── index.php │ │ │ └── main.php │ │ └── stub/ │ │ ├── AnonymousComponentClass.php │ │ └── AnonymousModelClass.php │ ├── behaviors/ │ │ ├── AttributeBehaviorTest.php │ │ ├── AttributeTypecastBehaviorTest.php │ │ ├── AttributesBehaviorTest.php │ │ ├── BlameableBehaviorConsoleTest.php │ │ ├── BlameableBehaviorTest.php │ │ ├── CacheableWidgetBehaviorTest.php │ │ ├── OptimisticLockBehaviorTest.php │ │ ├── SluggableBehaviorTest.php │ │ └── TimestampBehaviorTest.php │ ├── caching/ │ │ ├── ApcCacheTest.php │ │ ├── ArrayCacheTest.php │ │ ├── CacheTestCase.php │ │ ├── CallbackDependencyTest.php │ │ ├── DbCacheTest.php │ │ ├── DbDependencyTest.php │ │ ├── DbQueryDependencyTest.php │ │ ├── DependencyTest.php │ │ ├── FileCacheTest.php │ │ ├── MemCacheTest.php │ │ ├── MemCachedTest.php │ │ ├── MssqlCacheTest.php │ │ ├── PgSQLCacheTest.php │ │ ├── TagDependencyTest.php │ │ └── WinCacheTest.php │ ├── console/ │ │ ├── ControllerTest.php │ │ ├── FakeController.php │ │ ├── FakeHelpController.php │ │ ├── FakeHelpControllerWithoutOutput.php │ │ ├── FakePhp71Controller.php │ │ ├── RequestTest.php │ │ ├── UnknownCommandExceptionTest.php │ │ ├── controllers/ │ │ │ ├── AssetControllerTest.php │ │ │ ├── BaseMessageControllerTest.php │ │ │ ├── CacheControllerTest.php │ │ │ ├── DbMessageControllerTest.php │ │ │ ├── EchoMigrateController.php │ │ │ ├── FixtureControllerTest.php │ │ │ ├── HelpControllerTest.php │ │ │ ├── MigrateControllerTest.php │ │ │ ├── MigrateControllerTestTrait.php │ │ │ ├── PHPMessageControllerTest.php │ │ │ ├── POMessageControllerTest.php │ │ │ ├── ServeControllerTest.php │ │ │ ├── SilencedCacheController.php │ │ │ ├── StdOutBufferControllerTrait.php │ │ │ └── stub/ │ │ │ └── index.php │ │ ├── stubs/ │ │ │ └── DummyService.php │ │ └── widgets/ │ │ └── TableTest.php │ ├── data/ │ │ ├── ActiveDataFilterTest.php │ │ ├── ActiveDataProviderCloningTest.php │ │ ├── ActiveDataProviderTest.php │ │ ├── ArrayDataProviderTest.php │ │ ├── BaseDataProviderTest.php │ │ ├── DataFilterTest.php │ │ ├── PaginationTest.php │ │ ├── SortTest.php │ │ └── SqlDataProviderTest.php │ ├── db/ │ │ ├── ActiveQueryModelConnectionTest.php │ │ ├── ActiveQueryTest.php │ │ ├── ActiveRecordTest.php │ │ ├── AnyCaseValue.php │ │ ├── AnyValue.php │ │ ├── BaseActiveRecordTest.php │ │ ├── BatchQueryResultTest.php │ │ ├── ColumnSchemaBackedEnumTest.php │ │ ├── ColumnSchemaBuilderTest.php │ │ ├── ColumnSchemaTest.php │ │ ├── CommandTest.php │ │ ├── CompareValue.php │ │ ├── ConnectionTest.php │ │ ├── DatabaseTestCase.php │ │ ├── GetTablesAliasTestTrait.php │ │ ├── QueryBuilderTest.php │ │ ├── QueryTest.php │ │ ├── SchemaBuilderTraitTest.php │ │ ├── SchemaTest.php │ │ ├── SqlTokenTest.php │ │ ├── UnqueryableQueryMock.php │ │ ├── cubrid/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ └── UniqueValidatorTest.php │ │ ├── enums/ │ │ │ ├── Status.php │ │ │ ├── StatusTypeInt.php │ │ │ └── StatusTypeString.php │ │ ├── mssql/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── DbMessageSourceTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryCacheTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ ├── UniqueValidatorTest.php │ │ │ └── type/ │ │ │ ├── BooleanTest.php │ │ │ └── VarbinaryTest.php │ │ ├── mysql/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── BaseActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ ├── UniqueValidatorTest.php │ │ │ ├── connection/ │ │ │ │ └── DeadLockTest.php │ │ │ └── type/ │ │ │ └── JsonTest.php │ │ ├── oci/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ └── UniqueValidatorTest.php │ │ ├── pgsql/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── ArrayParserTest.php │ │ │ ├── BaseActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ ├── UniqueValidatorTest.php │ │ │ └── type/ │ │ │ └── BooleanTest.php │ │ ├── sqlite/ │ │ │ ├── ActiveDataProviderTest.php │ │ │ ├── ActiveFixtureTest.php │ │ │ ├── ActiveQueryTest.php │ │ │ ├── ActiveRecordTest.php │ │ │ ├── BatchQueryResultTest.php │ │ │ ├── ColumnSchemaBuilderTest.php │ │ │ ├── CommandTest.php │ │ │ ├── ConnectionTest.php │ │ │ ├── ExistValidatorTest.php │ │ │ ├── QueryBuilderTest.php │ │ │ ├── QueryTest.php │ │ │ ├── SchemaTest.php │ │ │ ├── SqlTokenizerTest.php │ │ │ ├── UniqueValidatorTest.php │ │ │ ├── conditions/ │ │ │ │ └── InconditionBuilderTest.php │ │ │ └── type/ │ │ │ └── BooleanTest.php │ │ ├── stubs/ │ │ │ └── BackedEnumStubs.php │ │ └── testBatchInsertWithYield.php │ ├── di/ │ │ ├── ContainerTest.php │ │ ├── InstanceTest.php │ │ ├── ServiceLocatorTest.php │ │ ├── stubs/ │ │ │ ├── AbstractColor.php │ │ │ ├── Alpha.php │ │ │ ├── Bar.php │ │ │ ├── BarSetter.php │ │ │ ├── Beta.php │ │ │ ├── Car.php │ │ │ ├── Color.php │ │ │ ├── Corge.php │ │ │ ├── Foo.php │ │ │ ├── FooBaz.php │ │ │ ├── FooProperty.php │ │ │ ├── Kappa.php │ │ │ ├── Qux.php │ │ │ ├── QuxAnother.php │ │ │ ├── QuxFactory.php │ │ │ ├── QuxInterface.php │ │ │ ├── StaticMethodsWithIntersectionTypes.php │ │ │ ├── StaticMethodsWithUnionTypes.php │ │ │ ├── UnionTypeNotNull.php │ │ │ ├── UnionTypeNull.php │ │ │ ├── UnionTypeWithClass.php │ │ │ ├── Variadic.php │ │ │ └── Zeta.php │ │ └── testContainerWithVariadicCallable.php │ ├── filters/ │ │ ├── AccessRuleTest.php │ │ ├── AjaxFilterTest.php │ │ ├── ContentNegotiatorTest.php │ │ ├── CorsTest.php │ │ ├── HostControlTest.php │ │ ├── HttpCacheTest.php │ │ ├── PageCacheTest.php │ │ ├── RateLimiterTest.php │ │ ├── auth/ │ │ │ ├── AuthMethodTest.php │ │ │ ├── AuthTest.php │ │ │ ├── BasicAuthTest.php │ │ │ └── CompositeAuthTest.php │ │ └── stubs/ │ │ ├── ExposedLogger.php │ │ ├── MockAuthManager.php │ │ ├── RateLimit.php │ │ └── UserIdentity.php │ ├── grid/ │ │ ├── ActionColumnTest.php │ │ ├── CheckboxColumnTest.php │ │ ├── DataColumnTest.php │ │ ├── GridViewTest.php │ │ ├── RadiobuttonColumnTest.php │ │ └── SerialColumnTest.php │ ├── helpers/ │ │ ├── ArrayHelperTest.php │ │ ├── BaseConsoleTest.php │ │ ├── BaseUrlTest.php │ │ ├── ConsoleStub.php │ │ ├── ConsoleTest.php │ │ ├── FallbackInflector.php │ │ ├── FileHelperTest.php │ │ ├── FormatConverterTest.php │ │ ├── HtmlTest.php │ │ ├── InflectorTest.php │ │ ├── IpHelperTest.php │ │ ├── JsonTest.php │ │ ├── MarkdownTest.php │ │ ├── ReplaceArrayValueTest.php │ │ ├── StringHelperTest.php │ │ ├── UnsetArrayValueTest.php │ │ ├── UrlTest.php │ │ └── VarDumperTest.php │ ├── i18n/ │ │ ├── DbMessageSourceTest.php │ │ ├── FallbackMessageFormatterTest.php │ │ ├── FormatterDateTest.php │ │ ├── FormatterNumberTest.php │ │ ├── FormatterTest.php │ │ ├── GettextMessageSourceTest.php │ │ ├── GettextMoFileTest.php │ │ ├── GettextPoFileTest.php │ │ ├── I18NTest.php │ │ ├── IntlTestHelper.php │ │ ├── LocaleTest.php │ │ └── MessageFormatterTest.php │ ├── log/ │ │ ├── ArrayTarget.php │ │ ├── DbTargetTest.php │ │ ├── DispatcherTest.php │ │ ├── EmailTargetTest.php │ │ ├── FileTargetTest.php │ │ ├── LoggerTest.php │ │ ├── MySQLTargetTest.php │ │ ├── PgSQLTargetTest.php │ │ ├── SqliteTargetTest.php │ │ ├── SyslogTargetTest.php │ │ ├── TargetTest.php │ │ └── mocks/ │ │ ├── CustomLogger.php │ │ ├── TargetMock.php │ │ └── typed_error.php │ ├── mail/ │ │ ├── BaseMailerTest.php │ │ └── BaseMessageTest.php │ ├── models/ │ │ └── JsonModel.php │ ├── mutex/ │ │ ├── FileMutexTest.php │ │ ├── MutexTestTrait.php │ │ ├── MysqlMutexTest.php │ │ ├── PgsqlMutexTest.php │ │ ├── RetryAcquireTraitTest.php │ │ └── mocks/ │ │ └── DumbMutex.php │ ├── rbac/ │ │ ├── ActionRule.php │ │ ├── AuthorRule.php │ │ ├── DbManagerTestCase.php │ │ ├── ExposedPhpManager.php │ │ ├── ManagerTestCase.php │ │ ├── MySQLManagerCacheTest.php │ │ ├── MySQLManagerTest.php │ │ ├── PgSQLManagerCacheTest.php │ │ ├── PgSQLManagerTest.php │ │ ├── PhpManagerTest.php │ │ └── SqliteManagerTest.php │ ├── requirements/ │ │ └── YiiRequirementCheckerTest.php │ ├── rest/ │ │ ├── IndexActionTest.php │ │ ├── SerializerTest.php │ │ └── UrlRuleTest.php │ ├── test/ │ │ ├── ActiveFixtureTest.php │ │ ├── ArrayFixtureTest.php │ │ ├── FixtureTest.php │ │ ├── custom/ │ │ │ └── customer.php │ │ └── data/ │ │ ├── array_fixture.php │ │ ├── customer.php │ │ └── profile.php │ ├── validators/ │ │ ├── BooleanValidatorTest.php │ │ ├── CompareValidatorTest.php │ │ ├── DateValidatorTest.php │ │ ├── DefaultValueValidatorTest.php │ │ ├── EachValidatorTest.php │ │ ├── EmailValidatorTest.php │ │ ├── ExistValidatorTest.php │ │ ├── FileValidatorTest.php │ │ ├── FilterValidatorTest.php │ │ ├── IpValidatorTest.php │ │ ├── NumberValidatorTest.php │ │ ├── RangeValidatorTest.php │ │ ├── RegularExpressionValidatorTest.php │ │ ├── RequiredValidatorTest.php │ │ ├── SafeValidatorTest.php │ │ ├── StringValidatorTest.php │ │ ├── TrimValidatorTest.php │ │ ├── UniqueValidatorTest.php │ │ ├── UrlValidatorTest.php │ │ ├── ValidatorTest.php │ │ └── data/ │ │ └── mimeType/ │ │ ├── test.odt │ │ ├── test.tar.xz │ │ ├── test.txt │ │ └── test.xml │ ├── web/ │ │ ├── AssetBundleTest.php │ │ ├── AssetConverterTest.php │ │ ├── ControllerTest.php │ │ ├── CookieCollectionTest.php │ │ ├── ErrorActionTest.php │ │ ├── ErrorHandlerTest.php │ │ ├── FakeController.php │ │ ├── FakePhp71Controller.php │ │ ├── FakePhp7Controller.php │ │ ├── FakePhp80Controller.php │ │ ├── FormatterTest.php │ │ ├── GroupUrlRuleTest.php │ │ ├── HeaderCollectionTest.php │ │ ├── JsonResponseFormatterTest.php │ │ ├── LinkTest.php │ │ ├── MultipartFormDataParserTest.php │ │ ├── Post.php │ │ ├── RequestTest.php │ │ ├── ResponseTest.php │ │ ├── UploadedFileTest.php │ │ ├── UrlManagerCreateUrlTest.php │ │ ├── UrlManagerParseUrlTest.php │ │ ├── UrlManagerTest.php │ │ ├── UrlNormalizerTest.php │ │ ├── UrlRuleTest.php │ │ ├── UserIdentity.php │ │ ├── UserTest.php │ │ ├── ViewTest.php │ │ ├── XmlResponseFormatterTest.php │ │ ├── mocks/ │ │ │ └── TestRequestComponent.php │ │ ├── session/ │ │ │ ├── AbstractDbSessionTest.php │ │ │ ├── CacheSessionTest.php │ │ │ ├── SessionTest.php │ │ │ ├── SessionTestTrait.php │ │ │ ├── mssql/ │ │ │ │ └── DbSessionTest.php │ │ │ ├── mysql/ │ │ │ │ └── DbSessionTest.php │ │ │ ├── pgsql/ │ │ │ │ └── DbSessionTest.php │ │ │ └── sqlite/ │ │ │ └── DbSessionTest.php │ │ └── stubs/ │ │ ├── CachedUrlRule.php │ │ ├── ModelBindingStub.php │ │ ├── ModelStub.php │ │ └── VendorImage.php │ └── widgets/ │ ├── ActiveFieldTest.php │ ├── ActiveFormTest.php │ ├── BlockTest.php │ ├── BreadcrumbsTest.php │ ├── ContentDecoratorTest.php │ ├── DetailViewTest.php │ ├── FragmentCacheTest.php │ ├── LinkPagerTest.php │ ├── LinkSorterTest.php │ ├── ListViewTest.php │ ├── MenuTest.php │ ├── PjaxTest.php │ └── SpacelessTest.php ├── js/ │ ├── data/ │ │ ├── yii.activeForm.html │ │ ├── yii.gridView.html │ │ └── yii.html │ └── tests/ │ ├── yii.activeForm.test.js │ ├── yii.captcha.test.js │ ├── yii.gridView.test.js │ ├── yii.test.js │ └── yii.validation.test.js ├── runtime/ │ └── .gitignore └── test-local.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .appveyor.yml ================================================ build: false version: dev-{build} clone_folder: C:\projects\yii2 environment: matrix: - php_ver: 7.4.0 cache: - '%APPDATA%\Composer' - '%LOCALAPPDATA%\Composer' - C:\tools\php -> .appveyor.yml - C:\tools\composer.phar -> .appveyor.yml init: - SET PATH=C:\tools\php;%PATH% install: - ps: Set-Service wuauserv -StartupType Manual - IF NOT EXIST C:\tools\php (choco install --yes --allow-empty-checksums php --version %php_ver% --params '/InstallDir:C:\tools\php') - cd C:\tools\php - copy php.ini-production php.ini - echo date.timezone="UTC" >> php.ini - echo memory_limit=512M >> php.ini - echo extension_dir=ext >> php.ini - echo extension=php_curl.dll >> php.ini - echo extension=php_fileinfo.dll >> php.ini - echo extension=php_gd2.dll >> php.ini - echo extension=php_intl.dll >> php.ini - echo extension=php_mbstring.dll >> php.ini - echo extension=php_openssl.dll >> php.ini - echo extension=php_pdo_sqlite.dll >> php.ini - IF NOT EXIST C:\tools\composer.phar (cd C:\tools && appveyor DownloadFile https://getcomposer.org/download/2.6.3/composer.phar) before_test: - cd C:\projects\yii2 - php C:\tools\composer.phar update --no-interaction --no-progress --prefer-stable --no-ansi test_script: - cd C:\projects\yii2 - vendor\bin\phpunit --exclude-group mssql,mysql,pgsql,sqlite,db,oci,wincache,cubrid ================================================ FILE: .codecov.yml ================================================ ignore: - "framework/classes.php" - "framework/views/messageConfig.php" ================================================ FILE: .dockerignore ================================================ .git vendor docs ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.yml] indent_size = 2 ================================================ FILE: .git-blame-ignore-revs ================================================ # Reformat code to be PSR-2 compatible b5f8a4dc22d5f8188405a2099d85fc154226c9b2 # Added php-cs-fixer coding standards validation to Travis CI ba0ab403b52124c941dbeb46fbd9efdc12252a5d # Coding style fixes 9d327baa8b2c80b53d4d405678f03e6b89ff6e38 # Add visibility for all class elements c82da8dc829d557e82b12edbed9e37003dfcc9a3 # Fix codestyle in build and tests 909396074eef92d62dd34b4709bb7351e722bec3 # Add void return to method in tests d71f7309aeec1ac0fef223840cb65bbbf96f1f99 # Use `::class` instead of `::className()` in tests c960f93dfeefe760eb44bcdfa4463dcc7b29cc43 # Replace deprecated PHPUnit mock builder `setMethods()` usage with `createPartialMock()`, `onlyMethods()` and `addMethods()` methods 80545100b3f40423118b5cd413fcdb9c7dd7fa5e # Make test data providers static and declare array return types da20adc82aef3eb5ae0bad3889fe6695e6424f06 # Fix codestyle in `tests` 2f8e62d6b64324099b44cbac69395fef0e53b13e # Short array syntax 1f6a8230732d829cdf2f3ca6755e4ac32f2c6f4f # CS fixes. 7a7d2a9c06c099de8064729ca3fc95fb24241b75 # Fixed error PHPCS latest version in `PHP 8.5` f66d5c8864712aef233b8131c2f99721101f61d0 ================================================ FILE: .gitattributes ================================================ # Autodetect text files * text=auto eol=lf # ...Unless the name matches the following overriding patterns # Definitively text files *.php text *.css text *.js text *.txt text *.md text *.xml text *.json text *.bat text *.sql text *.yml text # Ensure those won't be messed up with *.png binary *.jpg binary *.gif binary *.ttf binary # Ignore some meta files when creating an archive of this repository # We do not ignore any content, because this repo represents the # `yiisoft/yii2-dev` package, which is expected to ship all tests and docs. /.appveyor.yml export-ignore /.github export-ignore /.editorconfig export-ignore /.git-blame-ignore-revs export-ignore /.gitattributes export-ignore /.gitignore export-ignore # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ /framework/CHANGELOG.md merge=union ================================================ FILE: .github/CONTRIBUTING.md ================================================ Contributing to Yii 2 ===================== - [Report an issue](../docs/internals/report-an-issue.md) - [Translate documentation or messages](../docs/internals/translation-workflow.md) - [Give us feedback or start a design discussion](https://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/) - [Contribute to the core code or fix bugs](../docs/internals/git-workflow.md) ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: yiisoft github: [yiisoft] tidelift: "packagist/yiisoft/yii2" ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### What steps will reproduce the problem? ### What is the expected result? ### What do you get instead? ### Additional info | Q | A | ---------------- | --- | Yii version | 2.0.? | PHP version | | Operating system | ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ | Q | A | ------------- | --- | Is bugfix? | ✔️/❌ | New feature? | ✔️/❌ | Breaks BC? | ✔️/❌ | Fixed issues | ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you find in Yii. DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help. Please note that as a non-commercial OpenSource project we are not able to pay bounties at the moment. ================================================ FILE: .github/actions/php-setup/action.yml ================================================ --- name: PHP setup with composer. description: Setup PHP environment with composer and install dependencies. inputs: composer-command: description: Composer command (install or update) to run. default: update required: false type: string composer-flags: description: Additional composer flags default: >- --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi required: false type: string composer-version: description: Composer version to use. default: required: false type: string coverage-driver: description: Code coverage driver to use (pcov, xdebug). default: none required: false type: string extensions: description: List of extensions to PHP. default: required: false type: string ignore-platform-reqs: description: Whether to add --ignore-platform-reqs to composer command. default: false required: false type: boolean ini-values: description: Initial values for PHP configuration. default: date.timezone='UTC' required: false type: string php-version: description: PHP versions as a JSON array string '["8.4"]'. default: '["7.4","8.0","8.1","8.2","8.3","8.4"]' required: false type: string tools: description: Tools to test, separated by comma. default: pie required: false type: string runs: using: composite steps: - name: Install PHP uses: shivammathur/setup-php@v2 with: coverage: ${{ inputs.coverage-driver }} extensions: ${{ inputs.extensions }} ini-values: ${{ inputs.ini-values }} php-version: ${{ inputs.php-version }} tools: ${{ inputs.tools }} - name: Update composer. shell: bash run: composer self-update ${{ inputs.composer-version }} - name: Install dependencies with composer. shell: bash run: >- composer ${{ inputs.composer-command }} ${{ inputs.composer-flags }} ${{ inputs.ignore-platform-reqs == 'true' && '--ignore-platform-reqs' || '' }} ================================================ FILE: .github/actions/phpunit/action.yml ================================================ --- name: PHPUnit Test Runner. description: Run PHPUnit tests with coverage and configurable options. inputs: additional-args: description: Additional PHPUnit arguments. default: "--log-junit junit.xml --verbose" required: false type: string configuration: description: PHPUnit configuration file. default: "" required: false type: string coverage-driver: description: Code coverage driver to use (pcov, xdebug, none). default: none required: false type: string coverage-file: description: Coverage output file name. default: coverage.xml required: false type: string coverage-format: description: Coverage report format (clover, html, xml). default: clover required: false type: string coverage-token: description: Codecov token for uploading coverage. default: "" required: false type: string debug: description: Display warnings in phpunit. default: "" required: false type: string exclude-group: description: Exclude group from phpunit. default: "" required: false type: string group: description: Include specific group in phpunit. default: "" required: false type: string path: description: Path to PHPUnit executable. default: vendor/bin/phpunit required: false type: string test-suite: description: Specific test suite to run. default: "" required: false type: string runs: using: composite steps: - name: Build PHPUnit command. id: build-cmd shell: bash run: | PATH_INPUT="${{ inputs.path }}" CONFIG_INPUT="${{ inputs.configuration }}" SUITE_INPUT="${{ inputs.test-suite }}" GROUP_INPUT="${{ inputs.group }}" EXCLUDE_GROUP_INPUT="${{ inputs.exclude-group }}" DEBUG_INPUT="${{ inputs.debug }}" ADDITIONAL_ARGS="${{ inputs.additional-args }}" COVERAGE_DRIVER="${{ inputs.coverage-driver }}" COVERAGE_FORMAT="${{ inputs.coverage-format }}" COVERAGE_FILE="${{ inputs.coverage-file }}" PHPUNIT_CMD="$PATH_INPUT --colors=always" add_param() { if [ -n "$2" ]; then PHPUNIT_CMD="$PHPUNIT_CMD $1 $2" fi } if [ -n "$COVERAGE_DRIVER" ] && [ "$COVERAGE_DRIVER" != "none" ]; then PHPUNIT_CMD="$PHPUNIT_CMD --coverage-$COVERAGE_FORMAT=$COVERAGE_FILE" fi add_param "--configuration" "$CONFIG_INPUT" add_param "--testsuite" "$SUITE_INPUT" add_param "--group" "$GROUP_INPUT" add_param "--exclude-group" "$EXCLUDE_GROUP_INPUT" if [ -n "$DEBUG_INPUT" ]; then PHPUNIT_CMD="$PHPUNIT_CMD $DEBUG_INPUT" fi if [ -n "$ADDITIONAL_ARGS" ]; then PHPUNIT_CMD="$PHPUNIT_CMD $ADDITIONAL_ARGS" fi echo "command=$PHPUNIT_CMD" >> $GITHUB_OUTPUT echo "PHPUnit command: $PHPUNIT_CMD" - name: Run PHPUnit tests on Linux. shell: bash if: runner.os != 'Windows' run: ${{ steps.build-cmd.outputs.command }} - name: Run PHPUnit tests on Windows. shell: pwsh if: runner.os == 'Windows' run: Invoke-Expression "${{ steps.build-cmd.outputs.command }}" - name: Upload test results to Codecov. if: ${{ !cancelled() && inputs.coverage-driver != 'none' }} uses: codecov/test-results-action@v1 with: token: ${{ inputs.coverage-token }} - name: Upload coverage to Codecov. if: ${{ !cancelled() && inputs.coverage-driver != 'none' }} uses: codecov/codecov-action@v5 with: files: ./${{ inputs.coverage-file }} token: ${{ inputs.coverage-token }} ================================================ FILE: .github/workflows/build.yml ================================================ --- name: build permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: PHP_EXTENSIONS: curl, dom, imagick, intl, mbstring, mcrypt, memcached PHP_INI_VALUES: apc.enabled=1,apc.shm_size=32M,apc.enable_cli=1, date.timezone='UTC' PHPUNIT_EXCLUDE_GROUP: db,wincache XDEBUG_MODE: coverage jobs: phpunit: name: PHP ${{ matrix.php }} env: COVERAGE_DRIVER: ${{ matrix.php < 8.1 && 'xdebug' || 'pcov' }} IGNORE_PLATFORM_REQS: false runs-on: ubuntu-22.04 services: &memcached-service memcached: image: memcached:latest ports: - 11211:11211 options: >- --health-cmd "timeout 5 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/11211'" --health-interval 10s --health-retries 5 --health-timeout 5s strategy: fail-fast: false matrix: php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5] steps: &build-steps - name: Checkout. uses: actions/checkout@v5 - name: Generate french locale. run: sudo locale-gen fr_FR.UTF-8 - name: Setup PHP with Composer. uses: ./.github/actions/php-setup with: coverage-driver: ${{ env.COVERAGE_DRIVER }} extensions: ${{ matrix.php < 8.0 && 'apc' || 'apcu' }}, ${{ env.PHP_EXTENSIONS }} ignore-platform-reqs: ${{ env.IGNORE_PLATFORM_REQS }} ini-values: ${{ env.PHP_INI_VALUES }}, session.save_path="${{ runner.temp }}" php-version: ${{ matrix.php }} - name: Run PHPUnit tests. uses: ./.github/actions/phpunit with: coverage-driver: ${{ env.COVERAGE_DRIVER }} coverage-token: ${{ secrets.CODECOV_TOKEN }} exclude-group: ${{ env.PHPUNIT_EXCLUDE_GROUP }} phpunit-dev: name: PHP ${{ matrix.php }} env: COVERAGE_DRIVER: none IGNORE_PLATFORM_REQS: true runs-on: ubuntu-22.04 services: *memcached-service strategy: fail-fast: false matrix: php: [8.6] steps: *build-steps ================================================ FILE: .github/workflows/ci-mariadb.yml ================================================ name: ci-mariadb permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: MariaDB tests with coverage uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mariadb-${{ github.ref }} coverage-driver: xdebug database-env: '{"MARIADB_ROOT_PASSWORD":"root","MARIADB_DATABASE":"yiitest"}' database-health-cmd: mariadb-admin ping database-image: mariadb database-port: "3306" database-type: mysql database-versions: '["10.4","latest"]' extensions: curl, intl, pdo, pdo_mysql os: '["ubuntu-22.04"]' php-version: '["7.4","8.5"]' phpunit-group: mysql secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-dev: name: MariaDB tests uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mariadb-dev-${{ github.ref }} coverage-driver: none database-env: '{"MARIADB_ROOT_PASSWORD":"root","MARIADB_DATABASE":"yiitest"}' database-health-cmd: mariadb-admin ping database-image: mariadb database-port: "3306" database-type: mysql database-versions: '["latest"]' extensions: curl, intl, pdo, pdo_mysql os: '["ubuntu-22.04"]' php-version: '["8.0","8.1","8.2","8.3","8.4"]' phpunit-group: mysql ================================================ FILE: .github/workflows/ci-mssql.yml ================================================ name: ci-mssql permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: MSSQL tests with coverage uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mssql-${{ github.ref }} coverage-driver: xdebug database-env: '{"SA_PASSWORD":"YourStrong!Passw0rd","ACCEPT_EULA":"Y","MSSQL_PID":"Developer"}' database-health-cmd: /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1' database-image: mcr.microsoft.com/mssql/server database-port: "1433" database-type: mssql database-versions: '["2019-latest","2022-latest"]' extensions: curl, intl, pdo, pdo_sqlsrv hook: | sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 docker exec -i database /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q "IF DB_ID(N'yiitest') IS NULL CREATE DATABASE yiitest;" os: '["ubuntu-22.04"]' php-version: '["7.4","8.5"]' phpunit-group: mssql secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-dev: name: MSSQL tests uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mssql-dev-${{ github.ref }} coverage-driver: none database-env: '{"SA_PASSWORD":"YourStrong!Passw0rd","ACCEPT_EULA":"Y","MSSQL_PID":"Developer"}' database-health-cmd: /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1' database-image: mcr.microsoft.com/mssql/server database-port: "1433" database-type: mssql database-versions: '["2022-latest"]' extensions: curl, intl, pdo, pdo_sqlsrv hook: | sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 docker exec -i database /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q "IF DB_ID(N'yiitest') IS NULL CREATE DATABASE yiitest;" os: '["ubuntu-22.04"]' php-version: '["8.0","8.1","8.2","8.3","8.4"]' phpunit-group: mssql ================================================ FILE: .github/workflows/ci-mysql.yml ================================================ name: ci-mysql permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: MySQL tests with coverage uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mysql-${{ github.ref }} coverage-driver: xdebug database-env: '{"MYSQL_ROOT_PASSWORD":"root","MYSQL_DATABASE":"yiitest"}' database-health-cmd: mysqladmin ping database-image: mysql database-port: "3306" database-type: mysql database-versions: '["5.7","latest"]' extensions: curl, intl, pdo, pdo_mysql os: '["ubuntu-22.04"]' php-version: '["7.4","8.5"]' phpunit-group: mysql secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-dev: name: MySQL tests uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: mysql-dev-${{ github.ref }} coverage-driver: none database-env: '{"MYSQL_ROOT_PASSWORD":"root","MYSQL_DATABASE":"yiitest"}' database-health-cmd: mysqladmin ping database-image: mysql database-port: "3306" database-type: mysql database-versions: '["latest"]' extensions: curl, intl, pdo, pdo_mysql os: '["ubuntu-22.04"]' php-version: '["8.0","8.1","8.2","8.3","8.4"]' phpunit-group: mysql ================================================ FILE: .github/workflows/ci-node.yml ================================================ name: build-node on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths env: DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: NPM 10 on ubuntu-22.04 runs-on: ubuntu-22.04 steps: - name: Monitor action permissions. if: runner.os != 'Windows' uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Checkout. uses: actions/checkout@v5 - name: Install dependencies. run: composer update $DEFAULT_COMPOSER_FLAGS - name: Install JQuery `3.6.*@stable` for tests. run: composer require "bower-asset/jquery:3.6.*@stable" - name: Install node.js. uses: actions/setup-node@v4 with: node-version: 20 - name: Tests. run: | npm install npm run lint npm test ================================================ FILE: .github/workflows/ci-oracle.yml ================================================ name: ci-oracle permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: Oracle tests with coverage uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: oracle-${{ github.ref }} coverage-driver: xdebug database-env: '{"ORACLE_PASSWORD":"oracle"}' database-health-cmd: "healthcheck.sh" database-health-retries: 10 database-image: gvenzl/oracle-free database-port: "1521" database-type: oracle database-versions: '["slim-faststart"]' extensions: curl, intl, oci8, pdo, pdo_oci hook: docker exec -i database sqlplus -s system/oracle@//localhost/FREE < tests/data/oci/optimize_for_tests.sql os: '["ubuntu-22.04"]' php-version: '["7.4","8.5"]' phpunit-group: oci secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/ci-pgsql.yml ================================================ name: ci-pgsql permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: PostgreSQL tests with coverage uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: pgsql-${{ github.ref }} coverage-driver: xdebug database-env: '{"POSTGRES_USER":"postgres","POSTGRES_PASSWORD":"postgres","POSTGRES_DB":"yiitest"}' database-health-cmd: pg_isready database-image: postgres database-port: "5432" database-type: pgsql database-versions: '["10","11","12","13","14","15","16","17","latest"]' extensions: curl, intl, pdo, pdo_pgsql os: '["ubuntu-22.04"]' php-version: '["7.4","8.5"]' phpunit-group: pgsql secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-dev: name: PostgreSQL tests uses: yiisoft/yii2-actions/.github/workflows/database.yml@master with: concurrency-group: pgsql-dev-${{ github.ref }} coverage-driver: none database-env: '{"POSTGRES_USER":"postgres","POSTGRES_PASSWORD":"postgres","POSTGRES_DB":"yiitest"}' database-health-cmd: pg_isready database-image: postgres database-port: "5432" database-type: pgsql database-versions: '["latest"]' extensions: curl, intl, pdo, pdo_pgsql os: '["ubuntu-22.04"]' php-version: '["8.0","8.1","8.2","8.3","8.4"]' phpunit-group: pgsql ================================================ FILE: .github/workflows/ci-sqlite.yml ================================================ name: ci-sqlite permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: tests: name: SQLite tests with coverage uses: yiisoft/yii2-actions/.github/workflows/phpunit.yml@master with: concurrency-group: sqlite-${{ github.ref }} coverage-driver: xdebug enable-concurrency: true extensions: curl, intl, pdo, pdo_sqlite os: '["ubuntu-22.04","windows-latest"]' php-version: '["7.4","8.5"]' phpunit-group: sqlite secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-dev: name: SQLite tests uses: yiisoft/yii2-actions/.github/workflows/phpunit.yml@master with: concurrency-group: sqlite-dev-${{ github.ref }} coverage-driver: none enable-concurrency: true extensions: curl, intl, pdo, pdo_sqlite os: '["ubuntu-22.04"]' php-version: '["8.0","8.1","8.2","8.3","8.4"]' phpunit-group: sqlite ================================================ FILE: .github/workflows/linter.yaml ================================================ name: linter permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: phpcs: uses: yiisoft/yii2-actions/.github/workflows/linter.yml@master with: php-version: '["7.4", "8.5"]' ================================================ FILE: .github/workflows/static.yml ================================================ name: static analysis permissions: contents: read pull-requests: write on: pull_request: &ignore-paths paths-ignore: - ".appveyor.yml" - ".dockerignore" - ".editorconfig" - ".git-blame-ignore-revs" - ".gitattributes" - ".github/CONTRIBUTING.md" - ".github/FUNDING.yml" - ".github/ISSUE_TEMPLATE.md" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/SECURITY.md" - ".gitignore" - ".gitlab-ci.yml" - "code-of-conduct.md" - "docs/**" - "framework/.gitignore" - "framework/.phpstorm.meta.php" - "framework/CHANGELOG.md" - "framework/LICENSE.md" - "framework/README.md" - "framework/UPGRADE.md" - "eslint.config.js" - "LICENSE.md" - "README.md" - "ROADMAP.md" push: *ignore-paths jobs: phpstan: uses: yiisoft/yii2-actions/.github/workflows/phpstan.yml@master with: configuration: phpstan.dist.neon concurrency-group: phpstan-${{ github.ref }} extensions: apcu, curl, dom, imagick, intl, mbstring, mcrypt, memcached php-version: '["8.5"]' phpstan-7x: uses: yiisoft/yii2-actions/.github/workflows/phpstan.yml@master with: configuration: phpstan-7x.dist.neon concurrency-group: phpstan7x-${{ github.ref }} extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached php-version: '["7.4"]' ================================================ FILE: .gitignore ================================================ # phpstorm project files .idea *.iml # netbeans project files nbproject # zend studio for eclipse project files .buildpath .project .settings # sublime text project / workspace files *.sublime-project *.sublime-workspace # visual studio code project files .vscode # windows thumbnail cache Thumbs.db # composer vendor dir /vendor # cubrid install dir /cubrid # composer itself is not needed composer.phar # composer.lock in applications is ignored since it's automatically created by composer when application is installed /apps/*/composer.lock # Mac DS_Store Files .DS_Store # phpunit itself is not needed phpunit.phar # local phpunit config /phpunit.xml .phpunit.result.cache # ignore dev installed apps and extensions /apps /extensions /packages # NPM packages /node_modules .env package-lock.json # local phpstan config phpstan.neon ================================================ FILE: .gitlab-ci.yml ================================================ image: docker:latest services: - docker:dind variables: DOCKER_YII2_PHP_IMAGE: yiisoftware/yii2-php:7.4-apache DOCKER_MYSQL_IMAGE: percona:5.7 DOCKER_POSTGRES_IMAGE: postgres:9.3 before_script: - apk add --no-cache git curl docker-compose - docker info - cd tests stages: - travis - test - cleanup test: stage: test script: - docker-compose up --build -d - docker-compose run --rm php vendor/bin/phpunit -v --exclude caching,db,data --log-junit tests/_junit/test.xml caching: stage: test only: - tests/caching - tests/full script: - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml - docker-compose up --build -d - docker-compose run --rm php vendor/bin/phpunit -v --group caching --exclude db db: stage: test only: - tests/mysql - tests/full script: - docker-compose up --build -d - docker-compose run --rm php vendor/bin/phpunit -v --group db --exclude caching,mysql,pgsql,mssql,cubrid,oci mysql: stage: test only: - tests/mysql - tests/full script: - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml - docker-compose up --build -d # wait for db (retry X times) - docker-compose run --rm php bash -c "while ! curl mysql:3306; do ((c++)) && ((c==30)) && break; sleep 2; done" - docker-compose run --rm php vendor/bin/phpunit -v --group mysql pgsql: stage: test only: - tests/pgsql - tests/full script: - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml - docker-compose up --build -d # wait for db (retry X times) - docker-compose run --rm php bash -c 'while [ true ]; do curl postgres:5432; if [ $? == 52 ]; then break; fi; ((c++)) && ((c==25)) && break; sleep 2; done' - docker-compose run --rm php vendor/bin/phpunit -v --group pgsql cubrid: stage: test only: - tests/cubrid - tests/extra script: - cd cubrid - docker-compose up --build -d # wait for db (retry X times) - docker-compose run --rm php bash -c 'while [ true ]; do curl cubrid:1523; if [ $? == 56 ]; then break; fi; ((c++)) && ((c==20)) && break; sleep 3; done' - sleep 5 - docker-compose run --rm php /project/vendor/bin/phpunit -v --group cubrid mssql: stage: test only: - tests/mssql - tests/extra script: - cd mssql - docker-compose up --build -d # wait for db (retry X times) - docker-compose run --rm php bash -c 'while [ true ]; do curl mssql:1433; if [ $? == 52 ]; then break; fi; ((c++)) && ((c==15)) && break; sleep 5; done' - sleep 3 # Note: Password has to be the last parameter - docker-compose run --rm sqlcmd sh -c 'sqlcmd -S mssql -U sa -Q "CREATE DATABASE yii2test" -P Microsoft-12345' - docker-compose run --rm php vendor/bin/phpunit -v --group mssql travis: stage: travis only: - travis script: - export COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml:docker-compose.pgsql.yml - docker-compose up --build -d # wait for dbs ... - sleep 10 - docker-compose run --rm php vendor/bin/phpunit -v --exclude mssql,cubrid,oci,wincache,cubrid ================================================ FILE: .well-known/funding-manifest-urls ================================================ https://www.yiiframework.com/funding.json ================================================ FILE: Dockerfile ================================================ ARG DOCKER_YII2_PHP_IMAGE FROM ${DOCKER_YII2_PHP_IMAGE} # Project source-code WORKDIR /project ADD composer.* /project/ # Install packages RUN /usr/local/bin/composer install --prefer-dist ADD ./ /project ENV PATH /project/vendor/bin:${PATH} ================================================ FILE: LICENSE.md ================================================ Copyright © 2008 by Yii Software (https://www.yiiframework.com/) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Yii Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================

Yii Framework

Yii 2 is a modern framework designed to be a solid foundation for your PHP application. It is fast, secure and efficient and works right out of the box pre-configured with reasonable defaults. The framework is easy to adjust to meet your needs, because Yii has been designed to be flexible. [![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2.svg?logo=packagist&style=for-the-badge&label=Stable)](https://packagist.org/packages/yiisoft/yii2) [![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2.svg?style=for-the-badge)](https://packagist.org/packages/yiisoft/yii2) [![Build Status](https://img.shields.io/github/actions/workflow/status/yiisoft/yii2/build.yml?style=for-the-badge&logo=github&label=Build)](https://github.com/yiisoft/yii2/actions/workflows/build.yml) [![Static Analysis](https://img.shields.io/github/actions/workflow/status/yiisoft/yii2/static.yml?style=for-the-badge&label=Static&logo=github)](https://github.com/yiisoft/yii2/actions/workflows/static.yml) [![codecov](https://img.shields.io/codecov/c/github/yiisoft/yii2.svg?style=for-the-badge&logo=codecov&logoColor=white&label=Codecov)](https://codecov.io/gh/yiisoft/yii2) Installation ------------ > [!IMPORTANT] > - The minimum required [PHP](https://www.php.net/) version of Yii is PHP `7.4`. > - It works best with PHP `8`. - [Follow the Definitive Guide](https://www.yiiframework.com/doc-2.0/guide-start-installation.html) in order to get step by step instructions. Documentation ------------- - A [Definitive Guide](https://www.yiiframework.com/doc/guide/2.0) and a [Class Reference](https://www.yiiframework.com/doc/api/2.0) cover every detail of the framework. - There is a [PDF version](https://www.yiiframework.com/doc/download/yii-guide-2.0-en.pdf) of the Definitive Guide and a [Definitive Guide Mirror](http://stuff.cebe.cc/yii2docs/) which is updated every 15 minutes. - For Yii 1.1 users, there is [Upgrading from Yii 1.1](https://www.yiiframework.com/doc/guide/2.0/en/intro-upgrade-from-v1) to get an idea of what has changed in 2.0. Versions & PHP compatibility ---------------------------- > [!NOTE] > See ["Release Cycle" at the website](https://www.yiiframework.com/release-cycle) for detailed information about supported versions. Community --------- - Participate in [discussions at forums](https://www.yiiframework.com/forum/). - [Community Slack](https://join.slack.com/t/yii/shared_invite/MjIxMjMxMTk5MTU1LTE1MDE3MDAwMzMtM2VkMTMyMjY1Ng) and [Chat in IRC](https://www.yiiframework.com/chat/). - Follow us on [Facebook](https://www.facebook.com/groups/yiitalk/), [Twitter](https://twitter.com/yiiframework) and [GitHub](https://github.com/yiisoft/yii2). - Check [other communities](https://github.com/yiisoft/yii2/wiki/communities). Contributing ------------ The framework is [Open Source](LICENSE.md) powered by [an excellent community](https://github.com/yiisoft/yii2/graphs/contributors). You may join us and: - [Report an issue](docs/internals/report-an-issue.md) - [Translate documentation or messages](docs/internals/translation-workflow.md) - [Give us feedback or start a design discussion](https://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/) - [Contribute to the core code or fix bugs](docs/internals/git-workflow.md) - [Become a sponsor](#sponsoring) ### Reporting Security issues > [!WARNING] > Please do not report security vulnerabilities through public GitHub issues. Please refer to a [special page at the website](https://www.yiiframework.com/security/) describing proper workflow for security issue reports. ### Directory Structure ``` build/ internally used build tools docs/ documentation framework/ core framework code tests/ tests of the core framework code ``` ### Spreading the Word Acknowledging or citing Yii 2 is as important as direct contributions. **In presentations** If you are giving a presentation or talk featuring work that makes use of Yii 2 and would like to acknowledge it, we suggest using [our logo](https://www.yiiframework.com/logo/) on your title slide. **In projects** If you are using Yii 2 as part of an OpenSource project, a way to acknowledge it is to [use a special badge](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=for-the-badge&logo=yii) in your README: [![Yii2](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=for-the-badge&logo=yii)](https://www.yiiframework.com/) If your code is hosted at GitHub, you can place the following in your README.md file to get the badge: ``` [![Yii2](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=for-the-badge&logo=yii)](https://www.yiiframework.com/) ``` ### Sponsoring Support this project by becoming a sponsor or a backer. [![Open Collective sponsors](https://img.shields.io/opencollective/sponsors/yiisoft?style=for-the-badge&logo=opencollective)](https://opencollective.com/yiisoft) [![Open Collective backers](https://img.shields.io/opencollective/backers/yiisoft?style=for-the-badge&logo=opencollective)](https://opencollective.com/yiisoft) ================================================ FILE: ROADMAP.md ================================================ > Roadmap for Yii 3.0 and further was moved to [yiisoft/docs](https://github.com/yiisoft/docs/blob/master/003-roadmap.md). - Enhancements are not accepted for framework version 2.0. - Enhancements are accepted for 2.0 extensions. - Bug and security fixes are expected. - Pull requests and maintainers are very welcome. Above would stand as it is [for two years after Yii 3.0 release](https://www.yiiframework.com/release-cycle). ## Additional releases While we focus on 3.0, we tag 2.0 releases and extension releases [about once in a week](https://www.yiiframework.com/release-cycle). ## 2.0.16+ (since 2019 till 3.0 release + 2 years) - Bugfixes. ## 2.0.15 (2nd quarter of 2018) - Since this release main focus is bug fixing. - No full-branch merges into 3.0. - No enhancements are accepted. ## 2.0.14 (1st quarter of 2018) Will be last release with features and enhancements the last one that will be merged into 3.0 directly. ================================================ FILE: build/.htaccess ================================================ deny from all ================================================ FILE: build/build ================================================ #!/usr/bin/env php 'yii-build', 'basePath' => __DIR__, 'controllerNamespace' => 'yii\build\controllers', 'enableCoreCommands' => false, ]); $application->run(); ================================================ FILE: build/build.bat ================================================ @echo off rem ------------------------------------------------------------- rem build script for Windows. rem rem This is the bootstrap script for running build on Windows. rem rem @author Qiang Xue rem @link https://www.yiiframework.com/ rem @copyright 2008 Yii Software LLC rem @license https://www.yiiframework.com/license/ rem @version $Id$ rem ------------------------------------------------------------- @setlocal set BUILD_PATH=%~dp0 if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe %PHP_COMMAND% "%BUILD_PATH%build" %* @endlocal ================================================ FILE: build/build.xml ================================================ Building package ${pkgname}... Copying files to build directory... Changing file permissions... Generating source release file... Building documentation... Building Guide PDF... Building Blog PDF... Building API... Generating doc release file... Building online API... Copying tutorials... Copying release text files... Finished building Web files. Please update yiisite/common/data/versions.php file with the following code: '1.1'=>array( 'version'=>'${yii.version}', 'revision'=>'${yii.revision}', 'date'=>'${yii.date}', 'latest'=>true, ), Synchronizing code changes for ${pkgname}... Building autoload map... Building yiilite.php... Extracting i18n messages... Cleaning up the build... Welcome to use Yii build script! -------------------------------- You may use the following command format to build a target: phing <target name> where <target name> can be one of the following: - sync : synchronize yiilite.php and BaseYii.php - message : extract i18n messages of the framework - src : build source release - doc : build documentation release (Windows only) - clean : clean up the build ================================================ FILE: build/controllers/ClassmapController.php ================================================ * @since 2.0 * * @extends Controller */ class ClassmapController extends Controller { public $defaultAction = 'create'; /** * Creates a class map for the core Yii classes. * @param string $root the root path of Yii framework. Defaults to YII2_PATH. * @param string $mapFile the file to contain the class map. Defaults to YII2_PATH . '/classes.php'. */ public function actionCreate($root = null, $mapFile = null) { if ($root === null) { $root = YII2_PATH; } $root = FileHelper::normalizePath($root); if ($mapFile === null) { $mapFile = YII2_PATH . '/classes.php'; } $options = [ 'filter' => function ($path) { if (is_file($path)) { $file = basename($path); if ($file[0] < 'A' || $file[0] > 'Z') { return false; } } return null; }, 'only' => ['*.php'], 'except' => [ '/Yii.php', '/BaseYii.php', '/console/', '/requirements/', ], ]; $files = FileHelper::findFiles($root, $options); $map = []; foreach ($files as $file) { if (strpos($file, $root) !== 0) { throw new Exception("Something wrong: $file\n"); } $path = str_replace('\\', '/', substr($file, \strlen($root))); $map[$path] = " 'yii" . substr(str_replace('/', '\\', $path), 0, -4) . "' => YII2_PATH . '$path',"; } ksort($map); $map = implode("\n", $map); $output = << * @since 2.0 * * @extends Controller */ class DevController extends Controller { /** * {@inheritdoc} */ public $defaultAction = 'all'; /** * @var bool whether to use HTTP when cloning GitHub repositories */ public $useHttp = false; /** * @var bool whether to use --no-progress option when running composer */ public $composerNoProgress = false; /** * @var array */ public $apps = [ 'basic' => 'git@github.com:yiisoft/yii2-app-basic.git', 'advanced' => 'git@github.com:yiisoft/yii2-app-advanced.git', 'benchmark' => 'git@github.com:yiisoft/yii2-app-benchmark.git', ]; /** * @var array */ public $extensions = [ 'apidoc' => 'git@github.com:yiisoft/yii2-apidoc.git', 'authclient' => 'git@github.com:yiisoft/yii2-authclient.git', 'bootstrap' => 'git@github.com:yiisoft/yii2-bootstrap.git', 'bootstrap4' => 'git@github.com:yiisoft/yii2-bootstrap4.git', 'bootstrap5' => 'git@github.com:yiisoft/yii2-bootstrap5.git', 'composer' => 'git@github.com:yiisoft/yii2-composer.git', 'debug' => 'git@github.com:yiisoft/yii2-debug.git', 'elasticsearch' => 'git@github.com:yiisoft/yii2-elasticsearch.git', 'faker' => 'git@github.com:yiisoft/yii2-faker.git', 'gii' => 'git@github.com:yiisoft/yii2-gii.git', 'httpclient' => 'git@github.com:yiisoft/yii2-httpclient.git', 'imagine' => 'git@github.com:yiisoft/yii2-imagine.git', 'jui' => 'git@github.com:yiisoft/yii2-jui.git', 'mongodb' => 'git@github.com:yiisoft/yii2-mongodb.git', 'queue' => 'git@github.com:yiisoft/yii2-queue.git', 'redis' => 'git@github.com:yiisoft/yii2-redis.git', 'shell' => 'git@github.com:yiisoft/yii2-shell.git', 'smarty' => 'git@github.com:yiisoft/yii2-smarty.git', 'sphinx' => 'git@github.com:yiisoft/yii2-sphinx.git', 'swiftmailer' => 'git@github.com:yiisoft/yii2-swiftmailer.git', 'symfonymailer' => 'git@github.com:yiisoft/yii2-symfonymailer.git', 'twig' => 'git@github.com:yiisoft/yii2-twig.git', ]; /** * Install all extensions and advanced + basic app. */ public function actionAll() { if (!$this->confirm('Install all applications and all extensions now?')) { return 1; } foreach ($this->extensions as $ext => $repo) { $ret = $this->actionExt($ext); if ($ret !== 0) { return $ret; } } foreach ($this->apps as $app => $repo) { $ret = $this->actionApp($app); if ($ret !== 0) { return $ret; } } return 0; } /** * Runs a command in all extension and application directories. * * Can be used to run e.g. `git pull`. * * ./build/build dev/run git pull * * @param string $command the command to run */ public function actionRun($command) { $command = implode(' ', \func_get_args()); // root of the dev repo $base = \dirname(\dirname(__DIR__)); $dirs = $this->listSubDirs("$base/extensions"); $dirs = array_merge($dirs, $this->listSubDirs("$base/apps")); asort($dirs); $oldcwd = getcwd(); foreach ($dirs as $dir) { $displayDir = substr($dir, \strlen($base)); $this->stdout("Running '$command' in $displayDir...\n", Console::BOLD); chdir($dir); passthru($command); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); } chdir($oldcwd); } /** * This command installs a project template in the `apps` directory and links the framework and extensions. * * It basically runs the following commands in the dev repo root: * * - Run `composer update` * - `rm -rf apps/basic/vendor/yiisoft/yii2` * - `rm -rf apps/basic/vendor/yiisoft/yii2-*` * * And replaces them with symbolic links to the extensions and framework path in the dev repo. * * Extensions required by the application are automatically installed using the `ext` action. * * @param string $app the application name e.g. `basic` or `advanced`. * @param string $repo url of the git repo to clone if it does not already exist. * @return int return code */ public function actionApp($app, $repo = null) { // root of the dev repo $base = \dirname(\dirname(__DIR__)); $appDir = "$base/apps/$app"; if (!file_exists($appDir)) { if (empty($repo)) { if (isset($this->apps[$app])) { $repo = $this->apps[$app]; if ($this->useHttp) { $repo = str_replace('git@github.com:', 'https://github.com/', $repo); } } else { $this->stderr("Repo argument is required for app '$app'.\n", Console::FG_RED); return 1; } } $this->stdout("cloning application repo '$app' from '$repo'...\n", Console::BOLD); passthru('git clone ' . escapeshellarg($repo) . ' ' . $appDir, $returnVar); if ($returnVar !== 0) { $this->stdout("Error occurred while cloning repository.\n", Console::BOLD, Console::FG_RED); return 1; } $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); } // cleanup $this->stdout("cleaning up application '$app' vendor directory...\n", Console::BOLD); $this->cleanupVendorDir($appDir); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); // composer update $this->stdout("updating composer for app '$app'...\n", Console::BOLD); chdir($appDir); $command = 'composer update --prefer-dist'; if ($this->composerNoProgress) { $command .= ' --no-progress'; } passthru($command); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); // link directories $this->stdout("linking framework and extensions to '$app' app vendor dir...\n", Console::BOLD); $this->linkFrameworkAndExtensions($appDir, $base); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); return 0; } /** * This command installs an extension in the `extensions` directory and links the framework and other extensions. * * @param string $extension the application name e.g. `basic` or `advanced`. * @param string $repo url of the git repo to clone if it does not already exist. * * @return int */ public function actionExt($extension, $repo = null) { // root of the dev repo $base = \dirname(\dirname(__DIR__)); $extensionDir = "$base/extensions/$extension"; if (!file_exists($extensionDir)) { if (empty($repo)) { if (isset($this->extensions[$extension])) { $repo = $this->extensions[$extension]; if ($this->useHttp) { $repo = str_replace('git@github.com:', 'https://github.com/', $repo); } } else { $this->stderr("Repo argument is required for extension '$extension'.\n", Console::FG_RED); return 1; } } $this->stdout("cloning extension repo '$extension' from '$repo'...\n", Console::BOLD); passthru('git clone ' . escapeshellarg($repo) . ' ' . $extensionDir); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); } // cleanup $this->stdout("cleaning up extension '$extension' vendor directory...\n", Console::BOLD); $this->cleanupVendorDir($extensionDir); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); // composer update $this->stdout("updating composer for extension '$extension'...\n", Console::BOLD); chdir($extensionDir); $command = 'composer update --prefer-dist'; if ($this->composerNoProgress) { $command .= ' --no-progress'; } passthru($command); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); // link directories $this->stdout("linking framework and extensions to '$extension' vendor dir...\n", Console::BOLD); $this->linkFrameworkAndExtensions($extensionDir, $base); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); return 0; } /** * {@inheritdoc} */ public function options($actionID) { $options = parent::options($actionID); if (\in_array($actionID, ['ext', 'app', 'all'], true)) { $options[] = 'useHttp'; $options[] = 'composerNoProgress'; } return $options; } /** * Remove all symlinks in the vendor subdirectory of the directory specified. * @param string $dir base directory */ protected function cleanupVendorDir($dir) { if (is_link($link = "$dir/vendor/yiisoft/yii2")) { $this->stdout("Removing symlink $link.\n"); FileHelper::unlink($link); } $extensions = $this->findDirs("$dir/vendor/yiisoft"); foreach ($extensions as $ext) { if (is_link($link = "$dir/vendor/yiisoft/yii2-$ext")) { $this->stdout("Removing symlink $link.\n"); FileHelper::unlink($link); } } } /** * Creates symlinks to framework and extension sources for the application. * @param string $dir application directory * @param string $base Yii sources base directory * * @return int */ protected function linkFrameworkAndExtensions($dir, $base) { if (is_dir($link = "$dir/vendor/yiisoft/yii2")) { $this->stdout("Removing dir $link.\n"); FileHelper::removeDirectory($link); $this->stdout("Creating symlink for $link.\n"); symlink("$base/framework", $link); } $extensions = $this->findDirs("$dir/vendor/yiisoft"); foreach ($extensions as $ext) { if (is_dir($link = "$dir/vendor/yiisoft/yii2-$ext")) { $this->stdout("Removing dir $link.\n"); FileHelper::removeDirectory($link); $this->stdout("Creating symlink for $link.\n"); if (!file_exists("$base/extensions/$ext")) { $ret = $this->actionExt($ext); if ($ret !== 0) { return $ret; } } symlink("$base/extensions/$ext", $link); } } return ExitCode::OK; } /** * Get a list of subdirectories for directory specified. * @param string $dir directory to read * * @return array list of subdirectories */ protected function listSubDirs($dir) { $list = []; $handle = opendir($dir); if ($handle === false) { throw new InvalidParamException("Unable to open directory: $dir"); } while (($file = readdir($handle)) !== false) { if ($file === '.' || $file === '..') { continue; } // ignore hidden directories if (strpos($file, '.') === 0) { continue; } if (is_dir("$dir/$file")) { $list[] = "$dir/$file"; } } closedir($handle); return $list; } /** * Finds linkable applications. * * @param string $dir directory to search in * @return array list of applications command can link */ protected function findDirs($dir) { $list = []; $handle = @opendir($dir); if ($handle === false) { return []; } while (($file = readdir($handle)) !== false) { if ($file === '.' || $file === '..') { continue; } $path = $dir . DIRECTORY_SEPARATOR . $file; if (is_dir($path) && preg_match('/^yii2-(.*)$/', $file, $matches)) { $list[] = $matches[1]; } } closedir($handle); foreach ($list as $i => $e) { if ($e === 'composer') { // skip composer to not break composer update unset($list[$i]); } } return $list; } } ================================================ FILE: build/controllers/MimeTypeController.php ================================================ * @since 2.0 * * @extends Controller */ class MimeTypeController extends Controller { /** * @var array MIME type aliases */ private $_aliases = [ 'text/rtf' => 'application/rtf', 'text/xml' => 'application/xml', 'image/svg' => 'image/svg+xml', 'image/x-bmp' => 'image/bmp', 'image/x-bitmap' => 'image/bmp', 'image/x-xbitmap' => 'image/bmp', 'image/x-win-bitmap' => 'image/bmp', 'image/x-windows-bmp' => 'image/bmp', 'image/ms-bmp' => 'image/bmp', 'image/x-ms-bmp' => 'image/bmp', 'application/bmp' => 'image/bmp', 'application/x-bmp' => 'image/bmp', 'application/x-win-bitmap' => 'image/bmp', ]; /** * @var array MIME types to add to the ones parsed from Apache files */ private $_additionalMimeTypes = [ 'apng' => 'image/apng', 'avif' => 'image/avif', 'jfif' => 'image/jpeg', 'mjs' => 'text/javascript', 'pjp' => 'image/jpeg', 'pjpeg' => 'image/jpeg', ]; /** * @param string $outFile the mime file to update. Defaults to @yii/helpers/mimeTypes.php * @param string $aliasesOutFile the aliases file to update. Defaults to @yii/helpers/mimeAliases.php */ public function actionIndex($outFile = null, $aliasesOutFile = null, $extensionsOutFile = null) { if ($outFile === null) { $outFile = Yii::getAlias('@yii/helpers/mimeTypes.php'); } if ($aliasesOutFile === null) { $aliasesOutFile = Yii::getAlias('@yii/helpers/mimeAliases.php'); } if ($extensionsOutFile === null) { $extensionsOutFile = Yii::getAlias('@yii/helpers/mimeExtensions.php'); } $this->stdout('Downloading mime-type file from apache httpd repository...'); if ($apacheMimeTypesFileContent = file_get_contents('https://raw.githubusercontent.com/apache/httpd/refs/heads/trunk/docs/conf/mime.types')) { $this->stdout("Done.\n", Console::FG_GREEN); $this->generateMimeTypesFile($outFile, $apacheMimeTypesFileContent); $this->generateMimeAliasesFile($aliasesOutFile); $this->generateMimeExtensionsFile($extensionsOutFile, $apacheMimeTypesFileContent); } else { $this->stderr("Failed to download mime.types file from apache Git.\n"); } } /** * @param string $outFile * @param string $content */ private function generateMimeTypesFile($outFile, $content) { $this->stdout("Generating file $outFile..."); $mimeMap = []; foreach (explode("\n", $content) as $line) { $line = trim($line); if (empty($line) || strpos($line, '#') === 0) { // skip comments and empty lines continue; } $parts = preg_split('/\s+/', $line); $mime = array_shift($parts); foreach ($parts as $ext) { if (!empty($ext)) { $mimeMap[$ext] = $mime; } } } $mimeMap = array_replace($mimeMap, $this->_additionalMimeTypes); ksort($mimeMap, SORT_STRING); $array = VarDumper::export($mimeMap); $content = <<= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) { \$mimeTypes = array_replace(\$mimeTypes, ['xz' => 'application/octet-stream']); } return \$mimeTypes; EOD; file_put_contents($outFile, $content); $this->stdout("done.\n", Console::FG_GREEN); } /** * @param string $outFile */ private function generateMimeAliasesFile($outFile) { $this->stdout("generating file $outFile..."); $array = VarDumper::export($this->_aliases); $content = <<stdout("done.\n", Console::FG_GREEN); } /** * @param string $outFile * @param string $content */ private function generateMimeExtensionsFile($outFile, $content) { $this->stdout("Generating file $outFile..."); $extensionMap = []; foreach (explode("\n", $content) as $line) { $line = trim($line); if (empty($line) || strpos($line, '#') === 0) { // skip comments and empty lines continue; } $parts = preg_split('/\s+/', $line); $mime = array_shift($parts); if (!empty($parts)) { $extensionMap[$mime] = []; foreach ($parts as $ext) { if (!empty($ext)) { $extensionMap[$mime][] = $ext; } } } } foreach ($this->_additionalMimeTypes as $ext => $mime) { if (!array_key_exists($mime, $extensionMap)) { $extensionMap[$mime] = []; } $extensionMap[$mime][] = $ext; } foreach ($extensionMap as $mime => $extensions) { if (count($extensions) === 1) { $extensionMap[$mime] = $extensions[0]; } } ksort($extensionMap, SORT_STRING); $array = VarDumper::export($extensionMap); $content = <<stdout("done.\n", Console::FG_GREEN); } } ================================================ FILE: build/controllers/PhpDocController.php ================================================ * @author Alexander Makarov * @since 2.0 * * @extends ConsoleController */ class PhpDocController extends ConsoleController { /** * Manually added PHPDoc properties that do not need to be removed or changed. * * @var array */ private const MANUALLY_ADDED_PROPERTIES = [ WebController::class => [ 'request', 'response', 'view', ], ConsoleController::class => [ 'request', 'response', 'help', ], Model::class => [ 'errors', ], Module::class => [ 'aliases', ], Dispatcher::class => [ 'flushInterval', 'logger', ], Target::class => [ 'enabled', ], WebRequest::class => [ 'hostInfo', ], QueryBuilder::class => [ 'conditionClasses', ], ]; private const PROPERTIES_ENCLOSURE = " *\n"; private const TYPE_REG_EXP = '\??[\w\\\-]+(?:<(?:[^<>]+|<[^<>]*>)*>|\{[^{}]*\}|\([^()]*\)(?:\s*:\s*[^()\s]+)?)?(?:\[\])*(?:\s*(?:\||&|\?|:)\s*\??[\w\\\-]+(?:<[^<>]*>)?(?:\[\])*)*'; /** * {@inheritdoc} */ public $defaultAction = 'property'; /** * @var bool whether to update class docs directly. Setting this to false will just output docs * for copy and paste. */ public $updateFiles = true; /** * @var bool whether to add copyright header to php files. This should be skipped in application code. */ public $skipFrameworkRequirements = false; /** * Generates `@property` annotations in class files from getters and setters. * * Property description will be taken from getter or setter or from an `@property` annotation * in the getters docblock if there is one defined. * * See https://github.com/yiisoft/yii2/wiki/Core-framework-code-style#documentation for details. * * @param string $root the directory to parse files from. Defaults to YII2_PATH. */ public function actionProperty($root = null) { $files = $this->findFiles($root); $nFilesTotal = 0; $nFilesUpdated = 0; foreach ($files as $file) { $result = $this->generateClassPropertyDocs($file); if ($result !== false) { list($className, $phpdoc) = $result; if ($this->updateFiles) { if ($this->updateClassPropertyDocs($file, $className, $phpdoc)) { $nFilesUpdated++; } } elseif (!empty($phpdoc)) { $this->stdout("\n[ " . $file . " ]\n\n", Console::BOLD); $this->stdout($phpdoc); } } $nFilesTotal++; } $this->stdout("\nParsed $nFilesTotal files.\n"); $this->stdout("Updated $nFilesUpdated files.\n"); } /** * Fix some issues with PHPDoc in files. * * @param string $root the directory to parse files from. Defaults to YII2_PATH. */ public function actionFix($root = null) { $files = $this->findFiles($root, false); $nFilesTotal = 0; $nFilesUpdated = 0; foreach ($files as $file) { $contents = file_get_contents($file); $hash = $this->hash($contents); // fix line endings $lines = preg_split('/(\r\n|\n|\r)/', $contents); if (!$this->skipFrameworkRequirements) { $this->fixFileDoc($lines); } $this->fixDocBlockIndentation($lines); $lines = array_values($this->fixLineSpacing($lines)); $newContent = implode("\n", $lines); if ($hash !== $this->hash($newContent)) { file_put_contents($file, $newContent); $nFilesUpdated++; } $nFilesTotal++; } $this->stdout("\nParsed $nFilesTotal files.\n"); $this->stdout("Updated $nFilesUpdated files.\n"); } /** * {@inheritdoc} */ public function options($actionID) { return array_merge(parent::options($actionID), ['updateFiles', 'skipFrameworkRequirements']); } /** * @param string $root * @param bool $needsInclude * @return array list of files. */ protected function findFiles($root, $needsInclude = true) { $except = []; if ($needsInclude) { $extensionExcept = [ 'apidoc' => [ '/helpers/PrettyPrinter.php', '/extensions/apidoc/helpers/ApiIndexer.php', '/extensions/apidoc/helpers/ApiMarkdownLaTeX.php', ], 'codeception' => [ '/TestCase.php', '/DbTestCase.php', ], 'gii' => [ '/components/DiffRendererHtmlInline.php', '/generators/extension/default/AutoloadExample.php', ], 'swiftmailer' => [ 'src/Logger.php', ], 'twig' => [ '/Extension.php', '/Optimizer.php', '/Template.php', '/TwigSimpleFileLoader.php', '/ViewRendererStaticClassProxy.php', ], ]; } else { $extensionExcept = []; } if ($root === null) { $root = \dirname(YII2_PATH); $extensionPath = "$root/extensions"; $this->setUpExtensionAliases($extensionPath); $except = [ '/apps/', '/build/', '/docs/', '/extensions/composer/', '/framework/BaseYii.php', '/framework/Yii.php', 'assets/', 'tests/', 'vendor/', ]; foreach ($extensionExcept as $ext => $paths) { foreach ($paths as $path) { $except[] = "/extensions/$ext$path"; } } } elseif (preg_match('~extensions/([\w-]+)[\\\\/]?$~', $root, $matches)) { $extensionPath = \dirname(rtrim($root, '\\/')); $this->setUpExtensionAliases($extensionPath); list(, $extension) = $matches; Yii::setAlias("@yii/$extension", (string)$root); if (is_file($autoloadFile = Yii::getAlias("@yii/$extension/vendor/autoload.php"))) { include $autoloadFile; } if (isset($extensionExcept[$extension])) { foreach ($extensionExcept[$extension] as $path) { $except[] = $path; } } $except[] = '/vendor/'; $except[] = '/tests/'; $except[] = '/docs/'; // // composer extension does not contain yii code // if ($extension === 'composer') { // return []; // } } elseif (preg_match('~apps/([\w-]+)[\\\\/]?$~', $root, $matches)) { $extensionPath = \dirname(\dirname(rtrim($root, '\\/'))) . '/extensions'; $this->setUpExtensionAliases($extensionPath); list(, $appName) = $matches; Yii::setAlias("@app-$appName", (string)$root); if (is_file($autoloadFile = Yii::getAlias("@app-$appName/vendor/autoload.php"))) { include $autoloadFile; } $except[] = '/runtime/'; $except[] = '/vendor/'; $except[] = '/tests/'; $except[] = '/docs/'; } $root = FileHelper::normalizePath($root); $options = [ 'filter' => function ($path) { if (is_file($path)) { $file = basename($path); if ($file[0] < 'A' || $file[0] > 'Z') { return false; } } return null; }, 'only' => ['*.php'], 'except' => array_merge($except, [ '.git/', 'views/', 'requirements/', 'gii/generators/', 'vendor/', '_support/', ]), ]; return FileHelper::findFiles($root, $options); } /** * @param string $extensionPath root path containing extension repositories. */ private function setUpExtensionAliases($extensionPath) { foreach (scandir($extensionPath) as $extension) { if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) { Yii::setAlias("@yii/$extension", "$extensionPath/$extension"); $composerConfigFile = $extensionPath . '/' . $extension . '/composer.json'; if (file_exists($composerConfigFile)) { $composerConfig = Json::decode(file_get_contents($composerConfigFile)); if (isset($composerConfig['autoload']['psr-4'])) { foreach ($composerConfig['autoload']['psr-4'] as $namespace => $subPath) { $alias = '@' . str_replace('\\', '/', $namespace); $path = rtrim("$extensionPath/$extension/$subPath", '/'); Yii::setAlias($alias, $path); } } } } } } /** * Fix file PHPDoc. */ protected function fixFileDoc(&$lines) { // find namespace $namespace = false; $namespaceLine = ''; $contentAfterNamespace = false; foreach ($lines as $i => $line) { $line = trim($line); if (!empty($line)) { if (strncmp($line, 'namespace', 9) === 0) { $namespace = $i; $namespaceLine = $line; } elseif ($namespace !== false) { $contentAfterNamespace = $i; break; } } } if ($namespace !== false && $contentAfterNamespace !== false) { while ($contentAfterNamespace > 0) { array_shift($lines); $contentAfterNamespace--; } $lines = array_merge([ ' $line) { if (preg_match('~^(\s*)/\*\*$~', $line, $matches)) { $docBlock = true; $indent = $matches[1]; } elseif (preg_match('~^(\s*)\*+/~', $line)) { if ($docBlock) { // could be the end of normal comment $lines[$i] = $indent . ' */'; } $docBlock = false; $codeBlock = false; $listIndent = ''; $tag = false; } elseif ($docBlock) { $line = ltrim($line); if (strpos($line, '*') === 0) { $line = substr($line, 1); } if (strpos($line, ' ') === 0) { $line = substr($line, 1); } $docLine = str_replace("\t", ' ', rtrim($line)); if (empty($docLine)) { $listIndent = ''; } elseif (strpos($docLine, '@') === 0) { $listIndent = ''; $codeBlock = false; $tag = true; $docLine = preg_replace('/\s+/', ' ', $docLine); $docLine = $this->fixParamTypes($docLine); } elseif (preg_match('/^(~~~|```)/', $docLine)) { $codeBlock = !$codeBlock; $listIndent = ''; } elseif (preg_match('/^(\s*)([0-9]+\.|-|\*|\+) /', $docLine, $matches)) { $listIndent = str_repeat(' ', \strlen($matches[0])); $tag = false; $lines[$i] = $indent . ' * ' . $docLine; continue; } if ($codeBlock) { $lines[$i] = rtrim($indent . ' * ' . $docLine); } else { $lines[$i] = rtrim($indent . ' * ' . (empty($listIndent) && !$tag ? $docLine : ($listIndent . ltrim($docLine)))); } } } } /** * @param string $line * @return string */ protected function fixParamTypes($line) { return preg_replace_callback('~@(param|return) ([\w\\|]+)~i', function ($matches) { $types = explode('|', $matches[2]); foreach ($types as $i => $type) { switch ($type) { case 'integer': $types[$i] = 'int'; break; case 'boolean': $types[$i] = 'bool'; break; } } return '@' . $matches[1] . ' ' . implode('|', $types); }, $line); } /** * Fixes line spacing code style for properties and constants. * @param string[] $lines * @return string[] */ protected function fixLineSpacing($lines) { $propertiesOnly = false; // remove blank lines between properties $skip = true; $level = 0; foreach ($lines as $i => $line) { if (strpos($line, 'class ') !== false) { $skip = false; } if ($skip) { continue; } // keep spaces in multi line arrays if (strpos($line, '*') === false && strncmp(trim($line), "'SQLSTATE[", 10) !== 0) { $level += substr_count($line, '[') - substr_count($line, ']'); } if (trim($line) === '') { if ($level == 0) { unset($lines[$i]); } } elseif (ltrim($line)[0] !== '*' && strpos($line, 'function ') !== false) { break; } elseif (trim($line) === '}') { $propertiesOnly = true; break; } } $lines = array_values($lines); // add back some $endofUse = false; $endofConst = false; $endofPublic = false; $endofProtected = false; $endofPrivate = false; $skip = true; $level = 0; // track array properties $property = ''; foreach ($lines as $i => $line) { if (strpos($line, 'class ') !== false) { $skip = false; } if ($skip) { continue; } // check for multi line array if ($level > 0) { ${'endof' . $property} = $i; } $line = trim($line); if (strncmp($line, 'public $', 8) === 0 || strncmp($line, 'public static $', 15) === 0) { $endofPublic = $i; $property = 'Public'; $level = 0; } elseif (strncmp($line, 'protected $', 11) === 0 || strncmp($line, 'protected static $', 18) === 0) { $endofProtected = $i; $property = 'Protected'; $level = 0; } elseif (strncmp($line, 'private $', 9) === 0 || strncmp($line, 'private static $', 16) === 0) { $endofPrivate = $i; $property = 'Private'; $level = 0; } elseif (strpos($line, 'const ') === 0) { $endofConst = $i; $property = false; } elseif (strpos($line, 'use ') === 0) { $endofUse = $i; $property = false; } elseif (strpos($line, '*') === 0) { $property = false; } elseif (strpos($line, '*') !== 0 && strpos($line, 'function ') !== false || $line === '}') { break; } // check for multi line array if ($property !== false && strncmp($line, "'SQLSTATE[", 10) !== 0) { $level += substr_count($line, '[') - substr_count($line, ']'); } } $endofAll = false; foreach (['Private', 'Protected', 'Public', 'Const', 'Use'] as $var) { if (${'endof' . $var} !== false) { $endofAll = ${'endof' . $var}; break; } } // $this->checkPropertyOrder($lineInfo); $result = []; foreach ($lines as $i => $line) { $result[] = $line; if (!($propertiesOnly && $i === $endofAll)) { if ( $i === $endofUse || $i === $endofConst || $i === $endofPublic || $i === $endofProtected || $i === $endofPrivate ) { $result[] = ''; } if ($i === $endofAll) { $result[] = ''; } } } return $result; } protected function checkPropertyOrder($lineInfo) { // TODO } protected function updateClassPropertyDocs($file, $className, $propertyDoc) { if ($this->shouldSkipClass($className)) { $this->stderr("[INFO] Skipping class $className.\n", Console::FG_BLUE, Console::BOLD); return false; } try { $ref = new \ReflectionClass($className); } catch (\Exception $e) { $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED); return false; } catch (\Error $e) { $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED); return false; } if ($ref->getFileName() != $file) { $this->stderr("[ERR] Unable to create ReflectionClass for class: $className loaded class is not from file: $file\n", Console::FG_RED); return false; } if ($this->isBaseObject($className, $ref)) { $this->stderr("[INFO] Skipping class $className as it is not a subclass of yii\\base\\BaseObject.\n", Console::FG_BLUE, Console::BOLD); return false; } if ($ref->isSubclassOf('yii\db\BaseActiveRecord')) { $this->stderr("[INFO] Skipping class $className as it is an ActiveRecord class, property handling is not supported yet.\n", Console::FG_BLUE, Console::BOLD); return false; } $oldDoc = $ref->getDocComment(); $newDoc = $this->cleanDocComment($this->updateDocComment($oldDoc, $propertyDoc, $className)); $seenSince = false; $seenAuthor = false; // TODO move these checks to different action $lines = explode("\n", $newDoc); $firstLine = trim($lines[1]); if ($firstLine === '*' || strncmp($firstLine, '* @', 3) === 0) { $this->stderr("[WARN] Class $className has no short description.\n", Console::FG_YELLOW, Console::BOLD); } foreach ($lines as $line) { $line = trim($line); if (strncmp($line, '* @since ', 9) === 0) { $seenSince = true; } elseif (strncmp($line, '* @author ', 10) === 0) { $seenAuthor = true; } } if (!$this->skipFrameworkRequirements) { if (!$seenSince) { $this->stderr("[ERR] No @since found in class doc in file: $file\n", Console::FG_RED); } if (!$seenAuthor) { $this->stderr("[ERR] No @author found in class doc in file: $file\n", Console::FG_RED); } } if (trim($oldDoc) != trim($newDoc)) { $fileContent = explode("\n", file_get_contents($file)); $start = $ref->getStartLine() - 2; $docStart = $start - \count(explode("\n", $oldDoc)) + 1; $newFileContent = []; $n = \count($fileContent); for ($i = 0; $i < $n; $i++) { if ($i > $start || $i < $docStart) { $newFileContent[] = $fileContent[$i]; } else { $newFileContent[] = trim($newDoc); $i = $start; } } file_put_contents($file, implode("\n", $newFileContent)); return true; } return false; } /** * remove multi empty lines and trim trailing whitespace. * * @param $doc * @return string */ protected function cleanDocComment($doc) { $lines = explode("\n", $doc); $n = \count($lines); for ($i = 0; $i < $n; $i++) { $lines[$i] = rtrim($lines[$i]); if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') { unset($lines[$i]); } } return implode("\n", $lines); } /** * Replace property annotations in doc comment. * @param $doc * @param $properties * @return string */ protected function updateDocComment($doc, $properties, $className) { $manuallyAddedProperties = self::MANUALLY_ADDED_PROPERTIES[$className] ?? []; $lines = explode("\n", $doc); $propertyPart = false; $propertyPosition = false; $lastPropertyName = null; $hasManuallyAddedProperties = false; foreach ($lines as $i => $line) { $line = trim($line); if (strncmp($line, '* @property', 11) === 0) { $propertyPart = true; } elseif ($propertyPart && $line === '*') { $propertyPosition = $i; $propertyPart = false; } if (strncmp($line, '* @author ', 10) === 0 && $propertyPosition === false) { $propertyPosition = $i - 1; $propertyPart = false; } if ($propertyPart) { preg_match( '/@property(?:-read|-write)?\s+([\\\\\w\|\[\]]+)\s+\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/', $line, $matches ); if (isset($matches[2])) { $lastPropertyName = $matches[2]; } if (in_array($lastPropertyName, $manuallyAddedProperties)) { $hasManuallyAddedProperties = true; } else { unset($lines[$i]); } } } if ($properties === '') { return implode("\n", $lines); } // if no properties or other tags were present add properties at the end if ($propertyPosition === false) { $propertyPosition = \count($lines) - 2; } // if there are properties that were added manually, remove start enclosure if ($hasManuallyAddedProperties) { $properties = substr($properties, strlen(self::PROPERTIES_ENCLOSURE)); } $finalDoc = ''; foreach ($lines as $i => $line) { if (!$hasManuallyAddedProperties || $i !== $propertyPosition) { $finalDoc .= $line . "\n"; } if ($i == $propertyPosition) { $finalDoc .= $properties; } } return $finalDoc; } protected function generateClassPropertyDocs($fileName) { $phpdoc = ''; $file = str_replace("\r", '', str_replace("\t", ' ', file_get_contents($fileName, true))); $ns = $this->match('#\nnamespace (?[\w\\\\]+);\n#', $file); $namespace = reset($ns); if ($namespace === false) { $namespace = '\\'; } else { $namespace = $namespace['name']; } $classes = $this->match('#\n(?:abstract )?(?:final )?class (?\w+)( extends .+)?( implements .+)?\n\{(?.*)\n\}(\n|$)#', $file); if (\count($classes) > 1) { $this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED); return false; } if (\count($classes) < 1) { $interfaces = $this->match('#\ninterface (?\w+)( extends .+)?\n\{(?.*)\n\}(\n|$)#', $file); if (\count($interfaces) == 1) { return false; } if (\count($interfaces) > 1) { $this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED); } else { $traits = $this->match('#\ntrait (?\w+)\n\{(?.*)\n\}(\n|$)#', $file); if (\count($traits) == 1) { return false; } if (\count($traits) > 1) { $this->stderr("[ERR] There should be only one class/trait/interface in a file: $fileName\n", Console::FG_RED); } else { $this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED); } } return false; } $className = null; foreach ($classes as &$class) { $className = $namespace . '\\' . $class['name']; $gets = $this->match( '#\* @return (?' . self::TYPE_REG_EXP . ')' . '(?: (?(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)((\*\n)|(\*\s.+))*\*/' . '[\s\n]{2,}(\#\[\\\\*.+\])*[\s\n]{2,}' . 'public function (?get)(?\w+)\((?:,? ?\$\w+ ?= ?[^,]+)*\)(\:\s*[\w\\|\\\\\\[\\]]+)?#', $class['content'], true ); $sets = $this->match( '#\* @param (?' . self::TYPE_REG_EXP . ') \$\w+' . '(?: (?(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)((\*\n)|(\*\s.+))*\*/' . '[\s\n]{2,}(\#\[\\\\*.+\])*[\s\n]{2,}' . 'public function (?set)(?\w+)\(([\w\\|\\\\\\[\\]]+\s*)?\$\w+(?:, ?\$\w+ ?= ?[^,]+)*\)(\:\s*[\w\\|\\\\\\[\\]]+)?#', $class['content'], true ); $acrs = array_merge($gets, $sets); $manuallyAddedProperties = self::MANUALLY_ADDED_PROPERTIES[$className] ?? []; $props = []; foreach ($acrs as &$acr) { $acr['name'] = lcfirst($acr['name']); if (in_array($acr['name'], $manuallyAddedProperties)) { continue; } $acr['comment'] = trim(preg_replace('#(^|\n)\s+\*\s?#', '$1 * ', $acr['comment'])); $props[$acr['name']][$acr['kind']] = [ 'type' => $acr['type'], 'comment' => $this->fixSentence($acr['comment']), ]; } if (\count($props) === 0) { continue; } ksort($props); foreach ($props as $propName => &$prop) { $docLine = ' * @property'; $note = ''; if (isset($prop['get'], $prop['set'])) { if ($prop['get']['type'] !== $prop['set']['type']) { $note = ' Note that the type of this property differs in getter and setter.' . ' See [[get' . ucfirst($propName) . '()]]' . ' and [[set' . ucfirst($propName) . '()]] for details.'; } } elseif (isset($prop['get'])) { if (!$this->hasSetterInParents($className, $propName)) { $docLine .= '-read'; } } elseif (isset($prop['set'])) { if (!$this->hasGetterInParents($className, $propName)) { $docLine .= '-write'; } } else { continue; } $docLine .= ' ' . $this->getPropParam($prop, 'type') . " $$propName "; $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note); foreach ($comment as &$cline) { $cline = ltrim(rtrim($cline), '* '); } $docLine = wordwrap($docLine . implode(' ', $comment), 110, "\n * ") . "\n"; $phpdoc .= $docLine; } } if ($phpdoc !== '') { $phpdoc = self::PROPERTIES_ENCLOSURE . $phpdoc . self::PROPERTIES_ENCLOSURE; } return [$className, $phpdoc]; } protected function match($pattern, $subject, $split = false) { $sets = []; // split subject by double newlines because regex sometimes has problems with matching // in the complete set of methods // example: yii\di\ServiceLocator setComponents() is not recognized in the whole but in // a part of the class. $parts = $split ? explode("\n\n", $subject) : [$subject]; foreach ($parts as $part) { preg_match_all($pattern . 'suU', $part, $matches, PREG_SET_ORDER); foreach ($matches as &$set) { foreach ($set as $i => $match) { if (is_numeric($i) /*&& $i != 0*/) { unset($set[$i]); } } $sets[] = $set; } } return $sets; } protected function fixSentence($str) { $str = rtrim($str, '*'); $str = rtrim($str); // TODO fix word wrap if ($str == '') { return ''; } return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[\strlen($str) - 1] !== '.' ? '.' : ''); } protected function getPropParam($prop, $param) { return isset($prop['property']) ? $prop['property'][$param] : (isset($prop['get']) ? $prop['get'][$param] : $prop['set'][$param]); } /** * Generate a hash value (message digest) * @param string $string message to be hashed. * @return string calculated message digest. */ private function hash($string) { if (!function_exists('hash')) { return sha1($string); } return hash('sha256', $string); } /** * @param string $className * @param string $propName * @return bool */ protected function hasGetterInParents($className, $propName) { $class = $className; try { while ($parent = get_parent_class($class)) { if (method_exists($parent, 'get' . ucfirst($propName))) { return true; } $class = $parent; } } catch (\Throwable $t) { $this->stderr("[ERR] Error when getting parents for $className\n", Console::FG_RED); return false; } return false; } /** * @param string $className * @param string $propName * @return bool */ protected function hasSetterInParents($className, $propName) { $class = $className; try { while ($parent = get_parent_class($class)) { if (method_exists($parent, 'set' . ucfirst($propName))) { return true; } $class = $parent; } } catch (\Throwable $t) { $this->stderr("[ERR] Error when getting parents for $className\n", Console::FG_RED); return false; } return false; } /** * @param string $className * @param \ReflectionClass $ref * @return bool */ protected function isBaseObject($className, \ReflectionClass $ref) { $isDeprecatedObject = false; if (PHP_VERSION_ID <= 70100) { $isDeprecatedObject = $ref->isSubclassOf('yii\base\Object') || $className === 'yii\base\Object'; } return !$isDeprecatedObject && !$ref->isSubclassOf('yii\base\BaseObject') && $className !== 'yii\base\BaseObject'; } private function shouldSkipClass($className) { if (PHP_VERSION_ID > 70100) { return $className === 'yii\base\Object'; } return false; } } ================================================ FILE: build/controllers/ReleaseController.php ================================================ * @since 2.0 * * @extends Controller */ class ReleaseController extends Controller { public $defaultAction = 'release'; /** * @var string base path to use for releases. */ public $basePath; /** * @var bool whether to make actual changes. If true, it will run without changing or pushing anything. */ public $dryRun = false; /** * @var bool whether to fetch the latest tags. */ public $update = false; /** * @var string override the default version. e.g. for major or patch releases. */ public $version; public function options($actionID) { $options = ['basePath']; if ($actionID === 'release') { $options[] = 'dryRun'; $options[] = 'version'; } elseif ($actionID === 'sort-changelog') { $options[] = 'version'; } elseif ($actionID === 'info') { $options[] = 'update'; } return array_merge(parent::options($actionID), $options); } public function beforeAction($action) { if (!$this->interactive) { throw new Exception('Sorry, but releases should be run interactively to ensure you actually verify what you are doing ;)'); } if ($this->basePath === null) { $this->basePath = \dirname(\dirname(__DIR__)); } $this->basePath = rtrim($this->basePath, '\\/'); return parent::beforeAction($action); } /** * Shows information about current framework and extension versions. */ public function actionInfo() { $items = [ 'framework', 'app-basic', 'app-advanced', ]; $extensionPath = "{$this->basePath}/extensions"; foreach (scandir($extensionPath) as $extension) { if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) { $items[] = $extension; } } if ($this->update) { foreach ($items as $item) { $this->stdout("fetching tags for $item..."); if ($item === 'framework') { $this->gitFetchTags((string)$this->basePath); } elseif (strncmp('app-', $item, 4) === 0) { $this->gitFetchTags("{$this->basePath}/apps/" . substr($item, 4)); } else { $this->gitFetchTags("{$this->basePath}/extensions/$item"); } $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); } } else { $this->stdout("\nInformation may be outdated, re-run with `--update` to fetch latest tags.\n\n"); } $versions = $this->getCurrentVersions($items); $nextVersions = $this->getNextVersions($versions, self::PATCH); // print version table $w = $this->minWidth(array_keys($versions)); $this->stdout(str_repeat(' ', $w + 2) . "Current Version Next Version\n", Console::BOLD); foreach ($versions as $ext => $version) { $this->stdout($ext . str_repeat(' ', $w + 3 - mb_strlen($ext)) . $version . ''); $this->stdout(str_repeat(' ', 17 - mb_strlen($version)) . $nextVersions[$ext] . "\n"); } } private function minWidth($a) { $w = 1; foreach ($a as $s) { if (($l = mb_strlen($s)) > $w) { $w = $l; } } return $w; } /** * Automation tool for making Yii framework and official extension releases. * * Usage: * * To make a release, make sure your git is clean (no uncommitted changes) and run the following command in * the yii dev repo root: * * ``` * ./build/build release framework * ``` * * or * * ``` * ./build/build release redis,bootstrap,apidoc * ``` * * You may use the `--dryRun` switch to test the command without changing or pushing anything: * * ``` * ./build/build release redis --dryRun * ``` * * The command will guide you through the complete release process including changing of files, * committing and pushing them. Each git command must be confirmed and can be skipped individually. * You may adjust changes in a separate shell or your IDE while the command is waiting for confirmation. * * @param array $what what do you want to release? this can either be: * * - an extension name such as `redis` or `bootstrap`, * - an application indicated by prefix `app-`, e.g. `app-basic`, * - or `framework` if you want to release a new version of the framework itself. * * @return int */ public function actionRelease(array $what) { if (\count($what) > 1) { $this->stdout("Currently only one simultaneous release is supported.\n"); return 1; } $this->stdout("This is the Yii release manager\n\n", Console::BOLD); if ($this->dryRun) { $this->stdout("Running in \"dry-run\" mode, nothing will actually be changed.\n\n", Console::BOLD, Console::FG_GREEN); } $this->validateWhat($what); $versions = $this->getCurrentVersions($what); if ($this->version !== null) { // if a version is explicitly given $newVersions = []; foreach ($versions as $k => $v) { $newVersions[$k] = $this->version; } } else { // otherwise, get next patch or minor $newVersions = $this->getNextVersions($versions, self::PATCH); } $this->stdout("You are about to prepare a new release for the following things:\n\n"); $this->printWhat($what, $newVersions, $versions); $this->stdout("\n"); $this->stdout("Before you make a release briefly go over the changes and check if you spot obvious mistakes:\n\n", Console::BOLD); $gitDir = reset($what) === 'framework' ? 'framework/' : ''; $gitVersion = $versions[reset($what)]; if (strncmp('app-', reset($what), 4) !== 0) { $this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n\n git diff $gitVersion.. {$gitDir}CHANGELOG.md\n\n"); $this->stdout("- are all new `@since` tags for this release version?\n"); } $this->stdout("- other issues with code changes?\n\n git diff -w $gitVersion.. {$gitDir}\n\n"); $travisUrl = reset($what) === 'framework' ? '' : '-' . reset($what); $this->stdout("- are unit tests passing on travis? https://travis-ci.com/yiisoft/yii2$travisUrl/builds\n"); $this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n"); $this->printWhatUrls($what, $versions); $this->stdout("\n"); if (!$this->confirm('When you continue, this tool will run cleanup jobs and update the changelog as well as other files (locally). Continue?', false)) { $this->stdout("Canceled.\n"); return 1; } foreach ($what as $ext) { if ($ext === 'framework') { $this->releaseFramework("{$this->basePath}/framework", $newVersions['framework']); } elseif (strncmp('app-', $ext, 4) === 0) { $this->releaseApplication(substr($ext, 4), "{$this->basePath}/apps/" . substr($ext, 4), $newVersions[$ext]); } else { $this->releaseExtension($ext, "{$this->basePath}/extensions/$ext", $newVersions[$ext]); } } return 0; } /** * This will generate application packages for download page. * * Usage: * * ``` * ./build/build release/package app-basic * ``` * * @param array $what what do you want to package? this can either be: * * - an application indicated by prefix `app-`, e.g. `app-basic`, * * @return int */ public function actionPackage(array $what) { $this->validateWhat($what, ['app']); $versions = $this->getCurrentVersions($what); $this->stdout("You are about to generate packages for the following things:\n\n"); foreach ($what as $ext) { if (strncmp('app-', $ext, 4) === 0) { $this->stdout(' - '); $this->stdout(substr($ext, 4), Console::FG_RED); $this->stdout(' application version '); } elseif ($ext === 'framework') { $this->stdout(' - Yii Framework version '); } else { $this->stdout(' - '); $this->stdout($ext, Console::FG_RED); $this->stdout(' extension version '); } $this->stdout($versions[$ext], Console::BOLD); $this->stdout("\n"); } $this->stdout("\n"); $packagePath = "{$this->basePath}/packages"; $this->stdout("Packages will be stored in $packagePath\n\n"); if (!$this->confirm('Continue?', false)) { $this->stdout("Canceled.\n"); return 1; } foreach ($what as $ext) { if ($ext === 'framework') { throw new Exception('Can not package framework.'); } elseif (strncmp('app-', $ext, 4) === 0) { $this->packageApplication(substr($ext, 4), $versions[$ext], $packagePath); } else { throw new Exception('Can not package extension.'); } } $this->stdout("\ndone. verify the versions composer installed above and push it to github!\n\n"); return 0; } /** * Sorts CHANGELOG for framework or extension. * * @param array $what what do you want to resort changelog for? this can either be: * * - an extension name such as `redis` or `bootstrap`, * - or `framework` if you want to release a new version of the framework itself. */ public function actionSortChangelog(array $what) { if (\count($what) > 1) { $this->stdout("Currently only one simultaneous release is supported.\n"); return 1; } $this->validateWhat($what, ['framework', 'ext'], false); $version = $this->version ?: array_values($this->getNextVersions($this->getCurrentVersions($what), self::PATCH))[0]; $this->stdout('sorting CHANGELOG of '); $this->stdout(reset($what), Console::BOLD); $this->stdout(' for version '); $this->stdout($version, Console::BOLD); $this->stdout('...'); $this->resortChangelogs($what, $version); $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN); } protected function printWhat(array $what, $newVersions, $versions) { foreach ($what as $ext) { if (strncmp('app-', $ext, 4) === 0) { $this->stdout(' - '); $this->stdout(substr($ext, 4), Console::FG_RED); $this->stdout(' application version '); } elseif ($ext === 'framework') { $this->stdout(' - Yii Framework version '); } else { $this->stdout(' - '); $this->stdout($ext, Console::FG_RED); $this->stdout(' extension version '); } $this->stdout($newVersions[$ext], Console::BOLD); $this->stdout(", last release was {$versions[$ext]}\n"); } } protected function printWhatUrls(array $what, $oldVersions) { foreach ($what as $ext) { if ($ext === 'framework') { $this->stdout("framework: https://github.com/yiisoft/yii2-framework/compare/{$oldVersions[$ext]}...master\n"); $this->stdout("app-basic: https://github.com/yiisoft/yii2-app-basic/compare/{$oldVersions[$ext]}...master\n"); $this->stdout("app-advanced: https://github.com/yiisoft/yii2-app-advanced/compare/{$oldVersions[$ext]}...master\n"); } else { $this->stdout($ext, Console::FG_RED); $this->stdout(": https://github.com/yiisoft/yii2-$ext/compare/{$oldVersions[$ext]}...master\n"); } } } /** * @param array $what list of items * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension` * @param bool $ensureGitClean * @throws \yii\base\Exception */ protected function validateWhat(array $what, $limit = [], $ensureGitClean = true) { foreach ($what as $w) { if (strncmp('app-', $w, 4) === 0) { if (!empty($limit) && !\in_array('app', $limit)) { throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n"); } if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) { throw new Exception("Application path does not exist: \"{$appPath}\"\n"); } if ($ensureGitClean) { $this->ensureGitClean($appPath); } } elseif ($w === 'framework') { if (!empty($limit) && !\in_array('framework', $limit)) { throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n"); } if (!is_dir($fwPath = "{$this->basePath}/framework")) { throw new Exception("Framework path does not exist: \"{$this->basePath}/framework\"\n"); } if ($ensureGitClean) { $this->ensureGitClean($fwPath); } } else { if (!empty($limit) && !\in_array('ext', $limit)) { throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n"); } if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) { throw new Exception("Extension path for \"$w\" does not exist: \"{$this->basePath}/extensions/$w\"\n"); } if ($ensureGitClean) { $this->ensureGitClean($extPath); } } } } protected function releaseFramework($frameworkPath, $version) { $this->stdout("\n"); $this->stdout($h = "Preparing framework release version $version", Console::BOLD); $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD); if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) { exit(1); } $this->runGit('git pull', $frameworkPath); // checks $this->stdout('check if framework composer.json matches yii2-dev composer.json...'); $this->checkComposer($frameworkPath); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); // adjustments $this->stdout('prepare classmap...', Console::BOLD); $this->dryRun || Yii::$app->runAction('classmap', [$frameworkPath]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('updating mimetype magic file and mime aliases...', Console::BOLD); $this->dryRun || Yii::$app->runAction('mime-type', ["$frameworkPath/helpers/mimeTypes.php"]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD); $this->dryRun || Yii::$app->runAction('php-doc/fix', [$frameworkPath]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD); $this->dryRun || Yii::$app->runAction('php-doc/property', [$frameworkPath]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('sorting changelogs...', Console::BOLD); $this->dryRun || $this->resortChangelogs(['framework'], $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('closing changelogs...', Console::BOLD); $this->dryRun || $this->closeChangelogs(['framework'], $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('updating Yii version...'); $this->dryRun || $this->updateYiiVersion($frameworkPath, $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("\nIn the following you can check the above changes using git diff.\n\n"); do { $this->runGit('git diff --color', $frameworkPath); $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n"); $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n"); } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?')); $this->stdout("\n\n"); $this->stdout(" **** RELEASE TIME! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout(" **** Commit, Tag and Push it! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n"); $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n"); $this->runGit("git commit -S -a -m \"release version $version\"", $frameworkPath); $this->runGit("git tag -s $version -m \"version $version\"", $frameworkPath); $this->runGit('git push', $frameworkPath); $this->runGit('git push --tags', $frameworkPath); $this->stdout("\n\n"); $this->stdout('CONGRATULATIONS! You have just released ', Console::FG_YELLOW, Console::BOLD); $this->stdout('framework', Console::FG_RED, Console::BOLD); $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD); $this->stdout($version, Console::BOLD); $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD); // TODO release applications // $this->composerSetStability($what, $version); // $this->resortChangelogs($what, $version); // $this->closeChangelogs($what, $version); // $this->composerSetStability($what, $version); // if (in_array('framework', $what)) { // $this->updateYiiVersion($version); // } // if done: // * ./build/build release/done framework 2.0.0-dev 2.0.0-rc // * ./build/build release/done redis 2.0.0-dev 2.0.0-rc // $this->openChangelogs($what, $nextVersion); // $this->composerSetStability($what, 'dev'); // if (in_array('framework', $what)) { // $this->updateYiiVersion($devVersion); // } // prepare next release $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD); $this->stdout('opening changelogs...', Console::BOLD); $nextVersion = $this->getNextVersions(['framework' => $version], self::PATCH); // TODO support other versions $this->dryRun || $this->openChangelogs(['framework'], $nextVersion['framework']); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('updating Yii version...'); $this->dryRun || $this->updateYiiVersion($frameworkPath, $nextVersion['framework'] . '-dev'); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("\n"); $this->runGit('git diff --color', $frameworkPath); $this->stdout("\n\n"); $this->runGit('git commit -a -m "prepare for next release"', $frameworkPath); $this->runGit('git push', $frameworkPath); $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nThe following steps are left for you to do manually:\n\n"); $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions $this->stdout("- wait for your changes to be propagated to the repo and create a tag $version on https://github.com/yiisoft/yii2-framework\n\n"); $this->stdout(" git clone git@github.com:yiisoft/yii2-framework.git\n"); $this->stdout(" cd yii2-framework/\n"); $grepVersion = preg_quote($version, '~'); $this->stdout(" export RELEASECOMMIT=$(git log --oneline |grep \"$grepVersion\" | grep -Po \"^[0-9a-f]+\")\n"); $this->stdout(" git tag -s $version -m \"version $version\" \$RELEASECOMMIT\n"); $this->stdout(" git tag --verify $version\n"); $this->stdout(" git push --tags\n\n"); $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion['framework']} and {$nextVersion2['framework']}: https://github.com/yiisoft/yii2/milestones\n"); $this->stdout("- create a release on github.\n"); $this->stdout("- release news and announcement.\n"); $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n"); $this->stdout(" https://github.com/yiisoft-contrib/yiiframework.com/blob/master/config/versions.php#L69\n"); $this->stdout("\n"); $this->stdout("- release applications: ./build/build release app-basic\n"); $this->stdout("- release applications: ./build/build release app-advanced\n"); $this->stdout("\n"); } protected function releaseApplication($name, $path, $version) { $this->stdout("\n"); $this->stdout($h = "Preparing release for application $name version $version", Console::BOLD); $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD); if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) { exit(1); } $this->runGit('git pull', $path); // adjustments $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD); $this->setAppAliases($name, $path); $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path, 'skipFrameworkRequirements' => true]); $this->resetAppAliases(); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD); $this->setAppAliases($name, $path); $this->dryRun || Yii::$app->runAction('php-doc/property', [$path, 'skipFrameworkRequirements' => true]); $this->resetAppAliases(); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("updating composer stability...\n", Console::BOLD); $this->dryRun || $this->composerSetStability(["app-$name"], $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("\nIn the following you can check the above changes using git diff.\n\n"); do { $this->runGit('git diff --color', $path); $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n"); $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n"); } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?')); $this->stdout("\n\n"); $this->stdout(" **** RELEASE TIME! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout(" **** Commit, Tag and Push it! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n"); $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n"); $this->runGit("git commit -S -a -m \"release version $version\"", $path); $this->runGit("git tag -s $version -m \"version $version\"", $path); $this->runGit('git push', $path); $this->runGit('git push --tags', $path); $this->stdout("\n\n"); $this->stdout('CONGRATULATIONS! You have just released application ', Console::FG_YELLOW, Console::BOLD); $this->stdout($name, Console::FG_RED, Console::BOLD); $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD); $this->stdout($version, Console::BOLD); $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD); // prepare next release $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD); $this->stdout("updating composer stability...\n", Console::BOLD); $this->dryRun || $this->composerSetStability(["app-$name"], 'dev'); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $nextVersion = $this->getNextVersions(["app-$name" => $version], self::PATCH); // TODO support other versions $this->stdout("\n"); $this->runGit('git diff --color', $path); $this->stdout("\n\n"); $this->runGit('git commit -a -m "prepare for next release"', $path); $this->runGit('git push', $path); $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nThe following steps are left for you to do manually:\n\n"); $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion["app-$name"]} and {$nextVersion2["app-$name"]}: https://github.com/yiisoft/yii2-app-$name/milestones\n"); $this->stdout("- Create Application packages and upload them to framework release at github: ./build/build release/package app-$name\n"); $this->stdout("\n"); } private $_oldAlias; protected function setAppAliases($app, $path) { $this->_oldAlias = Yii::getAlias('@app'); switch ($app) { case 'basic': Yii::setAlias('@app', $path); break; case 'advanced': // setup @frontend, @backend etc... require "$path/common/config/bootstrap.php"; break; } } protected function resetAppAliases() { Yii::setAlias('@app', $this->_oldAlias); } protected function packageApplication($name, $version, $packagePath) { FileHelper::createDirectory($packagePath); $this->runCommand("composer create-project yiisoft/yii2-app-$name $name $version", $packagePath); // clear cookie validation key in basic app if (is_file($configFile = "$packagePath/$name/config/web.php")) { $this->sed( "/'cookieValidationKey' => '.*?',/", "'cookieValidationKey' => '',", $configFile ); } $this->runCommand("tar zcf yii-$name-app-$version.tgz $name", $packagePath); } protected function releaseExtension($name, $path, $version) { $this->stdout("\n"); $this->stdout($h = "Preparing release for extension $name version $version", Console::BOLD); $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD); if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) { exit(1); } $this->runGit('git pull', $path); // adjustments $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD); $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD); $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('sorting changelogs...', Console::BOLD); $this->dryRun || $this->resortChangelogs([$name], $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout('closing changelogs...', Console::BOLD); $this->dryRun || $this->closeChangelogs([$name], $version); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("\nIn the following you can check the above changes using git diff.\n\n"); do { $this->runGit('git diff --color', $path); $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n"); $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n"); } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?')); $this->stdout("\n\n"); $this->stdout(" **** RELEASE TIME! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout(" **** Commit, Tag and Push it! ****\n", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n"); $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n"); $this->runGit("git commit -S -a -m \"release version $version\"", $path); $this->runGit("git tag -s $version -m \"version $version\"", $path); $this->runGit('git push', $path); $this->runGit('git push --tags', $path); $this->stdout("\n\n"); $this->stdout('CONGRATULATIONS! You have just released extension ', Console::FG_YELLOW, Console::BOLD); $this->stdout($name, Console::FG_RED, Console::BOLD); $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD); $this->stdout($version, Console::BOLD); $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD); // prepare next release $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD); $this->stdout('opening changelogs...', Console::BOLD); $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]); $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); $this->stdout("\n"); $this->runGit('git diff --color', $path); $this->stdout("\n\n"); $this->runGit('git commit -a -m "prepare for next release"', $path); $this->runGit('git push', $path); $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD); $this->stdout("\n\nThe following steps are left for you to do manually:\n\n"); $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion[$name]} and {$nextVersion2[$name]}: https://github.com/yiisoft/yii2-$name/milestones\n"); $this->stdout("- release news and announcement.\n"); $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n"); $this->stdout("\n"); } protected function runCommand($cmd, $path) { $this->stdout("running $cmd ...", Console::BOLD); if ($this->dryRun) { $this->stdout("dry run, command `$cmd` not executed.\n"); return; } chdir($path); exec($cmd, $output, $ret); if ($ret != 0) { echo implode("\n", $output); throw new Exception("Command \"$cmd\" failed with code " . $ret); } $this->stdout("\ndone.\n", Console::BOLD, Console::FG_GREEN); } protected function runGit($cmd, $path) { if ($this->confirm("Run `$cmd`?", true)) { if ($this->dryRun) { $this->stdout("dry run, command `$cmd` not executed.\n"); return; } chdir($path); exec($cmd, $output, $ret); echo implode("\n", $output); if ($ret != 0) { throw new Exception("Command \"$cmd\" failed with code " . $ret); } echo "\n"; } } protected function ensureGitClean($path) { chdir($path); exec('git status --porcelain -uno', $changes, $ret); if ($ret != 0) { throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret); } if (!empty($changes)) { throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true)); } } protected function gitFetchTags($path) { try { chdir($path); } catch (\yii\base\ErrorException $e) { throw new Exception('Failed to fetch git tags in ' . $path . ': ' . $e->getMessage()); } exec('git fetch --tags', $output, $ret); if ($ret != 0) { throw new Exception('Command "git fetch --tags" failed with code ' . $ret); } } protected function checkComposer($fwPath) { if (!$this->confirm("\nNot yet automated: Please check if composer.json dependencies in framework dir match the one in repo root. Continue?", false)) { exit; } } protected function closeChangelogs($what, $version) { $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/')); $headline = $version . ' ' . date('F d, Y'); $this->sed( '/' . $v . ' under development\R(-+?)\R/', $headline . "\n" . str_repeat('-', \strlen($headline)) . "\n", $this->getChangelogs($what) ); } protected function openChangelogs($what, $version) { $headline = "\n$version under development\n"; $headline .= str_repeat('-', \strlen($headline) - 2) . "\n\n- no changes in this release.\n"; foreach ($this->getChangelogs($what) as $file) { $lines = explode("\n", file_get_contents($file)); $hl = [ array_shift($lines), array_shift($lines), ]; array_unshift($lines, $headline); file_put_contents($file, implode("\n", array_merge($hl, $lines))); } } protected function resortChangelogs($what, $version) { foreach ($this->getChangelogs($what) as $file) { // split the file into relevant parts list($start, $changelog, $end) = $this->splitChangelog($file, $version); $changelog = $this->resortChangelog($changelog); file_put_contents($file, implode("\n", array_merge($start, $changelog, $end))); } } /** * Extract changelog content for a specific version. * @param string $file * @param string $version * @return array */ protected function splitChangelog($file, $version) { $lines = explode("\n", file_get_contents($file)); // split the file into relevant parts $start = []; $changelog = []; $end = []; $state = 'start'; foreach ($lines as $l => $line) { // starting from the changelogs headline if ( isset($lines[$l - 2]) && strpos($lines[$l - 2], $version) !== false && isset($lines[$l - 1]) && strncmp($lines[$l - 1], '---', 3) === 0 ) { $state = 'changelog'; } if ($state === 'changelog' && isset($lines[$l + 1]) && strncmp($lines[$l + 1], '---', 3) === 0) { $state = 'end'; } // add continued lines to the last item to keep them together if (!empty(${$state}) && trim($line) !== '' && strncmp($line, '- ', 2) !== 0) { end(${$state}); if (($k = key(${$state})) !== null) { ${$state}[$k] .= "\n" . $line; } } else { ${$state}[] = $line; } } return [$start, $changelog, $end]; } /** * Ensure sorting of the changelog lines. * @param string[] $changelog * @return string[] */ protected function resortChangelog($changelog) { // cleanup whitespace foreach ($changelog as $i => $line) { $changelog[$i] = rtrim($line); } $changelog = array_filter($changelog); $i = 0; ArrayHelper::multisort($changelog, function ($line) use (&$i) { if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+/', $line, $m)) { $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F']; return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++); } return 'B' . $i++; }, SORT_ASC, SORT_NATURAL); // re-add leading and trailing lines array_unshift($changelog, ''); $changelog[] = ''; $changelog[] = ''; return $changelog; } protected function getChangelogs($what) { $changelogs = []; if (\in_array('framework', $what)) { $changelogs[] = $this->getFrameworkChangelog(); } return array_merge($changelogs, $this->getExtensionChangelogs($what)); } protected function getFrameworkChangelog() { return $this->basePath . '/framework/CHANGELOG.md'; } protected function getExtensionChangelogs($what) { return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function ($elem) use ($what) { foreach ($what as $ext) { if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) { return true; } } return false; }); } protected function composerSetStability($what, $version) { $apps = []; if (\in_array('app-advanced', $what)) { $apps[] = $this->basePath . '/apps/advanced/composer.json'; } if (\in_array('app-basic', $what)) { $apps[] = $this->basePath . '/apps/basic/composer.json'; } if (\in_array('app-benchmark', $what)) { $apps[] = $this->basePath . '/apps/benchmark/composer.json'; } if (empty($apps)) { return; } $stability = 'stable'; if (strpos($version, 'alpha') !== false) { $stability = 'alpha'; } elseif (strpos($version, 'beta') !== false) { $stability = 'beta'; } elseif (strpos($version, 'rc') !== false) { $stability = 'RC'; } elseif (strpos($version, 'dev') !== false) { $stability = 'dev'; } $this->sed( '/"minimum-stability": "(.+?)",/', '"minimum-stability": "' . $stability . '",', $apps ); } protected function updateYiiVersion($frameworkPath, $version) { $this->sed( '/function getVersion\(\)\R {4}\{\R {8}return \'(.+?)\';/', "function getVersion()\n {\n return '$version';", $frameworkPath . '/BaseYii.php' ); } protected function sed($pattern, $replace, $files) { foreach ((array) $files as $file) { file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file))); } } protected function getCurrentVersions(array $what) { $versions = []; foreach ($what as $ext) { if ($ext === 'framework') { chdir("{$this->basePath}/framework"); } elseif (strncmp('app-', $ext, 4) === 0) { chdir("{$this->basePath}/apps/" . substr($ext, 4)); } else { chdir("{$this->basePath}/extensions/$ext"); } $tags = []; exec('git tag', $tags, $ret); if ($ret != 0) { throw new Exception('Command "git tag" failed with code ' . $ret); } rsort($tags, SORT_NATURAL); // TODO this can not deal with alpha/beta/rc... // exclude 3.0.0-alpha1 tag if (($key = array_search('3.0.0-alpha1', $tags, true)) !== false) { unset($tags[$key]); } $versions[$ext] = reset($tags); } return $versions; } public const MINOR = 'minor'; public const PATCH = 'patch'; protected function getNextVersions(array $versions, $type) { foreach ($versions as $k => $v) { if (empty($v)) { $versions[$k] = '2.0.0'; continue; } $parts = explode('.', $v); switch ($type) { case self::MINOR: $parts[1] = (int) $parts[1] + 1; $parts[2] = 0; if (isset($parts[3])) { unset($parts[3]); } break; case self::PATCH: $parts[2] = (int) $parts[2] + 1; if (isset($parts[3])) { unset($parts[3]); } break; default: throw new Exception('Unknown version type.'); } $versions[$k] = implode('.', $parts); } return $versions; } } ================================================ FILE: build/controllers/TranslationController.php ================================================ report_guide_ru.html * * @author Alexander Makarov * * @extends Controller */ class TranslationController extends Controller { public $defaultAction = 'report'; /** * Creates a report about documentation updates since last update of same named translations. * * @param string $sourcePath the directory where the original documentation files are * @param string $translationPath the directory where the translated documentation files are * @param string $title custom title to use for report */ public function actionReport($sourcePath, $translationPath, $title = 'Translation report') { $sourcePath = trim($sourcePath, '/\\'); $translationPath = trim($translationPath, '/\\'); $results = []; $dir = new DirectoryIterator($sourcePath); foreach ($dir as $fileinfo) { /** @var DirectoryIterator $fileinfo */ if (!$fileinfo->isDot() && !$fileinfo->isDir()) { $translatedFilePath = $translationPath . '/' . $fileinfo->getFilename(); $sourceFilePath = $sourcePath . '/' . $fileinfo->getFilename(); $errors = $this->checkFiles($translatedFilePath); $diff = empty($errors) ? $this->getDiff($translatedFilePath, $sourceFilePath) : ''; if (!empty($diff)) { $errors[] = 'Translation outdated.'; } $result = [ 'errors' => $errors, 'diff' => $diff, ]; $results[$fileinfo->getFilename()] = $result; } } // checking if there are obsolete translation files $dir = new DirectoryIterator($translationPath); foreach ($dir as $fileinfo) { /** @var DirectoryIterator $fileinfo */ if (!$fileinfo->isDot() && !$fileinfo->isDir()) { $translatedFilePath = $translationPath . '/' . $fileinfo->getFilename(); $errors = $this->checkFiles(null, $translatedFilePath); if (!empty($errors)) { $results[$fileinfo->getFilename()]['errors'] = $errors; } } } echo $this->renderFile(__DIR__ . '/views/translation/report_html.php', [ 'results' => $results, 'sourcePath' => $sourcePath, 'translationPath' => $translationPath, 'title' => $title, ]); } /** * Checks for files existence. * * @param string $translatedFilePath * @param string $sourceFilePath * @return array errors */ protected function checkFiles($translatedFilePath = null, $sourceFilePath = null) { $errors = []; if ($translatedFilePath !== null && !file_exists($translatedFilePath)) { $errors[] = 'Translation does not exist.'; } if ($sourceFilePath !== null && !file_exists($sourceFilePath)) { $errors[] = 'Source does not exist.'; } return $errors; } /** * Getting DIFF from git. * * @param string $translatedFilePath path pointing to translated file * @param string $sourceFilePath path pointing to original file * @return string DIFF */ protected function getDiff($translatedFilePath, $sourceFilePath) { $lastTranslationHash = shell_exec('git log -1 --format=format:"%H" -- ' . $translatedFilePath); return shell_exec('git diff ' . $lastTranslationHash . '..HEAD -- ' . $sourceFilePath); } /** * Adds all necessary HTML tags and classes to diff output. * * @param string $diff DIFF * @return string highlighted DIFF */ public function highlightDiff($diff) { $lines = explode("\n", $diff); foreach ($lines as $key => $val) { if (strpos($val, '@') === 0) { $lines[$key] = '' . Html::encode($val) . ''; } elseif (strpos($val, '+') === 0) { $lines[$key] = '' . Html::encode($val) . ''; } elseif (strpos($val, '-') === 0) { $lines[$key] = '' . Html::encode($val) . ''; } else { $lines[$key] = Html::encode($val); } } return implode("\n", $lines); } } ================================================ FILE: build/controllers/Utf8Controller.php ================================================ * * @extends Controller */ class Utf8Controller extends Controller { public $defaultAction = 'check-guide'; /** * Check guide for non-printable characters that may break docs generation. * * @param string $directory the directory to check. If not specified, the default * guide directory will be checked. */ public function actionCheckGuide($directory = null) { if ($directory === null) { $directory = \dirname(\dirname(__DIR__)) . '/docs'; } if (is_file($directory)) { $files = [$directory]; } else { $files = FileHelper::findFiles($directory, [ 'only' => ['*.md'], ]); } foreach ($files as $file) { $content = file_get_contents($file); $chars = preg_split('//u', $content, null, PREG_SPLIT_NO_EMPTY); $line = 1; $pos = 0; foreach ($chars as $c) { $ord = $this->unicodeOrd($c); $pos++; if ($ord == 0x000A) { $line++; $pos = 0; } if ($ord === false) { $this->found('BROKEN UTF8', $c, $line, $pos, $file); continue; } // https://unicode-table.com/en/blocks/general-punctuation/ if ( 0x2000 <= $ord && $ord <= 0x200F || 0x2028 <= $ord && $ord <= 0x202E || 0x205f <= $ord && $ord <= 0x206F ) { $this->found('UNSUPPORTED SPACE CHARACTER', $c, $line, $pos, $file); continue; } if ( $ord < 0x0020 && $ord != 0x000A && $ord != 0x0009 || 0x0080 <= $ord && $ord < 0x009F ) { $this->found('CONTROL CHARACTER', $c, $line, $pos, $file); continue; } // if ($ord > 0x009F) { // $this->found("NON ASCII CHARACTER", $c, $line, $pos, $file); // continue; // } } } } private $_foundFiles = []; private function found($what, $char, $line, $pos, $file) { if (!isset($this->_foundFiles[$file])) { $this->stdout("$file: \n", Console::BOLD); $this->_foundFiles[$file] = $file; } $hexcode = dechex($this->unicodeOrd($char)); $hexcode = str_repeat('0', max(4 - \strlen($hexcode), 0)) . $hexcode; $this->stdout(" at $line:$pos FOUND $what: 0x$hexcode '$char' https://unicode-table.com/en/$hexcode/\n"); } /** * Equivalent for ord() just for unicode. * * https://stackoverflow.com/questions/10333098/utf-8-safe-equivalent-of-ord-or-charcodeat-in-php/10333324#10333324 * * @param $c * @return bool|int */ private function unicodeOrd($c) { $h = \ord($c[0]); if ($h <= 0x7F) { return $h; } elseif ($h < 0xC2) { return false; } elseif ($h <= 0xDF) { return ($h & 0x1F) << 6 | (\ord($c[1]) & 0x3F); } elseif ($h <= 0xEF) { return ($h & 0x0F) << 12 | (\ord($c[1]) & 0x3F) << 6 | (\ord($c[2]) & 0x3F); } elseif ($h <= 0xF4) { return ($h & 0x0F) << 18 | (\ord($c[1]) & 0x3F) << 12 | (\ord($c[2]) & 0x3F) << 6 | (\ord($c[3]) & 0x3F); } return false; } } ================================================ FILE: build/controllers/views/translation/report_html.php ================================================ context; ?> Translation report

  • Source:
  • Translation:
$result): ?>

highlightDiff($result['diff']) ?>
================================================ FILE: code-of-conduct.md ================================================ Yii Contributor Code of Conduct ======================= ## Our Pledge As contributors and maintainers of this project, and in order to keep Yii community open and welcoming, we ask to respect all community members. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Personal attacks * Trolling or insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include posting via an official social media account, within project GitHub, official forum or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting core team members. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4.0, available at [https://contributor-covenant.org/version/1/4/][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ ================================================ FILE: composer.json ================================================ { "name": "yiisoft/yii2-dev", "description": "Yii PHP Framework Version 2 - Development Package", "keywords": [ "yii2", "framework" ], "homepage": "https://www.yiiframework.com/", "type": "yii2-extension", "license": "BSD-3-Clause", "authors": [ { "name": "Qiang Xue", "email": "qiang.xue@gmail.com", "homepage": "https://www.yiiframework.com/", "role": "Founder and project lead" }, { "name": "Alexander Makarov", "email": "sam@rmcreative.ru", "homepage": "https://rmcreative.ru/", "role": "Core framework development" }, { "name": "Maurizio Domba", "homepage": "http://mdomba.info/", "role": "Core framework development" }, { "name": "Carsten Brandt", "email": "mail@cebe.cc", "homepage": "https://www.cebe.cc/", "role": "Core framework development" }, { "name": "Timur Ruziev", "email": "resurtm@gmail.com", "homepage": "http://resurtm.com/", "role": "Core framework development" }, { "name": "Paul Klimov", "email": "klimov.paul@gmail.com", "role": "Core framework development" }, { "name": "Dmitry Naumenko", "email": "d.naumenko.a@gmail.com", "role": "Core framework development" }, { "name": "Boudewijn Vahrmeijer", "email": "info@dynasource.eu", "homepage": "http://dynasource.eu", "role": "Core framework development" } ], "support": { "issues": "https://github.com/yiisoft/yii2/issues?state=open", "forum": "https://forum.yiiframework.com/", "wiki": "https://www.yiiframework.com/wiki", "irc": "ircs://irc.libera.chat:6697/yii", "source": "https://github.com/yiisoft/yii2" }, "minimum-stability": "dev", "prefer-stable": true, "replace": { "yiisoft/yii2": "self.version" }, "require": { "php": ">=7.4.0", "ext-mbstring": "*", "ext-ctype": "*", "lib-pcre": "*", "yiisoft/yii2-composer": "~2.0.4", "ezyang/htmlpurifier": "^4.17", "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", "bower-asset/punycode": "^2.2", "bower-asset/yii2-pjax": "~2.0.1" }, "require-dev": { "cebe/indent": "~1.0.2", "dealerdirect/phpcodesniffer-composer-installer": "*", "dms/phpunit-arraysubset-asserts": "^0.5", "phpunit/phpunit": "^9.6", "yiisoft/yii2-coding-standards": "^3.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0" }, "repositories": [ { "type": "composer", "url": "https://asset-packagist.org" } ], "suggest": { "yiisoft/yii2-coding-standards": "you can use this package to check for code style issues when contributing to yii" }, "autoload": { "psr-4": { "yii\\": "framework/" } }, "autoload-dev": { "psr-4": { "yii\\build\\": "build/", "yiiunit\\": "tests/" } }, "config": { "allow-plugins": { "cweagans/composer-patches": true, "yiisoft/yii2-composer": true, "dealerdirect/phpcodesniffer-composer-installer": true } }, "bin": [ "framework/yii" ], "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } }, "scripts": { "cs": "./vendor/bin/phpcs", "cs-fix": "./vendor/bin/phpcbf" } } ================================================ FILE: contrib/completion/bash/yii ================================================ # This file implements bash completion for the ./yii command file. # It completes the commands available by the ./yii command. # See also: # - https://debian-administration.org/article/317/An_introduction_to_bash_completion_part_2 on how this works. # - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html # - https://www.yiiframework.com/doc-2.0/guide-tutorial-console.html#bash-completion # # Usage: # Temporarily you can source this file in you bash by typing: source yii # For permanent availability, copy or link this file to /etc/bash_completion.d/ # _yii() { local cur opts yii command COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" yii="${COMP_WORDS[0]}" # exit if ./yii does not exist test -f $yii || return 0 # lookup for command for word in ${COMP_WORDS[@]:1}; do if [[ $word != -* ]]; then command=$word break fi done [[ $cur == $command ]] && state="command" [[ $cur != $command ]] && state="option" [[ $cur = *=* ]] && state="value" [[ $prev == "help" ]] && state="help" case $state in command|help) # complete command/route if not given # fetch available commands from ./yii help/list command opts=$($yii help/list 2> /dev/null) ;; option) # fetch available options from ./yii help/list-action-options command opts=$($yii help/list-action-options $command 2> /dev/null | grep -o '^--[a-zA-Z0-9\-]*') ;; value) # TODO allow normal file completion after an option, e.g. --migrationPath=... ;; esac # generate completion suggestions COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 } # register completion for the ./yii command # you may adjust this line if your command file is named differently complete -o default -F _yii ./yii yii ================================================ FILE: contrib/completion/zsh/_yii ================================================ #compdef yii _yii() { local state command lastArgument commands options executive lastArgument=${words[${#words[@]}]} prevArgument=${words[${#words[@]}-1]} executive=$words[1] # lookup for command for word in ${words[@]:1}; do if [[ $word != -* ]]; then command=$word break fi done [[ $lastArgument == $command ]] && state="command" [[ $lastArgument != $command ]] && state="option" [[ $prevArgument == "help" ]] && state="help" case $state in command|help) commands=("${(@f)$(${executive} help/list 2>/dev/null)}") _describe 'command' commands ;; option) options=("${(@f)$(${executive} help/usage ${command} 2>/dev/null)}") _message -r "$options" suboptions=("${(@f)$(${executive} help/list-action-options ${command} 2>/dev/null)}") _describe -V -o -t suboption 'action options' suboptions ;; *) esac } compdef _yii yii ================================================ FILE: docs/documentation_style_guide.md ================================================ # Yii Documentation Style Guide Guidelines to go by when writing or editing any Yii documentation. *This needs to be expanded.* ## General Style * Try to use an active voice. * Use short, declarative sentences. * Demonstrate ideas using code as much as possible. * Never use "we". It's the Yii development team or the Yii core team. Better yet to put things in terms of the framework or the guide. * Use the Oxford comma (e.g., "this, that, and the other" not "this, that and the other"). ## Formatting * Use *italics* for emphasis, never capitalization, bold, or underlines. ## Lists * Numeric lists should be complete sentences that end with periods. * Bullet lists should be fragments that end with semicolon except the last item, which should end with a period. ## Blocks Blocks use the Markdown `> Type: `. There are four block types: * `Warning`, for bad security things and other problems * `Note`, to emphasize key concepts, things to avoid * `Info`, general information (an aside); not as strong as a "Note" * `Tip`, pro tips, extras, can be useful but may not be needed by everyone all the time The sentence after the colon should begin with a capital letter. When translating documentation, these Block indicators should not be translated. Keeps them intact as they are and only translate the block content. For translating the `Type` word, each guide translation should have a `blocktypes.json` file containing the translations. The following shows an example for German: ```json { "Warning:": "Achtung:", "Note:": "Hinweis:", "Info:": "Info:", "Tip:": "Tipp:" } ``` ## References * Yii 2.0 or Yii 2 (not Yii2 or Yii2.0) * Each "page" of the guide is referred to as a "section". * References to Code objects: - Refer to classes using the full namespace: `yii\base\Model` - Refer to class properties using the static syntax even if they are not static: `yii\base\Model::$validators` - Refer to class methods using the static syntax even if they are not static and include parenthesis to make it clear, that it is a method: `yii\base\Model::validate()` - references to code objects should be writting in `[[]]` to generate links to the API documentation. E.g. `[[yii\base\Model]]`, `[[yii\base\Model::$validators]]`, or `[[yii\base\Model::validate()]]`. ## Capitalizations * Web, not web * the guide or this guide, not the Guide ## Validating the docs The following are some scripts that help find broken links and other issues in the guide: Find broken links (some false-positives may occur): grep -rniP "\[\[[^\],']+?\][^\]]" docs/guide* grep -rniP "[^\[]\[[^\]\[,']+?\]\]" docs/guide* ## Attribution of Translators The names of the translators will be listed among the guide authors in the rendered versions of the guide. Therefor in each guide directory for a different language than english a `translators.json` file should be created that contains an array of names of the people who have participated in the translation. ```json [ "Jane Doe", "John Doe" ] ``` If you have contributed a significant part to the translation, feel free to send a pull request adding your name. ================================================ FILE: docs/guide/README.md ================================================ The Definitive Guide to Yii 2.0 =============================== This tutorial is released under the [Terms of Yii Documentation](https://www.yiiframework.com/doc/terms/). All Rights Reserved. 2014 (c) Yii Software LLC. Introduction ------------ * [About Yii](intro-yii.md) * [Upgrading from Version 1.1](intro-upgrade-from-v1.md) Getting Started --------------- * [What do you need to know](start-prerequisites.md) * [Installing Yii](start-installation.md) * [Running Applications](start-workflow.md) * [Saying Hello](start-hello.md) * [Working with Forms](start-forms.md) * [Working with Databases](start-databases.md) * [Generating Code with Gii](start-gii.md) * [Looking Ahead](start-looking-ahead.md) Application Structure --------------------- * [Application Structure Overview](structure-overview.md) * [Entry Scripts](structure-entry-scripts.md) * [Applications](structure-applications.md) * [Application Components](structure-application-components.md) * [Controllers](structure-controllers.md) * [Models](structure-models.md) * [Views](structure-views.md) * [Modules](structure-modules.md) * [Filters](structure-filters.md) * [Widgets](structure-widgets.md) * [Assets](structure-assets.md) * [Extensions](structure-extensions.md) Handling Requests ----------------- * [Request Handling Overview](runtime-overview.md) * [Bootstrapping](runtime-bootstrapping.md) * [Routing and URL Creation](runtime-routing.md) * [Requests](runtime-requests.md) * [Responses](runtime-responses.md) * [Sessions and Cookies](runtime-sessions-cookies.md) * [Handling Errors](runtime-handling-errors.md) * [Logging](runtime-logging.md) Key Concepts ------------ * [Components](concept-components.md) * [Properties](concept-properties.md) * [Events](concept-events.md) * [Behaviors](concept-behaviors.md) * [Configurations](concept-configurations.md) * [Aliases](concept-aliases.md) * [Class Autoloading](concept-autoloading.md) * [Service Locator](concept-service-locator.md) * [Dependency Injection Container](concept-di-container.md) Working with Databases ---------------------- * [Database Access Objects](db-dao.md): Connecting to a database, basic queries, transactions, and schema manipulation * [Query Builder](db-query-builder.md): Querying the database using a simple abstraction layer * [Active Record](db-active-record.md): The Active Record ORM, retrieving and manipulating records, and defining relations * [Migrations](db-migrations.md): Apply version control to your databases in a team development environment * [Sphinx](https://www.yiiframework.com/extension/yiisoft/yii2-sphinx/doc/guide) * [Redis](https://www.yiiframework.com/extension/yiisoft/yii2-redis/doc/guide) * [MongoDB](https://www.yiiframework.com/extension/yiisoft/yii2-mongodb/doc/guide) * [ElasticSearch](https://www.yiiframework.com/extension/yiisoft/yii2-elasticsearch/doc/guide) Getting Data from Users ----------------------- * [Creating Forms](input-forms.md) * [Validating Input](input-validation.md) * [Uploading Files](input-file-upload.md) * [Collecting Tabular Input](input-tabular-input.md) * [Getting Data for Multiple Models](input-multiple-models.md) * [Extending ActiveForm on the Client Side](input-form-javascript.md) Displaying Data --------------- * [Data Formatting](output-formatting.md) * [Pagination](output-pagination.md) * [Sorting](output-sorting.md) * [Data Providers](output-data-providers.md) * [Data Widgets](output-data-widgets.md) * [Working with Client Scripts](output-client-scripts.md) * [Theming](output-theming.md) Security -------- * [Security Overview](security-overview.md) * [Authentication](security-authentication.md) * [Authorization](security-authorization.md) * [Working with Passwords](security-passwords.md) * [Cryptography](security-cryptography.md) * [Auth Clients](https://www.yiiframework.com/extension/yiisoft/yii2-authclient/doc/guide) * [Best Practices](security-best-practices.md) Caching ------- * [Caching Overview](caching-overview.md) * [Data Caching](caching-data.md) * [Fragment Caching](caching-fragment.md) * [Page Caching](caching-page.md) * [HTTP Caching](caching-http.md) RESTful Web Services -------------------- * [Quick Start](rest-quick-start.md) * [Resources](rest-resources.md) * [Controllers](rest-controllers.md) * [Filtering Collections](rest-filtering-collections.md) * [Routing](rest-routing.md) * [Response Formatting](rest-response-formatting.md) * [Authentication](rest-authentication.md) * [Rate Limiting](rest-rate-limiting.md) * [Versioning](rest-versioning.md) * [Error Handling](rest-error-handling.md) Development Tools ----------------- * [Debug Toolbar and Debugger](https://www.yiiframework.com/extension/yiisoft/yii2-debug/doc/guide) * [Generating Code using Gii](https://www.yiiframework.com/extension/yiisoft/yii2-gii/doc/guide) * [Generating API Documentation](https://www.yiiframework.com/extension/yiisoft/yii2-apidoc) Testing ------- * [Testing Overview](test-overview.md) * [Testing environment setup](test-environment-setup.md) * [Unit Tests](test-unit.md) * [Functional Tests](test-functional.md) * [Acceptance Tests](test-acceptance.md) * [Fixtures](test-fixtures.md) Special Topics -------------- * [Advanced Project Template](https://www.yiiframework.com/extension/yiisoft/yii2-app-advanced/doc/guide) * [Building Application from Scratch](tutorial-start-from-scratch.md) * [Console Commands](tutorial-console.md) * [Core Validators](tutorial-core-validators.md) * [Docker](tutorial-docker.md) * [Internationalization](tutorial-i18n.md) * [Mailing](tutorial-mailing.md) * [Performance Tuning](tutorial-performance-tuning.md) * [Shared Hosting Environment](tutorial-shared-hosting.md) * [Template Engines](tutorial-template-engines.md) * [Working with Third-Party Code](tutorial-yii-integration.md) * [Using Yii as a micro framework](tutorial-yii-as-micro-framework.md) Widgets ------- * [GridView](https://www.yiiframework.com/doc-2.0/yii-grid-gridview.html) * [ListView](https://www.yiiframework.com/doc-2.0/yii-widgets-listview.html) * [DetailView](https://www.yiiframework.com/doc-2.0/yii-widgets-detailview.html) * [ActiveForm](https://www.yiiframework.com/doc-2.0/guide-input-forms.html#activerecord-based-forms-activeform) * [Pjax](https://www.yiiframework.com/doc-2.0/yii-widgets-pjax.html) * [Menu](https://www.yiiframework.com/doc-2.0/yii-widgets-menu.html) * [LinkPager](https://www.yiiframework.com/doc-2.0/yii-widgets-linkpager.html) * [LinkSorter](https://www.yiiframework.com/doc-2.0/yii-widgets-linksorter.html) * [Bootstrap Widgets](https://www.yiiframework.com/extension/yiisoft/yii2-bootstrap/doc/guide) * [jQuery UI Widgets](https://www.yiiframework.com/extension/yiisoft/yii2-jui/doc/guide) Helpers ------- * [Helpers Overview](helper-overview.md) * [ArrayHelper](helper-array.md) * [Html](helper-html.md) * [Json](helper-json.md) * [Url](helper-url.md) ================================================ FILE: docs/guide/caching-data.md ================================================ Data Caching ============ Data caching is about storing some PHP variables in cache and retrieving it later from cache. It is also the foundation for more advanced caching features, such as [query caching](#query-caching) and [page caching](caching-page.md). The following code is a typical usage pattern of data caching, where `$cache` refers to a [cache component](#cache-components): ```php // try retrieving $data from cache $data = $cache->get($key); if ($data === false) { // $data is not found in cache, calculate it from scratch $data = $this->calculateSomething(); // store $data in cache so that it can be retrieved next time $cache->set($key, $data); } // $data is available here ``` Since version 2.0.11, [cache component](#cache-components) provides [[yii\caching\Cache::getOrSet()|getOrSet()]] method that simplifies code for data getting, calculating and storing. The following code does exactly the same as the previous example: ```php $data = $cache->getOrSet($key, function () { return $this->calculateSomething(); }); ``` When cache has data associated with the `$key`, the cached value will be returned. Otherwise, the passed anonymous function will be executed to calculate the value that will be cached and returned. If the anonymous function requires some data from the outer scope, you can pass it with the `use` statement. For example: ```php $user_id = 42; $data = $cache->getOrSet($key, function () use ($user_id) { return $this->calculateSomething($user_id); }); ``` > Note: [[yii\caching\Cache::getOrSet()|getOrSet()]] method supports duration and dependencies as well. See [Cache Expiration](#cache-expiration) and [Cache Dependencies](#cache-dependencies) to know more. ## Cache Components Data caching relies on the so-called *cache components* which represent various cache storage, such as memory, files, databases. Cache components are usually registered as [application components](structure-application-components.md) so that they can be globally configurable and accessible. The following code shows how to configure the `cache` application component to use [memcached](https://memcached.org/) with two cache servers: ```php 'components' => [ 'cache' => [ 'class' => 'yii\caching\MemCache', 'servers' => [ [ 'host' => 'server1', 'port' => 11211, 'weight' => 100, ], [ 'host' => 'server2', 'port' => 11211, 'weight' => 50, ], ], ], ], ``` You can then access the above cache component using the expression `Yii::$app->cache`. If no cache component is specified, then Yii will use [yii\caching\FileCache](https://www.yiiframework.com/doc/api/2.0/yii-caching-filecache) as default. Because all cache components support the same set of APIs, you can swap the underlying cache component with a different one by reconfiguring it in the application configuration without modifying the code that uses the cache. For example, you can modify the above configuration to use [[yii\caching\ApcCache|APC cache]]: ```php 'components' => [ 'cache' => [ 'class' => 'yii\caching\ApcCache', ], ], ``` > Tip: You can register multiple cache application components. The component named `cache` is used by default by many cache-dependent classes (e.g. [[yii\web\UrlManager]]). ### Supported Cache Storage Yii supports a wide range of cache storage. The following is a summary: * [[yii\caching\ApcCache]]: uses PHP [APC](https://www.php.net/manual/en/book.apcu.php) extension. This option can be considered as the fastest one when dealing with cache for a centralized thick application (e.g. one server, no dedicated load balancers, etc.). * [[yii\caching\DbCache]]: uses a database table to store cached data. To use this cache, you must create a table as specified in [[yii\caching\DbCache::cacheTable]]. * [[yii\caching\ArrayCache]]: provides caching for the current request only by storing the values in an array. For enhanced performance of ArrayCache, you can disable serialization of the stored data by setting [[yii\caching\ArrayCache::$serializer]] to `false`. * [[yii\caching\DummyCache]]: serves as a cache placeholder which does no real caching. The purpose of this component is to simplify the code that needs to check the availability of cache. For example, during development or if the server doesn't have actual cache support, you may configure a cache component to use this cache. When an actual cache support is enabled, you can switch to use the corresponding cache component. In both cases, you may use the same code `Yii::$app->cache->get($key)` to attempt retrieving data from the cache without worrying that `Yii::$app->cache` might be `null`. * [[yii\caching\FileCache]]: uses standard files to store cached data. This is particularly suitable to cache large chunk of data, such as page content. * [[yii\caching\MemCache]]: uses PHP [memcache](https://www.php.net/manual/en/book.memcache.php) and [memcached](https://www.php.net/manual/en/book.memcached.php) extensions. This option can be considered as the fastest one when dealing with cache in a distributed applications (e.g. with several servers, load balancers, etc.) * [[yii\redis\Cache]]: implements a cache component based on [Redis](https://redis.io/) key-value store (redis version 2.6.12 or higher is required). * [[yii\caching\WinCache]]: uses PHP [WinCache](https://iis.net/downloads/microsoft/wincache-extension) ([see also](https://www.php.net/manual/en/book.wincache.php)) extension. > Tip: You may use different cache storage in the same application. A common strategy is to use memory-based cache storage to store data that is small but constantly used (e.g. statistical data), and use file-based or database-based cache storage to store data that is big and less frequently used (e.g. page content). ## Cache APIs All cache components have the same base class [[yii\caching\Cache]] and thus support the following APIs: * [[yii\caching\Cache::get()|get()]]: retrieves a data item from cache with a specified key. A `false` value will be returned if the data item is not found in the cache or is expired/invalidated. * [[yii\caching\Cache::set()|set()]]: stores a data item identified by a key in cache. * [[yii\caching\Cache::add()|add()]]: stores a data item identified by a key in cache if the key is not found in the cache. * [[yii\caching\Cache::getOrSet()|getOrSet()]]: retrieves a data item from cache with a specified key or executes passed callback, stores return of the callback in a cache by a key and returns that data. * [[yii\caching\Cache::multiGet()|multiGet()]]: retrieves multiple data items from cache with the specified keys. * [[yii\caching\Cache::multiSet()|multiSet()]]: stores multiple data items in cache. Each item is identified by a key. * [[yii\caching\Cache::multiAdd()|multiAdd()]]: stores multiple data items in cache. Each item is identified by a key. If a key already exists in the cache, the data item will be skipped. * [[yii\caching\Cache::exists()|exists()]]: returns a value indicating whether the specified key is found in the cache. * [[yii\caching\Cache::delete()|delete()]]: removes a data item identified by a key from the cache. * [[yii\caching\Cache::flush()|flush()]]: removes all data items from the cache. > Note: Do not cache a `false` boolean value directly because the [[yii\caching\Cache::get()|get()]] method uses `false` return value to indicate the data item is not found in the cache. You may put `false` in an array and cache this array instead to avoid this problem. Some cache storage, such as MemCache, APC, support retrieving multiple cached values in a batch mode, which may reduce the overhead involved in retrieving cached data. The APIs [[yii\caching\Cache::multiGet()|multiGet()]] and [[yii\caching\Cache::multiAdd()|multiAdd()]] are provided to exploit this feature. In case the underlying cache storage does not support this feature, it will be simulated. Because [[yii\caching\Cache]] implements `ArrayAccess`, a cache component can be used like an array. The following are some examples: ```php $cache['var1'] = $value1; // equivalent to: $cache->set('var1', $value1); $value2 = $cache['var2']; // equivalent to: $value2 = $cache->get('var2'); ``` ### Cache Keys Each data item stored in cache is uniquely identified by a key. When you store a data item in cache, you have to specify a key for it. Later when you retrieve the data item from cache, you should provide the corresponding key. You may use a string or an arbitrary value as a cache key. When a key is not a string, it will be automatically serialized into a string. A common strategy of defining a cache key is to include all determining factors in terms of an array. For example, [[yii\db\Schema]] uses the following key to cache schema information about a database table: ```php [ __CLASS__, // schema class name $this->db->dsn, // DB connection data source name $this->db->username, // DB connection login user $name, // table name ]; ``` As you can see, the key includes all necessary information needed to uniquely specify a database table. > Note: Values stored in cache via [[yii\caching\Cache::multiSet()|multiSet()]] or [[yii\caching\Cache::multiAdd()|multiAdd()]] can have only string or integer keys. If you need to set more complex key store the value separately via [[yii\caching\Cache::set()|set()]] or [[yii\caching\Cache::add()|add()]]. When the same cache storage is used by different applications, you should specify a unique cache key prefix for each application to avoid conflicts of cache keys. This can be done by configuring the [[yii\caching\Cache::keyPrefix]] property. For example, in the application configuration you can write the following code: ```php 'components' => [ 'cache' => [ 'class' => 'yii\caching\ApcCache', 'keyPrefix' => 'myapp', // a unique cache key prefix ], ], ``` To ensure interoperability, only alphanumeric characters should be used. ### Cache Expiration A data item stored in a cache will remain there forever unless it is removed because of some caching policy enforcement (e.g. caching space is full and the oldest data are removed). To change this behavior, you can provide an expiration parameter when calling [[yii\caching\Cache::set()|set()]] to store a data item. The parameter indicates for how many seconds the data item can remain valid in the cache. When you call [[yii\caching\Cache::get()|get()]] to retrieve the data item, if it has passed the expiration time, the method will return `false`, indicating the data item is not found in the cache. For example, ```php // keep the data in cache for at most 45 seconds $cache->set($key, $data, 45); sleep(50); $data = $cache->get($key); if ($data === false) { // $data is expired or is not found in the cache } ``` Since 2.0.11 you may set [[yii\caching\Cache::$defaultDuration|defaultDuration]] value in your cache component configuration if you prefer a custom cache duration over the default unlimited duration. This will allow you not to pass custom `duration` parameter to [[yii\caching\Cache::set()|set()]] each time. ### Cache Dependencies Besides expiration setting, cached data item may also be invalidated by changes of the so-called *cache dependencies*. For example, [[yii\caching\FileDependency]] represents the dependency of a file's modification time. When this dependency changes, it means the corresponding file is modified. As a result, any outdated file content found in the cache should be invalidated and the [[yii\caching\Cache::get()|get()]] call should return `false`. Cache dependencies are represented as objects of [[yii\caching\Dependency]] descendant classes. When you call [[yii\caching\Cache::set()|set()]] to store a data item in the cache, you can pass along an associated cache dependency object. For example, ```php // Create a dependency on the modification time of file example.txt. $dependency = new \yii\caching\FileDependency(['fileName' => 'example.txt']); // The data will expire in 30 seconds. // It may also be invalidated earlier if example.txt is modified. $cache->set($key, $data, 30, $dependency); // The cache will check if the data has expired. // It will also check if the associated dependency was changed. // It will return false if any of these conditions are met. $data = $cache->get($key); ``` Below is a summary of the available cache dependencies: - [[yii\caching\ChainedDependency]]: the dependency is changed if any of the dependencies on the chain is changed. - [[yii\caching\DbDependency]]: the dependency is changed if the query result of the specified SQL statement is changed. - [[yii\caching\ExpressionDependency]]: the dependency is changed if the result of the specified PHP expression is changed. - [[yii\caching\CallbackDependency]]: the dependency is changed if the result of the specified PHP callback is changed. - [[yii\caching\FileDependency]]: the dependency is changed if the file's last modification time is changed. - [[yii\caching\TagDependency]]: associates a cached data item with one or multiple tags. You may invalidate the cached data items with the specified tag(s) by calling [[yii\caching\TagDependency::invalidate()]]. > Note: Avoid using [[yii\caching\Cache::exists()|exists()]] method along with dependencies. It does not check whether the dependency associated with the cached data, if there is any, has changed. So a call to [[yii\caching\Cache::get()|get()]] may return `false` while [[yii\caching\Cache::exists()|exists()]] returns `true`. ## Query Caching Query caching is a special caching feature built on top of data caching. It is provided to cache the result of database queries. Query caching requires a [[yii\db\Connection|DB connection]] and a valid `cache` [application component](#cache-components). The basic usage of query caching is as follows, assuming `$db` is a [[yii\db\Connection]] instance: ```php $result = $db->cache(function ($db) { // the result of the SQL query will be served from the cache // if query caching is enabled and the query result is found in the cache return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne(); }); ``` Query caching can be used for [DAO](db-dao.md) as well as [ActiveRecord](db-active-record.md): ```php $result = Customer::getDb()->cache(function ($db) { return Customer::find()->where(['id' => 1])->one(); }); ``` > Info: Some DBMS (e.g. [MySQL](https://dev.mysql.com/doc/refman/5.6/en/query-cache.html)) also support query caching on the DB server-side. You may choose to use either query caching mechanism. The query caching described above has the advantage that you may specify flexible cache dependencies and are potentially more efficient. Since 2.0.14 you can use the following shortcuts: ```php (new Query())->cache(7200)->all(); // and User::find()->cache(7200)->all(); ``` ### Configurations Query caching has three global configurable options through [[yii\db\Connection]]: * [[yii\db\Connection::enableQueryCache|enableQueryCache]]: whether to turn on or off query caching. It defaults to `true`. Note that to effectively turn on query caching, you also need to have a valid cache, as specified by [[yii\db\Connection::queryCache|queryCache]]. * [[yii\db\Connection::queryCacheDuration|queryCacheDuration]]: this represents the number of seconds that a query result can remain valid in the cache. You can use 0 to indicate a query result should remain in the cache forever. This property is the default value used when [[yii\db\Connection::cache()]] is called without specifying a duration. * [[yii\db\Connection::queryCache|queryCache]]: this represents the ID of the cache application component. It defaults to `'cache'`. Query caching is enabled only if there is a valid cache application component. ### Usages You can use [[yii\db\Connection::cache()]] if you have multiple SQL queries that need to take advantage of query caching. The usage is as follows, ```php $duration = 60; // cache query results for 60 seconds. $dependency = ...; // optional dependency $result = $db->cache(function ($db) { // ... perform SQL queries here ... return $result; }, $duration, $dependency); ``` Any SQL queries in the anonymous function will be cached for the specified duration with the specified dependency. If the result of a query is found valid in the cache, the query will be skipped and the result will be served from the cache instead. If you do not specify the `$duration` parameter, the value of [[yii\db\Connection::queryCacheDuration|queryCacheDuration]] will be used instead. Sometimes within `cache()`, you may want to disable query caching for some particular queries. You can use [[yii\db\Connection::noCache()]] in this case. ```php $result = $db->cache(function ($db) { // SQL queries that use query caching $db->noCache(function ($db) { // SQL queries that do not use query caching }); // ... return $result; }); ``` If you just want to use query caching for a single query, you can call [[yii\db\Command::cache()]] when building the command. For example, ```php // use query caching and set query cache duration to be 60 seconds $customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->cache(60)->queryOne(); ``` You can also use [[yii\db\Command::noCache()]] to disable query caching for a single command. For example, ```php $result = $db->cache(function ($db) { // SQL queries that use query caching // do not use query caching for this command $customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->noCache()->queryOne(); // ... return $result; }); ``` ### Limitations Query caching does not work with query results that contain resource handlers. For example, when using the `BLOB` column type in some DBMS, the query result will return a resource handler for the column data. Some caching storage has size limitation. For example, memcache limits the maximum size of each entry to be 1MB. Therefore, if the size of a query result exceeds this limit, the caching will fail. ## Cache Flushing When you need to invalidate all the stored cache data, you can call [[yii\caching\Cache::flush()]]. You can flush the cache from the console by calling `yii cache/flush` as well. - `yii cache`: lists the available caches in application - `yii cache/flush cache1 cache2`: flushes the cache components `cache1`, `cache2` (you can pass multiple component names separated with space) - `yii cache/flush-all`: flushes all cache components in the application - `yii cache/flush-schema db`: clears DB schema cache for a given connection component > Info: Console application uses a separate configuration file by default. Ensure, that you have the same caching components in your web and console application configs to reach the proper effect. ================================================ FILE: docs/guide/caching-fragment.md ================================================ Fragment Caching ================ Fragment caching refers to caching a fragment of a Web page. For example, if a page displays a summary of yearly sale in a table, you can store this table in cache to eliminate the time needed to generate this table for each request. Fragment caching is built on top of [data caching](caching-data.md). To use fragment caching, use the following construct in a [view](structure-views.md): ```php if ($this->beginCache($id)) { // ... generate content here ... $this->endCache(); } ``` That is, enclose content generation logic in a pair of [[yii\base\View::beginCache()|beginCache()]] and [[yii\base\View::endCache()|endCache()]] calls. If the content is found in the cache, [[yii\base\View::beginCache()|beginCache()]] will render the cached content and return `false`, thus skip the content generation logic. Otherwise, your content generation logic will be called, and when [[yii\base\View::endCache()|endCache()]] is called, the generated content will be captured and stored in the cache. Like [data caching](caching-data.md), a unique `$id` is needed to identify a content cache. To delete fragment caching you can use ```php Yii::$app->cache->delete(['yii\widgets\FragmentCache', $id]); ``` ## Caching Options You may specify additional options about fragment caching by passing the option array as the second parameter to the [[yii\base\View::beginCache()|beginCache()]] method. Behind the scene, this option array will be used to configure a [[yii\widgets\FragmentCache]] widget which implements the actual fragment caching functionality. ### Duration Perhaps the most commonly used option of fragment caching is [[yii\widgets\FragmentCache::duration|duration]]. It specifies for how many seconds the content can remain valid in a cache. The following code caches the content fragment for at most one hour: ```php if ($this->beginCache($id, ['duration' => 3600])) { // ... generate content here ... $this->endCache(); } ``` If the option is not set, it will take the default value 60, which means the cached content will expire in 60 seconds. ### Dependencies Like [data caching](caching-data.md#cache-dependencies), content fragment being cached can also have dependencies. For example, the content of a post being displayed depends on whether or not the post is modified. To specify a dependency, set the [[yii\widgets\FragmentCache::dependency|dependency]] option, which can be either an [[yii\caching\Dependency]] object or a configuration array for creating a dependency object. The following code specifies that the fragment content depends on the change of the `updated_at` column value: ```php $dependency = [ 'class' => 'yii\caching\DbDependency', 'sql' => 'SELECT MAX(updated_at) FROM post', ]; if ($this->beginCache($id, ['dependency' => $dependency])) { // ... generate content here ... $this->endCache(); } ``` ### Variations Content being cached may be variated according to some parameters. For example, for a Web application supporting multiple languages, the same piece of view code may generate the content in different languages. Therefore, you may want to make the cached content variated according to the current application language. To specify cache variations, set the [[yii\widgets\FragmentCache::variations|variations]] option, which should be an array of scalar values, each representing a particular variation factor. For example, to make the cached content variated by the language, you may use the following code: ```php if ($this->beginCache($id, ['variations' => [Yii::$app->language]])) { // ... generate content here ... $this->endCache(); } ``` ### Toggling Caching Sometimes you may want to enable fragment caching only when certain conditions are met. For example, for a page displaying a form, you only want to cache the form when it is initially requested (via GET request). Any subsequent display (via POST request) of the form should not be cached because the form may contain user input. To do so, you may set the [[yii\widgets\FragmentCache::enabled|enabled]] option, like the following: ```php if ($this->beginCache($id, ['enabled' => Yii::$app->request->isGet])) { // ... generate content here ... $this->endCache(); } ``` ## Nested Caching Fragment caching can be nested. That is, a cached fragment can be enclosed within another fragment which is also cached. For example, the comments are cached in an inner fragment cache, and they are cached together with the post content in an outer fragment cache. The following code shows how two fragment caches can be nested: ```php if ($this->beginCache($id1)) { // ...content generation logic... if ($this->beginCache($id2, $options2)) { // ...content generation logic... $this->endCache(); } // ...content generation logic... $this->endCache(); } ``` Different caching options can be set for the nested caches. For example, the inner caches and the outer caches can use different cache duration values. Even when the data cached in the outer cache is invalidated, the inner cache may still provide the valid inner fragment. However, it is not true vice versa. If the outer cache is evaluated to be valid, it will continue to provide the same cached copy even after the content in the inner cache has been invalidated. Therefore, you must be careful in setting the durations or the dependencies of the nested caches, otherwise the outdated inner fragments may be kept in the outer fragment. ## Dynamic Content When using fragment caching, you may encounter the situation where a large fragment of content is relatively static except at one or a few places. For example, a page header may display the main menu bar together with the name of the current user. Another problem is that the content being cached may contain PHP code that must be executed for every request (e.g. the code for registering an asset bundle). Both problems can be solved by the so-called *dynamic content* feature. A dynamic content means a fragment of output that should not be cached even if it is enclosed within a fragment cache. To make the content dynamic all the time, it has to be generated by executing some PHP code for every request, even if the enclosing content is being served from cache. You may call [[yii\base\View::renderDynamic()]] within a cached fragment to insert dynamic content at the desired place, like the following, ```php if ($this->beginCache($id1)) { // ...content generation logic... echo $this->renderDynamic('return Yii::$app->user->identity->name;'); // ...content generation logic... $this->endCache(); } ``` The [[yii\base\View::renderDynamic()|renderDynamic()]] method takes a piece of PHP code as its parameter. The return value of the PHP code is treated as the dynamic content. The same PHP code will be executed for every request, no matter the enclosing fragment is being served from cached or not. > Note: since version 2.0.14 a dynamic content API is exposed via the [[yii\base\DynamicContentAwareInterface]] interface and its [[yii\base\DynamicContentAwareTrait]] trait. As an example, you may refer to the [[yii\widgets\FragmentCache]] class. ================================================ FILE: docs/guide/caching-http.md ================================================ HTTP Caching ============ Besides server-side caching that we have described in the previous sections, Web applications may also exploit client-side caching to save the time for generating and transmitting the same page content. To use client-side caching, you may configure [[yii\filters\HttpCache]] as a filter for controller actions whose rendering result may be cached on the client-side. [[yii\filters\HttpCache|HttpCache]] only works for `GET` and `HEAD` requests. It can handle three kinds of cache-related HTTP headers for these requests: * [[yii\filters\HttpCache::lastModified|Last-Modified]] * [[yii\filters\HttpCache::etagSeed|Etag]] * [[yii\filters\HttpCache::cacheControlHeader|Cache-Control]] ## `Last-Modified` Header The `Last-Modified` header uses a timestamp to indicate if the page has been modified since the client caches it. You may configure the [[yii\filters\HttpCache::lastModified]] property to enable sending the `Last-Modified` header. The property should be a PHP callable returning a UNIX timestamp about the page modification time. The signature of the PHP callable should be as follows, ```php /** * @param Action $action the action object that is being handled currently * @param array $params the value of the "params" property * @return int a UNIX timestamp representing the page modification time */ function ($action, $params) ``` The following is an example of making use of the `Last-Modified` header: ```php public function behaviors() { return [ [ 'class' => 'yii\filters\HttpCache', 'only' => ['index'], 'lastModified' => function ($action, $params) { $q = new \yii\db\Query(); return $q->from('post')->max('updated_at'); }, ], ]; } ``` The above code states that HTTP caching should be enabled for the `index` action only. It should generate a `Last-Modified` HTTP header based on the last update time of posts. When a browser visits the `index` page for the first time, the page will be generated on the server and sent to the browser; If the browser visits the same page again and there is no post being modified during the period, the server will not re-generate the page, and the browser will use the cached version on the client-side. As a result, server-side rendering and page content transmission are both skipped. ## `ETag` Header The "Entity Tag" (or `ETag` for short) header use a hash to represent the content of a page. If the page is changed, the hash will be changed as well. By comparing the hash kept on the client-side with the hash generated on the server-side, the cache may determine whether the page has been changed and should be re-transmitted. You may configure the [[yii\filters\HttpCache::etagSeed]] property to enable sending the `ETag` header. The property should be a PHP callable returning a seed for generating the ETag hash. The signature of the PHP callable should be as follows, ```php /** * @param Action $action the action object that is being handled currently * @param array $params the value of the "params" property * @return string a string used as the seed for generating an ETag hash */ function ($action, $params) ``` The following is an example of making use of the `ETag` header: ```php public function behaviors() { return [ [ 'class' => 'yii\filters\HttpCache', 'only' => ['view'], 'etagSeed' => function ($action, $params) { $post = $this->findModel(\Yii::$app->request->get('id')); return serialize([$post->title, $post->content]); }, ], ]; } ``` The above code states that HTTP caching should be enabled for the `view` action only. It should generate an `ETag` HTTP header based on the title and content of the requested post. When a browser visits the `view` page for the first time, the page will be generated on the server and sent to the browser; If the browser visits the same page again and there is no change to the title and content of the post, the server will not re-generate the page, and the browser will use the cached version on the client-side. As a result, server-side rendering and page content transmission are both skipped. ETags allow more complex and/or more precise caching strategies than `Last-Modified` headers. For instance, an ETag can be invalidated if the site has switched to another theme. Expensive ETag generation may defeat the purpose of using `HttpCache` and introduce unnecessary overhead, since they need to be re-evaluated on every request. Try to find a simple expression that invalidates the cache if the page content has been modified. > Note: In compliance to [RFC 7232](https://datatracker.ietf.org/doc/html/rfc7232#section-2.4), `HttpCache` will send out both `ETag` and `Last-Modified` headers if they are both configured. And if the client sends both of the `If-None-Match` header and the `If-Modified-Since` header, only the former will be respected. ## `Cache-Control` Header The `Cache-Control` header specifies the general caching policy for pages. You may send it by configuring the [[yii\filters\HttpCache::cacheControlHeader]] property with the header value. By default, the following header will be sent: ``` Cache-Control: public, max-age=3600 ``` ## Session Cache Limiter When a page uses session, PHP will automatically send some cache-related HTTP headers as specified in the `session.cache_limiter` PHP INI setting. These headers may interfere or disable the caching that you want from `HttpCache`. To prevent this problem, by default `HttpCache` will disable sending these headers automatically. If you want to change this behavior, you should configure the [[yii\filters\HttpCache::sessionCacheLimiter]] property. The property can take a string value, including `public`, `private`, `private_no_expire`, and `nocache`. Please refer to the PHP manual about [session_cache_limiter()](https://www.php.net/manual/en/function.session-cache-limiter.php) for explanations about these values. ## SEO Implications Search engine bots tend to respect cache headers. Since some crawlers have a limit on how many pages per domain they process within a certain time span, introducing caching headers may help indexing your site as they reduce the number of pages that need to be processed. ================================================ FILE: docs/guide/caching-overview.md ================================================ Caching ======= Caching is a cheap and effective way to improve the performance of a Web application. By storing relatively static data in cache and serving it from cache when requested, the application saves the time that would be required to generate the data from scratch every time. Caching can occur at different levels and places in a Web application. On the server-side, at the lower level, cache may be used to store basic data, such as a list of most recent article information fetched from database; and at the higher level, cache may be used to store fragments or whole of Web pages, such as the rendering result of the most recent articles. On the client-side, HTTP caching may be used to keep most recently visited page content in the browser cache. Yii supports all these caching mechanisms: * [Data caching](caching-data.md) * [Fragment caching](caching-fragment.md) * [Page caching](caching-page.md) * [HTTP caching](caching-http.md) ================================================ FILE: docs/guide/caching-page.md ================================================ Page Caching ============ Page caching refers to caching the content of a whole page on the server-side. Later when the same page is requested again, its content will be served from the cache instead of regenerating it from scratch. Page caching is supported by [[yii\filters\PageCache]], an [action filter](structure-filters.md). It can be used like the following in a controller class: ```php public function behaviors() { return [ [ 'class' => 'yii\filters\PageCache', 'only' => ['index'], 'duration' => 60, 'variations' => [ \Yii::$app->language, ], 'dependency' => [ 'class' => 'yii\caching\DbDependency', 'sql' => 'SELECT COUNT(*) FROM post', ], ], ]; } ``` The above code states that page caching should be used only for the `index` action. The page content should be cached for at most 60 seconds and should be variated by the current application language and the cached page should be invalidated if the total number of posts is changed. As you can see, page caching is very similar to [fragment caching](caching-fragment.md). They both support options such as `duration`, `dependencies`, `variations`, and `enabled`. Their main difference is that page caching is implemented as an [action filter](structure-filters.md) while fragment caching a [widget](structure-widgets.md). You can use [fragment caching](caching-fragment.md) as well as [dynamic content](caching-fragment.md#dynamic-content) together with page caching. ================================================ FILE: docs/guide/concept-aliases.md ================================================ Aliases ======= Aliases are used to represent file paths or URLs so that you don't have to hard-code absolute paths or URLs in your project. An alias must start with the `@` character to be differentiated from normal file paths and URLs. Alias defined without leading `@` will be prefixed with `@` character. Yii has many pre-defined aliases already available. For example, the alias `@yii` represents the installation path of the Yii framework; `@web` represents the base URL for the currently running Web application. Defining Aliases ---------------- You can define an alias for a file path or URL by calling [[Yii::setAlias()]]: ```php // an alias of a file path Yii::setAlias('@foo', '/path/to/foo'); // an alias of a URL Yii::setAlias('@bar', 'https://www.example.com'); // an alias of a concrete file that contains a \foo\Bar class Yii::setAlias('@foo/Bar.php', '/definitely/not/foo/Bar.php'); ``` > Note: The file path or URL being aliased may *not* necessarily refer to an existing file or resource. Given a defined alias, you may derive a new alias (without the need of calling [[Yii::setAlias()]]) by appending a slash `/` followed with one or more path segments. The aliases defined via [[Yii::setAlias()]] becomes the *root alias*, while aliases derived from it are *derived aliases*. For example, `@foo` is a root alias, while `@foo/bar/file.php` is a derived alias. You can define an alias using another alias (either root or derived): ```php Yii::setAlias('@foobar', '@foo/bar'); ``` Root aliases are usually defined during the [bootstrapping](runtime-bootstrapping.md) stage. For example, you may call [[Yii::setAlias()]] in the [entry script](structure-entry-scripts.md). For convenience, [Application](structure-applications.md) provides a writable property named `aliases` that you can configure in the application [configuration](concept-configurations.md): ```php return [ // ... 'aliases' => [ '@foo' => '/path/to/foo', '@bar' => 'https://www.example.com', ], ]; ``` Resolving Aliases ----------------- You can call [[Yii::getAlias()]] to resolve a root alias into the file path or URL it represents. The same method can also resolve a derived alias into the corresponding file path or URL: ```php echo Yii::getAlias('@foo'); // displays: /path/to/foo echo Yii::getAlias('@bar'); // displays: https://www.example.com echo Yii::getAlias('@foo/bar/file.php'); // displays: /path/to/foo/bar/file.php ``` The path/URL represented by a derived alias is determined by replacing the root alias part with its corresponding path/URL in the derived alias. > Note: The [[Yii::getAlias()]] method does not check whether the resulting path/URL refers to an existing file or resource. A root alias may also contain slash `/` characters. The [[Yii::getAlias()]] method is intelligent enough to tell which part of an alias is a root alias and thus correctly determines the corresponding file path or URL: ```php Yii::setAlias('@foo', '/path/to/foo'); Yii::setAlias('@foo/bar', '/path2/bar'); Yii::getAlias('@foo/test/file.php'); // displays: /path/to/foo/test/file.php Yii::getAlias('@foo/bar/file.php'); // displays: /path2/bar/file.php ``` If `@foo/bar` is not defined as a root alias, the last statement would display `/path/to/foo/bar/file.php`. Using Aliases ------------- Aliases are recognized in many places in Yii without needing to call [[Yii::getAlias()]] to convert them into paths or URLs. For example, [[yii\caching\FileCache::cachePath]] can accept both a file path and an alias representing a file path, thanks to the `@` prefix which allows it to differentiate a file path from an alias. ```php use yii\caching\FileCache; $cache = new FileCache([ 'cachePath' => '@runtime/cache', ]); ``` Please pay attention to the API documentation to see if a property or method parameter supports aliases. Predefined Aliases ------------------ Yii predefines a set of aliases to easily reference commonly used file paths and URLs: - `@yii`, the directory where the `BaseYii.php` file is located (also called the framework directory). - `@app`, the [[yii\base\Application::basePath|base path]] of the currently running application. - `@runtime`, the [[yii\base\Application::runtimePath|runtime path]] of the currently running application. Defaults to `@app/runtime`. - `@webroot`, the Web root directory of the currently running Web application. It is determined based on the directory containing the [entry script](structure-entry-scripts.md). - `@web`, the base URL of the currently running Web application. It has the same value as [[yii\web\Request::baseUrl]]. - `@vendor`, the [[yii\base\Application::vendorPath|Composer vendor directory]]. Defaults to `@app/vendor`. - `@bower`, the root directory that contains [bower packages](https://bower.io/). Defaults to `@vendor/bower`. - `@npm`, the root directory that contains [npm packages](https://www.npmjs.com/). Defaults to `@vendor/npm`. The `@yii` alias is defined when you include the `Yii.php` file in your [entry script](structure-entry-scripts.md). The rest of the aliases are defined in the application constructor when applying the application [configuration](concept-configurations.md). > Note: `@web` and `@webroot` aliases as their descriptions indicate are defined within [[yii\web\Application|Web application]] and therefore are not available for [[yii\console\Application|Console application]] by default. Extension Aliases ----------------- An alias is automatically defined for each [extension](structure-extensions.md) that is installed via Composer. Each alias is named after the root namespace of the extension as declared in its `composer.json` file, and each alias represents the root directory of the package. For example, if you install the `yiisoft/yii2-jui` extension, you will automatically have the alias `@yii/jui` defined during the [bootstrapping](runtime-bootstrapping.md) stage, equivalent to: ```php Yii::setAlias('@yii/jui', 'VendorPath/yiisoft/yii2-jui'); ``` ================================================ FILE: docs/guide/concept-autoloading.md ================================================ Class Autoloading ================= Yii relies on the [class autoloading mechanism](https://www.php.net/manual/en/language.oop5.autoload.php) to locate and include all required class files. It provides a high-performance class autoloader that is compliant with the [PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md). The autoloader is installed when you include the `Yii.php` file. > Note: For simplicity of description, in this section we will only talk about autoloading of classes. However, keep in mind that the content we are describing here applies to autoloading of interfaces and traits as well. Using the Yii Autoloader ------------------------ To make use of the Yii class autoloader, you should follow two simple rules when creating and naming your classes: * Each class must be under a [namespace](https://www.php.net/manual/en/language.namespaces.php) (e.g. `foo\bar\MyClass`) * Each class must be saved in an individual file whose path is determined by the following algorithm: ```php // $className is a fully qualified class name without the leading backslash $classFile = Yii::getAlias('@' . str_replace('\\', '/', $className) . '.php'); ``` For example, if a class name and namespace is `foo\bar\MyClass`, the [alias](concept-aliases.md) for the corresponding class file path would be `@foo/bar/MyClass.php`. In order for this alias to be resolvable into a file path, either `@foo` or `@foo/bar` must be a [root alias](concept-aliases.md#defining-aliases). When using the [Basic Project Template](start-installation.md), you may put your classes under the top-level namespace `app` so that they can be autoloaded by Yii without the need of defining a new alias. This is because `@app` is a [predefined alias](concept-aliases.md#predefined-aliases), and a class name like `app\components\MyClass` can be resolved into the class file `AppBasePath/components/MyClass.php`, according to the algorithm just described. In the [Advanced Project Template](https://github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide/README.md), each tier has its own root alias. For example, the front-end tier has a root alias `@frontend`, while the back-end tier root alias is `@backend`. As a result, you may put the front-end classes under the namespace `frontend` while the back-end classes are under `backend`. This will allow these classes to be autoloaded by the Yii autoloader. To add a custom namespace to the autoloader you need to define an alias for the base directory of the namespace using [[Yii::setAlias()]]. For example to load classes in the `foo` namespace that are located in the `path/to/foo` directory you will call `Yii::setAlias('@foo', 'path/to/foo')`. Class Map --------- The Yii class autoloader supports the *class map* feature, which maps class names to the corresponding class file paths. When the autoloader is loading a class, it will first check if the class is found in the map. If so, the corresponding file path will be included directly without further checks. This makes class autoloading super fast. In fact, all core Yii classes are autoloaded this way. You may add a class to the class map, stored in `Yii::$classMap`, using: ```php Yii::$classMap['foo\bar\MyClass'] = 'path/to/MyClass.php'; ``` [Aliases](concept-aliases.md) can be used to specify class file paths. You should set the class map in the [bootstrapping](runtime-bootstrapping.md) process so that the map is ready before your classes are used. Using Other Autoloaders ----------------------- Because Yii embraces Composer as a package dependency manager, it is recommended that you also install the Composer autoloader. If you are using 3rd-party libraries that have their own autoloaders, you should also install those. When using the Yii autoloader together with other autoloaders, you should include the `Yii.php` file *after* all other autoloaders are installed. This will make the Yii autoloader the first one responding to any class autoloading request. For example, the following code is extracted from the [entry script](structure-entry-scripts.md) of the [Basic Project Template](start-installation.md). The first line installs the Composer autoloader, while the second line installs the Yii autoloader: ```php require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php'; ``` You may use the Composer autoloader alone without the Yii autoloader. However, by doing so, the performance of your class autoloading may be degraded, and you must follow the rules set by Composer in order for your classes to be autoloadable. > Info: If you do not want to use the Yii autoloader, you must create your own version of the `Yii.php` file and include it in your [entry script](structure-entry-scripts.md). Autoloading Extension Classes ----------------------------- The Yii autoloader is capable of autoloading [extension](structure-extensions.md) classes. The sole requirement is that an extension specifies the `autoload` section correctly in its `composer.json` file. Please refer to the [Composer documentation](https://getcomposer.org/doc/04-schema.md#autoload) for more details about specifying `autoload`. In case you do not use the Yii autoloader, the Composer autoloader can still autoload extension classes for you. ================================================ FILE: docs/guide/concept-behaviors.md ================================================ Behaviors ========= Behaviors are instances of [[yii\base\Behavior]], or of a child class. Behaviors, also known as [mixins](https://en.wikipedia.org/wiki/Mixin), allow you to enhance the functionality of an existing [[yii\base\Component|component]] class without needing to change the class's inheritance. Attaching a behavior to a component "injects" the behavior's methods and properties into the component, making those methods and properties accessible as if they were defined in the component class itself. Moreover, a behavior can respond to the [events](concept-events.md) triggered by the component, which allows behaviors to also customize the normal code execution of the component. Defining Behaviors ------------------ To define a behavior, create a class that extends [[yii\base\Behavior]], or extends a child class. For example: ```php namespace app\components; use yii\base\Behavior; class MyBehavior extends Behavior { public $prop1; private $_prop2; public function getProp2() { return $this->_prop2; } public function setProp2($value) { $this->_prop2 = $value; } public function foo() { // ... } } ``` The above code defines the behavior class `app\components\MyBehavior`, with two properties `prop1` and `prop2` and one method `foo()`. Note that property `prop2` is defined via the getter `getProp2()` and the setter `setProp2()`. This is the case because [[yii\base\Behavior]] extends [[yii\base\BaseObject]] and therefore supports defining [properties](concept-properties.md) via getters and setters. Because this class is a behavior, when it is attached to a component, that component will then also have the `prop1` and `prop2` properties and the `foo()` method. > Tip: Within a behavior, you can access the component that the behavior is attached to through the [[yii\base\Behavior::owner]] property. > Note: In case [[yii\base\Behavior::__get()]] and/or [[yii\base\Behavior::__set()]] method of behavior is overridden you need to override [[yii\base\Behavior::canGetProperty()]] and/or [[yii\base\Behavior::canSetProperty()]] as well. Handling Component Events ------------------ If a behavior needs to respond to the events triggered by the component it is attached to, it should override the [[yii\base\Behavior::events()]] method. For example: ```php namespace app\components; use yii\db\ActiveRecord; use yii\base\Behavior; class MyBehavior extends Behavior { // ... public function events() { return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', ]; } public function beforeValidate($event) { // ... } } ``` The [[yii\base\Behavior::events()|events()]] method should return a list of events and their corresponding handlers. The above example declares that the [[yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]] event exists and defines its handler, `beforeValidate()`. When specifying an event handler, you may use one of the following formats: * a string that refers to the name of a method of the behavior class, like the example above * an array of an object or class name, and a method name as a string (without parentheses), e.g., `[$object, 'methodName']`; * an anonymous function The signature of an event handler should be as follows, where `$event` refers to the event parameter. Please refer to the [Events](concept-events.md) section for more details about events. ```php function ($event) { } ``` Attaching Behaviors ------------------- You can attach a behavior to a [[yii\base\Component|component]] either statically or dynamically. The former is more common in practice. To attach a behavior statically, override the [[yii\base\Component::behaviors()|behaviors()]] method of the component class to which the behavior is being attached. The [[yii\base\Component::behaviors()|behaviors()]] method should return a list of behavior [configurations](concept-configurations.md). Each behavior configuration can be either a behavior class name or a configuration array: ```php namespace app\models; use yii\db\ActiveRecord; use app\components\MyBehavior; class User extends ActiveRecord { public function behaviors() { return [ // anonymous behavior, behavior class name only MyBehavior::class, // named behavior, behavior class name only 'myBehavior2' => MyBehavior::class, // anonymous behavior, configuration array [ 'class' => MyBehavior::class, 'prop1' => 'value1', 'prop2' => 'value2', ], // named behavior, configuration array 'myBehavior4' => [ 'class' => MyBehavior::class, 'prop1' => 'value1', 'prop2' => 'value2', ] ]; } } ``` You may associate a name with a behavior by specifying the array key corresponding to the behavior configuration. In this case, the behavior is called a *named behavior*. In the above example, there are two named behaviors: `myBehavior2` and `myBehavior4`. If a behavior is not associated with a name, it is called an *anonymous behavior*. To attach a behavior dynamically, call the [[yii\base\Component::attachBehavior()]] method of the component to which the behavior is being attached: ```php use app\components\MyBehavior; // attach a behavior object $component->attachBehavior('myBehavior1', new MyBehavior()); // attach a behavior class $component->attachBehavior('myBehavior2', MyBehavior::class); // attach a configuration array $component->attachBehavior('myBehavior3', [ 'class' => MyBehavior::class, 'prop1' => 'value1', 'prop2' => 'value2', ]); ``` You may attach multiple behaviors at once using the [[yii\base\Component::attachBehaviors()]] method: ```php $component->attachBehaviors([ 'myBehavior1' => new MyBehavior(), // a named behavior MyBehavior::class, // an anonymous behavior ]); ``` You may also attach behaviors through [configurations](concept-configurations.md) like the following: ```php [ 'as myBehavior2' => MyBehavior::class, 'as myBehavior3' => [ 'class' => MyBehavior::class, 'prop1' => 'value1', 'prop2' => 'value2', ], ] ``` For more details, please refer to the [Configurations](concept-configurations.md#configuration-format) section. Using Behaviors --------------- To use a behavior, first attach it to a [[yii\base\Component|component]] per the instructions above. Once a behavior is attached to a component, its usage is straightforward. You can access a *public* member variable or a [property](concept-properties.md) defined by a getter and/or a setter of the behavior through the component it is attached to: ```php // "prop1" is a property defined in the behavior class echo $component->prop1; $component->prop1 = $value; ``` You can also call a *public* method of the behavior similarly: ```php // foo() is a public method defined in the behavior class $component->foo(); ``` As you can see, although `$component` does not define `prop1` and `foo()`, they can be used as if they are part of the component definition due to the attached behavior. If two behaviors define the same property or method and they are both attached to the same component, the behavior that is attached to the component *first* will take precedence when the property or method is accessed. A behavior may be associated with a name when it is attached to a component. If this is the case, you may access the behavior object using the name: ```php $behavior = $component->getBehavior('myBehavior'); ``` You may also get all behaviors attached to a component: ```php $behaviors = $component->getBehaviors(); ``` Detaching Behaviors ------------------- To detach a behavior, call [[yii\base\Component::detachBehavior()]] with the name associated with the behavior: ```php $component->detachBehavior('myBehavior1'); ``` You may also detach *all* behaviors: ```php $component->detachBehaviors(); ``` Using `TimestampBehavior` ------------------------- To wrap up, let's take a look at [[yii\behaviors\TimestampBehavior]]. This behavior supports automatically updating the timestamp attributes of an [[yii\db\ActiveRecord|Active Record]] model anytime the model is saved via `insert()`, `update()` or `save()` method. First, attach this behavior to the [[yii\db\ActiveRecord|Active Record]] class that you plan to use: ```php namespace app\models\User; use yii\db\ActiveRecord; use yii\behaviors\TimestampBehavior; class User extends ActiveRecord { // ... public function behaviors() { return [ [ 'class' => TimestampBehavior::class, 'attributes' => [ ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'], ], // if you're using datetime instead of UNIX timestamp: // 'value' => new Expression('NOW()'), ], ]; } } ``` The behavior configuration above specifies that when the record is being: * inserted, the behavior should assign the current UNIX timestamp to the `created_at` and `updated_at` attributes * updated, the behavior should assign the current UNIX timestamp to the `updated_at` attribute > Note: For the above implementation to work with MySQL database, please declare the columns(`created_at`, `updated_at`) as int(11) for being UNIX timestamp. With that code in place, if you have a `User` object and try to save it, you will find its `created_at` and `updated_at` are automatically filled with the current UNIX timestamp: ```php $user = new User; $user->email = 'test@example.com'; $user->save(); echo $user->created_at; // shows the current timestamp ``` The [[yii\behaviors\TimestampBehavior|TimestampBehavior]] also offers a useful method [[yii\behaviors\TimestampBehavior::touch()|touch()]], which will assign the current timestamp to a specified attribute and save it to the database: ```php $user->touch('login_time'); ``` Other behaviors --------------- There are several built-in and external behaviors available: - [[yii\behaviors\BlameableBehavior]] - automatically fills the specified attributes with the current user ID. - [[yii\behaviors\SluggableBehavior]] - automatically fills the specified attribute with a value that can be used as a slug in a URL. - [[yii\behaviors\AttributeBehavior]] - automatically assigns a specified value to one or multiple attributes of an ActiveRecord object when certain events happen. - [yii2tech\ar\softdelete\SoftDeleteBehavior](https://github.com/yii2tech/ar-softdelete) - provides methods to soft-delete and soft-restore ActiveRecord i.e. set flag or status which marks record as deleted. - [yii2tech\ar\position\PositionBehavior](https://github.com/yii2tech/ar-position) - allows managing records order in an integer field by providing reordering methods. Comparing Behaviors with Traits ---------------------- While behaviors are similar to [traits](https://www.php.net/traits) in that they both "inject" their properties and methods to the primary class, they differ in many aspects. As explained below, they both have pros and cons. They are more like complements to each other rather than alternatives. ### Reasons to Use Behaviors Behavior classes, like normal classes, support inheritance. Traits, on the other hand, can be considered as language-supported copy and paste. They do not support inheritance. Behaviors can be attached and detached to a component dynamically without requiring modification of the component class. To use a trait, you must modify the code of the class using it. Behaviors are configurable while traits are not. Behaviors can customize the code execution of a component by responding to its events. When there can be name conflicts among different behaviors attached to the same component, the conflicts are automatically resolved by prioritizing the behavior attached to the component first. Name conflicts caused by different traits requires manual resolution by renaming the affected properties or methods. ### Reasons to Use Traits Traits are much more efficient than behaviors as behaviors are objects that take both time and memory. IDEs are more friendly to traits as they are a native language construct. ================================================ FILE: docs/guide/concept-components.md ================================================ Components ========== Components are the main building blocks of Yii applications. Components are instances of [[yii\base\Component]], or an extended class. The three main features that components provide to other classes are: * [Properties](concept-properties.md) * [Events](concept-events.md) * [Behaviors](concept-behaviors.md) Separately and combined, these features make Yii classes much more customizable and easier to use. For example, the included [[yii\jui\DatePicker|date picker widget]], a user interface component, can be used in a [view](structure-views.md) to generate an interactive date picker: ```php use yii\jui\DatePicker; echo DatePicker::widget([ 'language' => 'ru', 'name' => 'country', 'clientOptions' => [ 'dateFormat' => 'yy-mm-dd', ], ]); ``` The widget's properties are easily writable because the class extends [[yii\base\Component]]. While components are very powerful, they are a bit heavier than normal objects, due to the fact that it takes extra memory and CPU time to support [event](concept-events.md) and [behavior](concept-behaviors.md) functionality in particular. If your components do not need these two features, you may consider extending your component class from [[yii\base\BaseObject]] instead of [[yii\base\Component]]. Doing so will make your components as efficient as normal PHP objects, but with added support for [properties](concept-properties.md). When extending your class from [[yii\base\Component]] or [[yii\base\BaseObject]], it is recommended that you follow these conventions: - If you override the constructor, specify a `$config` parameter as the constructor's *last* parameter, and then pass this parameter to the parent constructor. - Always call the parent constructor *at the end* of your overriding constructor. - If you override the [[yii\base\BaseObject::init()]] method, make sure you call the parent implementation of `init()` *at the beginning* of your `init()` method. For example: ```php 3, 'prop2' => 4]); // alternatively $component = \Yii::createObject([ 'class' => MyClass::class, 'prop1' => 3, 'prop2' => 4, ], [1, 2]); ``` > Info: While the approach of calling [[Yii::createObject()]] looks more complicated, it is more powerful because it is > implemented on top of a [dependency injection container](concept-di-container.md). The [[yii\base\BaseObject]] class enforces the following object lifecycle: 1. Pre-initialization within the constructor. You can set default property values here. 2. Object configuration via `$config`. The configuration may overwrite the default values set within the constructor. 3. Post-initialization within [[yii\base\BaseObject::init()|init()]]. You may override this method to perform sanity checks and normalization of the properties. 4. Object method calls. The first three steps all happen within the object's constructor. This means that once you get a class instance (i.e., an object), that object has already been initialized to a proper, reliable state. ================================================ FILE: docs/guide/concept-configurations.md ================================================ Configurations ============== Configurations are widely used in Yii when creating new objects or initializing existing objects. Configurations usually include the class name of the object being created, and a list of initial values that should be assigned to the object's [properties](concept-properties.md). Configurations may also include a list of handlers that should be attached to the object's [events](concept-events.md) and/or a list of [behaviors](concept-behaviors.md) that should also be attached to the object. In the following, a configuration is used to create and initialize a database connection: ```php $config = [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]; $db = Yii::createObject($config); ``` The [[Yii::createObject()]] method takes a configuration array as its argument, and creates an object by instantiating the class named in the configuration. When the object is instantiated, the rest of the configuration will be used to initialize the object's properties, event handlers, and behaviors. If you already have an object, you may use [[Yii::configure()]] to initialize the object's properties with a configuration array: ```php Yii::configure($object, $config); ``` Note that, in this case, the configuration array should not contain a `class` element. ## Configuration Format The format of a configuration can be formally described as: ```php [ 'class' => 'ClassName', 'propertyName' => 'propertyValue', 'on eventName' => $eventHandler, 'as behaviorName' => $behaviorConfig, ] ``` where * The `class` element specifies a fully qualified class name for the object being created. * The `propertyName` elements specify the initial values for the named property. The keys are the property names, and the values are the corresponding initial values. Only public member variables and [properties](concept-properties.md) defined by getters/setters can be configured. * The `on eventName` elements specify what handlers should be attached to the object's [events](concept-events.md). Notice that the array keys are formed by prefixing event names with `on `. Please refer to the [Events](concept-events.md) section for supported event handler formats. * The `as behaviorName` elements specify what [behaviors](concept-behaviors.md) should be attached to the object. Notice that the array keys are formed by prefixing behavior names with `as `; the value, `$behaviorConfig`, represents the configuration for creating a behavior, like a normal configuration described here. Below is an example showing a configuration with initial property values, event handlers, and behaviors: ```php [ 'class' => 'app\components\SearchEngine', 'apiKey' => 'xxxxxxxx', 'on search' => function ($event) { Yii::info("Keyword searched: " . $event->keyword); }, 'as indexer' => [ 'class' => 'app\components\IndexerBehavior', // ... property init values ... ], ] ``` ## Using Configurations Configurations are used in many places in Yii. At the beginning of this section, we have shown how to create an object according to a configuration by using [[Yii::createObject()]]. In this subsection, we will describe application configurations and widget configurations - two major usages of configurations. ### Application Configurations The configuration for an [application](structure-applications.md) is probably one of the most complex arrays in Yii. This is because the [[yii\web\Application|application]] class has a lot of configurable properties and events. More importantly, its [[yii\web\Application::components|components]] property can receive an array of configurations for creating components that are registered through the application. The following is an abstract from the application configuration file for the [Basic Project Template](start-installation.md). ```php $config = [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'extensions' => require __DIR__ . '/../vendor/yiisoft/extensions.php', 'components' => [ 'cache' => [ 'class' => 'yii\caching\FileCache', ], 'mailer' => [ 'class' => 'yii\symfonymailer\Mailer', ], 'log' => [ 'class' => 'yii\log\Dispatcher', 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', ], ], ], 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=stay2', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], ], ]; ``` The configuration does not have a `class` key. This is because it is used as follows in an [entry script](structure-entry-scripts.md), where the class name is already given, ```php (new yii\web\Application($config))->run(); ``` More details about configuring the `components` property of an application can be found in the [Applications](structure-applications.md) section and the [Service Locator](concept-service-locator.md) section. Since version 2.0.11, the application configuration supports [Dependency Injection Container](concept-di-container.md) configuration using `container` property. For example: ```php $config = [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'extensions' => require __DIR__ . '/../vendor/yiisoft/extensions.php', 'container' => [ 'definitions' => [ 'yii\widgets\LinkPager' => ['maxButtonCount' => 5] ], 'singletons' => [ // Dependency Injection Container singletons configuration ] ] ]; ``` To know more about the possible values of `definitions` and `singletons` configuration arrays and real-life examples, please read [Advanced Practical Usage](concept-di-container.md#advanced-practical-usage) subsection of the [Dependency Injection Container](concept-di-container.md) article. ### Widget Configurations When using [widgets](structure-widgets.md), you often need to use configurations to customize the widget properties. Both of the [[yii\base\Widget::widget()]] and [[yii\base\Widget::begin()]] methods can be used to create a widget. They take a configuration array, like the following, ```php use yii\widgets\Menu; echo Menu::widget([ 'activateItems' => false, 'items' => [ ['label' => 'Home', 'url' => ['site/index']], ['label' => 'Products', 'url' => ['product/index']], ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest], ], ]); ``` The above code creates a `Menu` widget and initializes its `activateItems` property to be `false`. The `items` property is also configured with menu items to be displayed. Note that because the class name is already given, the configuration array should NOT have the `class` key. ## Configuration Files When a configuration is very complex, a common practice is to store it in one or multiple PHP files, known as *configuration files*. A configuration file returns a PHP array representing the configuration. For example, you may keep an application configuration in a file named `web.php`, like the following, ```php return [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'extensions' => require __DIR__ . '/../vendor/yiisoft/extensions.php', 'components' => require __DIR__ . '/components.php', ]; ``` Because the `components` configuration is complex too, you store it in a separate file called `components.php` and "require" this file in `web.php` as shown above. The content of `components.php` is as follows, ```php return [ 'cache' => [ 'class' => 'yii\caching\FileCache', ], 'mailer' => [ 'class' => 'yii\symfonymailer\Mailer', ], 'log' => [ 'class' => 'yii\log\Dispatcher', 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', ], ], ], 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=stay2', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], ]; ``` To get a configuration stored in a configuration file, simply "require" it, like the following: ```php $config = require 'path/to/web.php'; (new yii\web\Application($config))->run(); ``` ## Default Configurations The [[Yii::createObject()]] method is implemented based on a [dependency injection container](concept-di-container.md). It allows you to specify a set of the so-called *default configurations* which will be applied to ALL instances of the specified classes when they are being created using [[Yii::createObject()]]. The default configurations can be specified by calling `Yii::$container->set()` in the [bootstrapping](runtime-bootstrapping.md) code. For example, if you want to customize [[yii\widgets\LinkPager]] so that ALL link pagers will show at most 5 page buttons (the default value is 10), you may use the following code to achieve this goal: ```php \Yii::$container->set('yii\widgets\LinkPager', [ 'maxButtonCount' => 5, ]); ``` Without using default configurations, you would have to configure `maxButtonCount` in every place where you use link pagers. ## Environment Constants Configurations often vary according to the environment in which an application runs. For example, in development environment, you may want to use a database named `mydb_dev`, while on production server you may want to use the `mydb_prod` database. To facilitate switching environments, Yii provides a constant named `YII_ENV` that you may define in the [entry script](structure-entry-scripts.md) of your application. For example, ```php defined('YII_ENV') or define('YII_ENV', 'dev'); ``` You may define `YII_ENV` as one of the following values: - `prod`: production environment. The constant `YII_ENV_PROD` will evaluate as `true`. This is the default value of `YII_ENV` if you do not define it. - `dev`: development environment. The constant `YII_ENV_DEV` will evaluate as `true`. - `test`: testing environment. The constant `YII_ENV_TEST` will evaluate as `true`. With these environment constants, you may specify your configurations conditionally based on the current environment. For example, your application configuration may contain the following code to enable the [debug toolbar and debugger](tool-debugger.md) in development environment. ```php $config = [...]; if (YII_ENV_DEV) { // configuration adjustments for 'dev' environment $config['bootstrap'][] = 'debug'; $config['modules']['debug'] = 'yii\debug\Module'; } return $config; ``` ================================================ FILE: docs/guide/concept-di-container.md ================================================ Dependency Injection Container ============================== A dependency injection (DI) container is an object that knows how to instantiate and configure objects and all their dependent objects. [Martin Fowler's article](https://martinfowler.com/articles/injection.html) has well explained why DI container is useful. Here we will mainly explain the usage of the DI container provided by Yii. Dependency Injection -------------------- Yii provides the DI container feature through the class [[yii\di\Container]]. It supports the following kinds of dependency injection: * Constructor injection; * Method injection; * Setter and property injection; * PHP callable injection; ### Constructor Injection The DI container supports constructor injection with the help of type hints for constructor parameters. The type hints tell the container which classes or interfaces are dependent when it is used to create a new object. The container will try to get the instances of the dependent classes or interfaces and then inject them into the new object through the constructor. For example, ```php class Foo { public function __construct(Bar $bar) { } } $foo = $container->get('Foo'); // which is equivalent to the following: $bar = new Bar; $foo = new Foo($bar); ``` ### Method Injection Usually the dependencies of a class are passed to the constructor and are available inside the class during the whole lifecycle. With Method Injection it is possible to provide a dependency that is only needed by a single method of the class and passing it to the constructor may not be possible or may cause too much overhead in the majority of use cases. A class method can be defined like the `doSomething()` method in the following example: ```php class MyClass extends \yii\base\Component { public function __construct(/*Some lightweight dependencies here*/, $config = []) { // ... } public function doSomething($param1, \my\heavy\Dependency $something) { // do something with $something } } ``` You may call that method either by passing an instance of `\my\heavy\Dependency` yourself or using [[yii\di\Container::invoke()]] like the following: ```php $obj = new MyClass(/*...*/); Yii::$container->invoke([$obj, 'doSomething'], ['param1' => 42]); // $something will be provided by the DI container ``` ### Setter and Property Injection Setter and property injection is supported through [configurations](concept-configurations.md). When registering a dependency or when creating a new object, you can provide a configuration which will be used by the container to inject the dependencies through the corresponding setters or properties. For example, ```php use yii\base\BaseObject; class Foo extends BaseObject { public $bar; private $_qux; public function getQux() { return $this->_qux; } public function setQux(Qux $qux) { $this->_qux = $qux; } } $container->get('Foo', [], [ 'bar' => $container->get('Bar'), 'qux' => $container->get('Qux'), ]); ``` > Info: The [[yii\di\Container::get()]] method takes its third parameter as a configuration array that should be applied to the object being created. If the class implements the [[yii\base\Configurable]] interface (e.g. [[yii\base\BaseObject]]), the configuration array will be passed as the last parameter to the class constructor; otherwise, the configuration will be applied *after* the object is created. ### PHP Callable Injection In this case, the container will use a registered PHP callable to build new instances of a class. Each time when [[yii\di\Container::get()]] is called, the corresponding callable will be invoked. The callable is responsible to resolve the dependencies and inject them appropriately to the newly created objects. For example, ```php $container->set('Foo', function ($container, $params, $config) { $foo = new Foo(new Bar); // ... other initializations ... return $foo; }); $foo = $container->get('Foo'); ``` To hide the complex logic for building a new object, you may use a static class method as callable. For example, ```php class FooBuilder { public static function build($container, $params, $config) { $foo = new Foo(new Bar); // ... other initializations ... return $foo; } } $container->set('Foo', ['app\helper\FooBuilder', 'build']); $foo = $container->get('Foo'); ``` By doing so, the person who wants to configure the `Foo` class no longer needs to be aware of how it is built. Registering Dependencies ------------------------ You can use [[yii\di\Container::set()]] to register dependencies. The registration requires a dependency name as well as a dependency definition. A dependency name can be a class name, an interface name, or an alias name; and a dependency definition can be a class name, a configuration array, or a PHP callable. ```php $container = new \yii\di\Container; // register a class name as is. This can be skipped. $container->set('yii\db\Connection'); // register an interface // When a class depends on the interface, the corresponding class // will be instantiated as the dependent object $container->set('yii\mail\MailInterface', 'yii\symfonymailer\Mailer'); // register an alias name. You can use $container->get('foo') // to create an instance of Connection $container->set('foo', 'yii\db\Connection'); // register an alias with `Instance::of` $container->set('bar', Instance::of('foo')); // register a class with configuration. The configuration // will be applied when the class is instantiated by get() $container->set('yii\db\Connection', [ 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); // register an alias name with class configuration // In this case, a "class" or "__class" element is required to specify the class $container->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); // register callable closure or array // The callable will be executed each time when $container->get('db') is called $container->set('db', function ($container, $params, $config) { return new \yii\db\Connection($config); }); $container->set('db', ['app\db\DbFactory', 'create']); // register a component instance // $container->get('pageCache') will return the same instance each time it is called $container->set('pageCache', new FileCache); ``` > Tip: If a dependency name is the same as the corresponding dependency definition, you do not need to register it with the DI container. A dependency registered via `set()` will generate an instance each time the dependency is needed. You can use [[yii\di\Container::setSingleton()]] to register a dependency that only generates a single instance: ```php $container->setSingleton('yii\db\Connection', [ 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); ``` Resolving Dependencies ---------------------- Once you have registered dependencies, you can use the DI container to create new objects, and the container will automatically resolve dependencies by instantiating them and injecting them into the newly created objects. The dependency resolution is recursive, meaning that if a dependency has other dependencies, those dependencies will also be resolved automatically. You can use [[yii\di\Container::get()|get()]] to either create or get object instance. The method takes a dependency name, which can be a class name, an interface name or an alias name. The dependency name may be registered via [[yii\di\Container::set()|set()]] or [[yii\di\Container::setSingleton()|setSingleton()]]. You may optionally provide a list of class constructor parameters and a [configuration](concept-configurations.md) to configure the newly created object. For example: ```php // "db" is a previously registered alias name $db = $container->get('db'); // equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]); $engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]); // equivalent to: $api = new \app\components\Api($host, $apiKey); $api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); ``` Behind the scene, the DI container does much more work than just creating a new object. The container will first inspect the class constructor to find out dependent class or interface names and then automatically resolve those dependencies recursively. The following code shows a more sophisticated example. The `UserLister` class depends on an object implementing the `UserFinderInterface` interface; the `UserFinder` class implements this interface and depends on a `Connection` object. All these dependencies are declared through type hinting of the class constructor parameters. With proper dependency registration, the DI container is able to resolve these dependencies automatically and creates a new `UserLister` instance with a simple call of `get('userLister')`. ```php namespace app\models; use yii\base\BaseObject; use yii\db\Connection; use yii\di\Container; interface UserFinderInterface { function findUser(); } class UserFinder extends BaseObject implements UserFinderInterface { public $db; public function __construct(Connection $db, $config = []) { $this->db = $db; parent::__construct($config); } public function findUser() { } } class UserLister extends BaseObject { public $finder; public function __construct(UserFinderInterface $finder, $config = []) { $this->finder = $finder; parent::__construct($config); } } $container = new Container; $container->set('yii\db\Connection', [ 'dsn' => '...', ]); $container->set('app\models\UserFinderInterface', [ 'class' => 'app\models\UserFinder', ]); $container->set('userLister', 'app\models\UserLister'); $lister = $container->get('userLister'); // which is equivalent to: $db = new \yii\db\Connection(['dsn' => '...']); $finder = new UserFinder($db); $lister = new UserLister($finder); ``` Practical Usage --------------- Yii creates a DI container when you include the `Yii.php` file in the [entry script](structure-entry-scripts.md) of your application. The DI container is accessible via [[Yii::$container]]. When you call [[Yii::createObject()]], the method will actually call the container's [[yii\di\Container::get()|get()]] method to create a new object. As aforementioned, the DI container will automatically resolve the dependencies (if any) and inject them into obtained object. Because Yii uses [[Yii::createObject()]] in most of its core code to create new objects, this means you can customize the objects globally by dealing with [[Yii::$container]]. For example, let's customize globally the default number of pagination buttons of [[yii\widgets\LinkPager]]. ```php \Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]); ``` Now if you use the widget in a view with the following code, the `maxButtonCount` property will be initialized as 5 instead of the default value 10 as defined in the class. ```php echo \yii\widgets\LinkPager::widget(); ``` You can still override the value set via DI container, though: ```php echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]); ``` > Note: Properties given in the widget call will always override the definition in the DI container. > Even if you specify an array, e.g. `'options' => ['id' => 'mypager']` these will not be merged > with other options but replace them. Another example is to take advantage of the automatic constructor injection of the DI container. Assume your controller class depends on some other objects, such as a hotel booking service. You can declare the dependency through a constructor parameter and let the DI container to resolve it for you. ```php namespace app\controllers; use yii\web\Controller; use app\components\BookingInterface; class HotelController extends Controller { protected $bookingService; public function __construct($id, $module, BookingInterface $bookingService, $config = []) { $this->bookingService = $bookingService; parent::__construct($id, $module, $config); } } ``` If you access this controller from browser, you will see an error complaining the `BookingInterface` cannot be instantiated. This is because you need to tell the DI container how to deal with this dependency: ```php \Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService'); ``` Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. Since Yii 2.0.36 when using PHP 7 action injection is available for both web and console controllers: ```php namespace app\controllers; use yii\web\Controller; use app\components\BookingInterface; class HotelController extends Controller { public function actionBook($id, BookingInterface $bookingService) { $result = $bookingService->book($id); // ... } } ``` Advanced Practical Usage --------------- Say we work on API application and have: - `app\components\Request` class that extends `yii\web\Request` and provides additional functionality - `app\components\Response` class that extends `yii\web\Response` and should have `format` property set to `json` on creation - `app\storage\FileStorage` and `app\storage\DocumentsReader` classes that implement some logic on working with documents that are located in some file storage: ```php class FileStorage { public function __construct($root) { // whatever } } class DocumentsReader { public function __construct(FileStorage $fs) { // whatever } } ``` It is possible to configure multiple definitions at once, passing configuration array to [[yii\di\Container::setDefinitions()|setDefinitions()]] or [[yii\di\Container::setSingletons()|setSingletons()]] method. Iterating over the configuration array, the methods will call [[yii\di\Container::set()|set()]] or [[yii\di\Container::setSingleton()|setSingleton()]] respectively for each item. The configuration array format is: - `key`: class name, interface name or alias name. The key will be passed to the [[yii\di\Container::set()|set()]] method as a first argument `$class`. - `value`: the definition associated with `$class`. Possible values are described in [[yii\di\Container::set()|set()]] documentation for the `$definition` parameter. Will be passed to the [[set()]] method as the second argument `$definition`. For example, let's configure our container to follow the aforementioned requirements: ```php $container->setDefinitions([ 'yii\web\Request' => 'app\components\Request', 'yii\web\Response' => [ 'class' => 'app\components\Response', 'format' => 'json' ], 'app\storage\DocumentsReader' => function ($container, $params, $config) { $fs = new app\storage\FileStorage('/var/tempfiles'); return new app\storage\DocumentsReader($fs); } ]); $reader = $container->get('app\storage\DocumentsReader'); // Will create DocumentReader object with its dependencies as described in the config ``` > Tip: Container may be configured in declarative style using application configuration since version 2.0.11. Check out the [Application Configurations](concept-configurations.md#application-configurations) subsection of the [Configurations](concept-configurations.md) guide article. Everything works, but in case we need to create `DocumentWriter` class, we shall copy-paste the line that creates `FileStorage` object, that is not the smartest way, obviously. As described in the [Resolving Dependencies](#resolving-dependencies) subsection, [[yii\di\Container::set()|set()]] and [[yii\di\Container::setSingleton()|setSingleton()]] can optionally take dependency's constructor parameters as a third argument. To set the constructor parameters, you may use the `__construct()` option: Let's modify our example: ```php $container->setDefinitions([ 'tempFileStorage' => [ // we've created an alias for convenience 'class' => 'app\storage\FileStorage', '__construct()' => ['/var/tempfiles'], // could be extracted from some config files ], 'app\storage\DocumentsReader' => [ 'class' => 'app\storage\DocumentsReader', '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ 'class' => 'app\storage\DocumentsWriter', '__construct()' => [Instance::of('tempFileStorage')] ] ]); $reader = $container->get('app\storage\DocumentsReader'); // Will behave exactly the same as in the previous example. ``` You might notice `Instance::of('tempFileStorage')` notation. It means, that the [[yii\di\Container|Container]] will implicitly provide a dependency registered with the name of `tempFileStorage` and pass it as the first argument of `app\storage\DocumentsWriter` constructor. > Note: [[yii\di\Container::setDefinitions()|setDefinitions()]] and [[yii\di\Container::setSingletons()|setSingletons()]] methods are available since version 2.0.11. Another step on configuration optimization is to register some dependencies as singletons. A dependency registered via [[yii\di\Container::set()|set()]] will be instantiated each time it is needed. Some classes do not change the state during runtime, therefore they may be registered as singletons in order to increase the application performance. A good example could be `app\storage\FileStorage` class, that executes some operations on file system with a simple API (e.g. `$fs->read()`, `$fs->write()`). These operations do not change the internal class state, so we can create its instance once and use it multiple times. ```php $container->setSingletons([ 'tempFileStorage' => [ 'class' => 'app\storage\FileStorage', '__construct()' => ['/var/tempfiles'] ], ]); $container->setDefinitions([ 'app\storage\DocumentsReader' => [ 'class' => 'app\storage\DocumentsReader', '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ 'class' => 'app\storage\DocumentsWriter', '__construct()' => [Instance::of('tempFileStorage')], ] ]); $reader = $container->get('app\storage\DocumentsReader'); ``` When to Register Dependencies ----------------------------- Because dependencies are needed when new objects are being created, their registration should be done as early as possible. The following are the recommended practices: * If you are the developer of an application, you can register your dependencies using application configuration. Please, read the [Application Configurations](concept-configurations.md#application-configurations) subsection of the [Configurations](concept-configurations.md) guide article. * If you are the developer of a redistributable [extension](structure-extensions.md), you can register dependencies in the bootstrapping class of the extension. Summary ------- Both dependency injection and [service locator](concept-service-locator.md) are popular design patterns that allow building software in a loosely-coupled and more testable fashion. We highly recommend you to read [Martin's article](https://martinfowler.com/articles/injection.html) to get a deeper understanding of dependency injection and service locator. Yii implements its [service locator](concept-service-locator.md) on top of the dependency injection (DI) container. When a service locator is trying to create a new object instance, it will forward the call to the DI container. The latter will resolve the dependencies automatically as described above. ================================================ FILE: docs/guide/concept-events.md ================================================ Events ====== Events allow you to inject custom code into existing code at certain execution points. You can attach custom code to an event so that when the event is triggered, the code gets executed automatically. For example, a mailer object may trigger a `messageSent` event when it successfully sends a message. If you want to keep track of the messages that are successfully sent, you could then simply attach the tracking code to the `messageSent` event. Yii introduces a base class called [[yii\base\Component]] to support events. If a class needs to trigger events, it should extend from [[yii\base\Component]], or from a child class. Event Handlers -------------- An event handler is a [PHP callback](https://www.php.net/manual/en/language.types.callable.php) that gets executed when the event it is attached to is triggered. You can use any of the following callbacks: - a global PHP function specified as a string (without parentheses), e.g., `'trim'`; - an object method specified as an array of an object and a method name as a string (without parentheses), e.g., `[$object, 'methodName']`; - a static class method specified as an array of a class name and a method name as a string (without parentheses), e.g., `['ClassName', 'methodName']`; - an anonymous function, e.g., `function ($event) { ... }`. The signature of an event handler is: ```php function ($event) { // $event is an object of yii\base\Event or a child class } ``` Through the `$event` parameter, an event handler may get the following information about the event that occurred: - [[yii\base\Event::name|event name]]; - [[yii\base\Event::sender|event sender]]: the object whose `trigger()` method was called; - [[yii\base\Event::data|custom data]]: the data that is provided when attaching the event handler (to be explained next). Attaching Event Handlers ------------------------ You can attach a handler to an event by calling the [[yii\base\Component::on()]] method. For example: ```php $foo = new Foo(); // this handler is a global function $foo->on(Foo::EVENT_HELLO, 'function_name'); // this handler is an object method $foo->on(Foo::EVENT_HELLO, [$object, 'methodName']); // this handler is a static class method $foo->on(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // this handler is an anonymous function $foo->on(Foo::EVENT_HELLO, function ($event) { // event handling logic }); ``` You may also attach event handlers through [configurations](concept-configurations.md). For more details, please refer to the [Configurations](concept-configurations.md#configuration-format) section. When attaching an event handler, you may provide additional data as the third parameter to [[yii\base\Component::on()]]. The data will be made available to the handler when the event is triggered and the handler is called. For example: ```php // The following code will display "abc" when the event is triggered // because $event->data contains the data passed as the 3rd argument to "on" $foo->on(Foo::EVENT_HELLO, 'function_name', 'abc'); function function_name($event) { echo $event->data; } ``` Event Handler Order ------------------- You may attach one or more handlers to a single event. When an event is triggered, the attached handlers will be called in the order that they were attached to the event. If a handler needs to stop the invocation of the handlers that follow it, it may set the [[yii\base\Event::handled]] property of the `$event` parameter to be `true`: ```php $foo->on(Foo::EVENT_HELLO, function ($event) { $event->handled = true; }); ``` By default, a newly attached handler is appended to the existing handler queue for the event. As a result, the handler will be called in the last place when the event is triggered. To insert the new handler at the start of the handler queue so that the handler gets called first, you may call [[yii\base\Component::on()]], passing `false` for the fourth parameter `$append`: ```php $foo->on(Foo::EVENT_HELLO, function ($event) { // ... }, $data, false); ``` Triggering Events ----------------- Events are triggered by calling the [[yii\base\Component::trigger()]] method. The method requires an *event name*, and optionally an event object that describes the parameters to be passed to the event handlers. For example: ```php namespace app\components; use yii\base\Component; use yii\base\Event; class Foo extends Component { const EVENT_HELLO = 'hello'; public function bar() { $this->trigger(self::EVENT_HELLO); } } ``` With the above code, any calls to `bar()` will trigger an event named `hello`. > Tip: It is recommended to use class constants to represent event names. In the above example, the constant `EVENT_HELLO` represents the `hello` event. This approach has three benefits. First, it prevents typos. Second, it can make events recognizable for IDE auto-completion support. Third, you can tell what events are supported in a class by simply checking its constant declarations. Sometimes when triggering an event you may want to pass along additional information to the event handlers. For example, a mailer may want to pass the message information to the handlers of the `messageSent` event so that the handlers can know the particulars of the sent messages. To do so, you can provide an event object as the second parameter to the [[yii\base\Component::trigger()]] method. The event object must be an instance of the [[yii\base\Event]] class or a child class. For example: ```php namespace app\components; use yii\base\Component; use yii\base\Event; class MessageEvent extends Event { public $message; } class Mailer extends Component { const EVENT_MESSAGE_SENT = 'messageSent'; public function send($message) { // ...sending $message... $event = new MessageEvent; $event->message = $message; $this->trigger(self::EVENT_MESSAGE_SENT, $event); } } ``` When the [[yii\base\Component::trigger()]] method is called, it will call all handlers attached to the named event. Detaching Event Handlers ------------------------ To detach a handler from an event, call the [[yii\base\Component::off()]] method. For example: ```php // the handler is a global function $foo->off(Foo::EVENT_HELLO, 'function_name'); // the handler is an object method $foo->off(Foo::EVENT_HELLO, [$object, 'methodName']); // the handler is a static class method $foo->off(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // the handler is an anonymous function $foo->off(Foo::EVENT_HELLO, $anonymousFunction); ``` Note that in general you should not try to detach an anonymous function unless you store it somewhere when it is attached to the event. In the above example, it is assumed that the anonymous function is stored as a variable `$anonymousFunction`. To detach *all* handlers from an event, simply call [[yii\base\Component::off()]] without the second parameter: ```php $foo->off(Foo::EVENT_HELLO); ``` Class-Level Event Handlers -------------------------- The above subsections described how to attach a handler to an event on an *instance level*. Sometimes, you may want to respond to an event triggered by *every* instance of a class instead of only by a specific instance. Instead of attaching an event handler to every instance, you may attach the handler on the *class level* by calling the static method [[yii\base\Event::on()]]. For example, an [Active Record](db-active-record.md) object will trigger an [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] event whenever it inserts a new record into the database. In order to track insertions done by *every* [Active Record](db-active-record.md) object, you may use the following code: ```php use Yii; use yii\base\Event; use yii\db\ActiveRecord; Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) { Yii::debug(get_class($event->sender) . ' is inserted'); }); ``` The event handler will be invoked whenever an instance of [[yii\db\ActiveRecord|ActiveRecord]], or one of its child classes, triggers the [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] event. In the handler, you can get the object that triggered the event through `$event->sender`. When an object triggers an event, it will first call instance-level handlers, followed by the class-level handlers. You may trigger a *class-level* event by calling the static method [[yii\base\Event::trigger()]]. A class-level event is not associated with a particular object. As a result, it will cause the invocation of class-level event handlers only. For example: ```php use yii\base\Event; Event::on(Foo::class, Foo::EVENT_HELLO, function ($event) { var_dump($event->sender); // displays "null" }); Event::trigger(Foo::class, Foo::EVENT_HELLO); ``` Note that, in this case, `$event->sender` is `null` instead of an object instance. > Note: Because a class-level handler will respond to an event triggered by any instance of that class, or any child classes, you should use it carefully, especially if the class is a low-level base class, such as [[yii\base\BaseObject]]. To detach a class-level event handler, call [[yii\base\Event::off()]]. For example: ```php // detach $handler Event::off(Foo::class, Foo::EVENT_HELLO, $handler); // detach all handlers of Foo::EVENT_HELLO Event::off(Foo::class, Foo::EVENT_HELLO); ``` Events using interfaces ------------- There is even more abstract way to deal with events. You can create a separated interface for the special event and implement it in classes, where you need it. For example, we can create the following interface: ```php namespace app\interfaces; interface DanceEventInterface { const EVENT_DANCE = 'dance'; } ``` And two classes, that implement it: ```php class Dog extends Component implements DanceEventInterface { public function meetBuddy() { echo "Woof!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } } class Developer extends Component implements DanceEventInterface { public function testsPassed() { echo "Yay!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } } ``` To handle the `EVENT_DANCE`, triggered by any of these classes, call [[yii\base\Event::on()|Event::on()]] and pass the interface class name as the first argument: ```php Event::on('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) { Yii::debug(get_class($event->sender) . ' just danced'); // Will log that Dog or Developer danced }); ``` You can trigger the event of those classes: ```php // trigger event for Dog class Event::trigger(Dog::class, DanceEventInterface::EVENT_DANCE); // trigger event for Developer class Event::trigger(Developer::class, DanceEventInterface::EVENT_DANCE); ``` But please notice, that you can not trigger all the classes, that implement the interface: ```php // DOES NOT WORK. Classes that implement this interface will NOT be triggered. Event::trigger('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE); ``` To detach event handler, call [[yii\base\Event::off()|Event::off()]]. For example: ```php // detaches $handler Event::off('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler); // detaches all handlers of DanceEventInterface::EVENT_DANCE Event::off('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE); ``` Global Events ------------- Yii supports a so-called *global event*, which is actually a trick based on the event mechanism described above. The global event requires a globally accessible Singleton, such as the [application](structure-applications.md) instance itself. To create the global event, an event sender calls the Singleton's `trigger()` method to trigger the event, instead of calling the sender's own `trigger()` method. Similarly, the event handlers are attached to the event on the Singleton. For example: ```php use Yii; use yii\base\Event; use app\components\Foo; Yii::$app->on('bar', function ($event) { echo get_class($event->sender); // displays "app\components\Foo" }); Yii::$app->trigger('bar', new Event(['sender' => new Foo])); ``` A benefit of using global events is that you do not need an object when attaching a handler to the event which will be triggered by the object. Instead, the handler attachment and the event triggering are both done through the Singleton (e.g. the application instance). However, because the namespace of the global events is shared by all parties, you should name the global events wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent"). Wildcard Events --------------- Since 2.0.14 you can setup event handler for multiple events matching wildcard pattern. For example: ```php use Yii; $foo = new Foo(); $foo->on('foo.event.*', function ($event) { // triggered for any event, which name starts on 'foo.event.' Yii::debug('trigger event: ' . $event->name); }); ``` Wildcard patterns can be used for class-level events as well. For example: ```php use yii\base\Event; use Yii; Event::on('app\models\*', 'before*', function ($event) { // triggered for any class in namespace 'app\models' for any event, which name starts on 'before' Yii::debug('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender)); }); ``` This allows you catching all application events by single handler using following code: ```php use yii\base\Event; use Yii; Event::on('*', '*', function ($event) { // triggered for any event at any class Yii::debug('trigger event: ' . $event->name); }); ``` > Note: usage wildcards for event handlers setup may reduce the application performance. It is better to be avoided if possible. In order to detach event handler specified by wildcard pattern, you should repeat same pattern at [[yii\base\Component::off()]] or [[yii\base\Event::off()]] invocation. Keep in mind that passing wildcard during detaching of event handler will detach only the handler specified for this wildcard, while handlers attached for regular event names will remain even if they match the pattern. For example: ```php use Yii; $foo = new Foo(); // attach regular handler $foo->on('event.hello', function ($event) { echo 'direct-handler' }); // attach wildcard handler $foo->on('*', function ($event) { echo 'wildcard-handler' }); // detach wildcard handler only! $foo->off('*'); $foo->trigger('event.hello'); // outputs: 'direct-handler' ``` ================================================ FILE: docs/guide/concept-properties.md ================================================ Properties ========== In PHP, class member variables are also called *properties*. These variables are part of the class definition, and are used to represent the state of a class instance (i.e., to differentiate one instance of the class from another). In practice, you may often want to handle the reading or writing of properties in special ways. For example, you may want to always trim a string when it is being assigned to a `label` property. You *could* use the following code to achieve this task: ```php $object->label = trim($label); ``` The drawback of the above code is that you would have to call `trim()` everywhere in your code where you might set the `label` property. If, in the future, the `label` property gets a new requirement, such as the first letter must be capitalized, you would again have to modify every bit of code that assigns a value to `label`. The repetition of code leads to bugs, and is a practice you want to avoid as much as possible. To solve this problem, Yii introduces a base class called [[yii\base\BaseObject]] that supports defining properties based on *getter* and *setter* class methods. If a class needs that functionality, it should extend from [[yii\base\BaseObject]], or from a child class. > Info: Nearly every core class in the Yii framework extends from [[yii\base\BaseObject]] or a child class. This means, that whenever you see a getter or setter in a core class, you can use it like a property. A getter method is a method whose name starts with the word `get`; a setter method starts with `set`. The name after the `get` or `set` prefix defines the name of a property. For example, a getter `getLabel()` and/or a setter `setLabel()` defines a property named `label`, as shown in the following code: ```php namespace app\components; use yii\base\BaseObject; class Foo extends BaseObject { private $_label; public function getLabel() { return $this->_label; } public function setLabel($value) { $this->_label = trim($value); } } ``` To be clear, the getter and setter methods create the property `label`, which in this case internally refers to a private property named `_label`. Properties defined by getters and setters can be used like class member variables. The main difference is that when such property is being read, the corresponding getter method will be called; when the property is being assigned a value, the corresponding setter method will be called. For example: ```php // equivalent to $label = $object->getLabel(); $label = $object->label; // equivalent to $object->setLabel('abc'); $object->label = 'abc'; ``` A property defined by a getter without a setter is *read only*. Trying to assign a value to such a property will cause an [[yii\base\InvalidCallException|InvalidCallException]]. Similarly, a property defined by a setter without a getter is *write only*, and trying to read such a property will also cause an exception. It is not common to have write-only properties. There are several special rules for, and limitations on, the properties defined via getters and setters: * The names of such properties are *case-insensitive*. For example, `$object->label` and `$object->Label` are the same. This is because method names in PHP are case-insensitive. * If the name of such a property is the same as a class member variable, the latter will take precedence. For example, if the above `Foo` class has a member variable `label`, then the assignment `$object->label = 'abc'` will affect the *member variable* `label`; that line would not call the `setLabel()` setter method. * These properties do not support visibility. It makes no difference to the defining getter or setter method if the property is public, protected or private. * The properties can only be defined by *non-static* getters and/or setters. Static methods will not be treated in the same manner. * A normal call to `property_exists()` does not work to determine magic properties. You should call [[yii\base\BaseObject::canGetProperty()|canGetProperty()]] or [[yii\base\BaseObject::canSetProperty()|canSetProperty()]] respectively. Returning to the problem described at the beginning of this guide, instead of calling `trim()` everywhere a `label` value is assigned, `trim()` now only needs to be invoked within the setter `setLabel()`. And if a new requirement makes it necessary that the label be initially capitalized, the `setLabel()` method can quickly be modified without touching any other code. The one change will universally affect every assignment to `label`. ================================================ FILE: docs/guide/concept-service-locator.md ================================================ Service Locator =============== A service locator is an object that knows how to provide all sorts of services (or components) that an application might need. Within a service locator, each component exists as only a single instance, uniquely identified by an ID. You use the ID to retrieve a component from the service locator. In Yii, a service locator is simply an instance of [[yii\di\ServiceLocator]] or a child class. The most commonly used service locator in Yii is the *application* object, which can be accessed through `\Yii::$app`. The services it provides are called *application components*, such as the `request`, `response`, and `urlManager` components. You may configure these components, or even replace them with your own implementations, easily through functionality provided by the service locator. Besides the application object, each module object is also a service locator. Modules implement [tree traversal](#tree-traversal). To use a service locator, the first step is to register components with it. A component can be registered via [[yii\di\ServiceLocator::set()]]. The following code shows different ways of registering components: ```php use yii\di\ServiceLocator; use yii\caching\FileCache; $locator = new ServiceLocator; // register "cache" using a class name that can be used to create a component $locator->set('cache', 'yii\caching\ApcCache'); // register "db" using a configuration array that can be used to create a component $locator->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=demo', 'username' => 'root', 'password' => '', ]); // register "search" using an anonymous function that builds a component $locator->set('search', function () { return new app\components\SolrService; }); // register "pageCache" using a component $locator->set('pageCache', new FileCache); ``` Once a component has been registered, you can access it using its ID, in one of the two following ways: ```php $cache = $locator->get('cache'); // or alternatively $cache = $locator->cache; ``` As shown above, [[yii\di\ServiceLocator]] allows you to access a component like a property using the component ID. When you access a component for the first time, [[yii\di\ServiceLocator]] will use the component registration information to create a new instance of the component and return it. Later, if the component is accessed again, the service locator will return the same instance. You may use [[yii\di\ServiceLocator::has()]] to check if a component ID has already been registered. If you call [[yii\di\ServiceLocator::get()]] with an invalid ID, an exception will be thrown. Because service locators are often being created with [configurations](concept-configurations.md), a writable property named [[yii\di\ServiceLocator::setComponents()|components]] is provided. This allows you to configure and register multiple components at once. The following code shows a configuration array that can be used to configure a service locator (e.g. an [application](structure-applications.md)) with the `db`, `cache`, `tz` and `search` components: ```php return [ // ... 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=demo', 'username' => 'root', 'password' => '', ], 'cache' => 'yii\caching\ApcCache', 'tz' => function() { return new \DateTimeZone(Yii::$app->formatter->defaultTimeZone); }, 'search' => function () { $solr = new app\components\SolrService('127.0.0.1'); // ... other initializations ... return $solr; }, ], ]; ``` In the above, there is an alternative way to configure the `search` component. Instead of directly writing a PHP callback which builds a `SolrService` instance, you can use a static class method to return such a callback, like shown as below: ```php class SolrServiceBuilder { public static function build($ip) { return function () use ($ip) { $solr = new app\components\SolrService($ip); // ... other initializations ... return $solr; }; } } return [ // ... 'components' => [ // ... 'search' => SolrServiceBuilder::build('127.0.0.1'), ], ]; ``` This alternative approach is most preferable when you are releasing a Yii component which encapsulates some non-Yii 3rd-party library. You use the static method like shown above to represent the complex logic of building the 3rd-party object, and the user of your component only needs to call the static method to configure the component. ## Tree traversal Modules allow arbitrary nesting; a Yii application is essentially a tree of modules. Since each of these modules is a service locator it makes sense for children to have access to their parent. This allows modules to use `$this->get('db')` instead of referencing the root service locator `Yii::$app->get('db')`. Added benefit is the option for a developer to override configuration in a module. Any request for a service to be retrieved from a module will be passed on to its parent in case the module is not able to satisfy it. Note that configuration from components in a module is never merged with configuration from a component in a parent module. The Service Locator pattern allows us to define named services but one cannot assume services with the same name use the same configuration parameters. ================================================ FILE: docs/guide/db-active-record.md ================================================ Active Record ============= [Active Record](https://en.wikipedia.org/wiki/Active_record_pattern) provides an object-oriented interface for accessing and manipulating data stored in databases. An Active Record class is associated with a database table, an Active Record instance corresponds to a row of that table, and an *attribute* of an Active Record instance represents the value of a particular column in that row. Instead of writing raw SQL statements, you would access Active Record attributes and call Active Record methods to access and manipulate the data stored in database tables. For example, assume `Customer` is an Active Record class which is associated with the `customer` table and `name` is a column of the `customer` table. You can write the following code to insert a new row into the `customer` table: ```php $customer = new Customer(); $customer->name = 'Qiang'; $customer->save(); ``` The above code is equivalent to using the following raw SQL statement for MySQL, which is less intuitive, more error prone, and may even have compatibility problems if you are using a different kind of database: ```php $db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [ ':name' => 'Qiang', ])->execute(); ``` Yii provides the Active Record support for the following relational databases: * MySQL 4.1 or later: via [[yii\db\ActiveRecord]] * PostgreSQL 7.3 or later: via [[yii\db\ActiveRecord]] * SQLite 2 and 3: via [[yii\db\ActiveRecord]] * Microsoft SQL Server 2008 or later: via [[yii\db\ActiveRecord]] * Oracle: via [[yii\db\ActiveRecord]] * CUBRID 9.3 or later: via [[yii\db\ActiveRecord]] (Note that due to a [bug](http://jira.cubrid.org/browse/APIS-658) in the cubrid PDO extension, quoting of values will not work, so you need CUBRID 9.3 as the client as well as the server) * Sphinx: via [[yii\sphinx\ActiveRecord]], requires the `yii2-sphinx` extension * ElasticSearch: via [[yii\elasticsearch\ActiveRecord]], requires the `yii2-elasticsearch` extension Additionally, Yii also supports using Active Record with the following NoSQL databases: * Redis 2.6.12 or later: via [[yii\redis\ActiveRecord]], requires the `yii2-redis` extension * MongoDB 1.3.0 or later: via [[yii\mongodb\ActiveRecord]], requires the `yii2-mongodb` extension In this tutorial, we will mainly describe the usage of Active Record for relational databases. However, most content described here are also applicable to Active Record for NoSQL databases. ## Declaring Active Record Classes To get started, declare an Active Record class by extending [[yii\db\ActiveRecord]]. ### Setting a table name By default each Active Record class is associated with its database table. The [[yii\db\ActiveRecord::tableName()|tableName()]] method returns the table name by converting the class name via [[yii\helpers\Inflector::camel2id()]]. You may override this method if the table is not named after this convention. Also a default [[yii\db\Connection::$tablePrefix|tablePrefix]] can be applied. For example if [[yii\db\Connection::$tablePrefix|tablePrefix]] is `tbl_`, `Customer` becomes `tbl_customer` and `OrderItem` becomes `tbl_order_item`. If a table name is given as `{{%TableName}}`, then the percentage character `%` will be replaced with the table prefix. For example, `{{%post}}` becomes `{{tbl_post}}`. The brackets around the table name are used for [quoting in an SQL query](db-dao.md#quoting-table-and-column-names). In the following example, we declare an Active Record class named `Customer` for the `customer` database table. ```php namespace app\models; use yii\db\ActiveRecord; class Customer extends ActiveRecord { const STATUS_INACTIVE = 0; const STATUS_ACTIVE = 1; /** * @return string the name of the table associated with this ActiveRecord class. */ public static function tableName() { return '{{customer}}'; } } ``` ### Active records are called "models" Active Record instances are considered as [models](structure-models.md). For this reason, we usually put Active Record classes under the `app\models` namespace (or other namespaces for keeping model classes). Because [[yii\db\ActiveRecord]] extends from [[yii\base\Model]], it inherits *all* [model](structure-models.md) features, such as attributes, validation rules, data serialization, etc. ## Connecting to Databases By default, Active Record uses the `db` [application component](structure-application-components.md) as the [[yii\db\Connection|DB connection]] to access and manipulate the database data. As explained in [Database Access Objects](db-dao.md), you can configure the `db` component in the application configuration like shown below, ```php return [ 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=testdb', 'username' => 'demo', 'password' => 'demo', ], ], ]; ``` If you want to use a different database connection other than the `db` component, you should override the [[yii\db\ActiveRecord::getDb()|getDb()]] method: ```php class Customer extends ActiveRecord { // ... public static function getDb() { // use the "db2" application component return \Yii::$app->db2; } } ``` ## Querying Data After declaring an Active Record class, you can use it to query data from the corresponding database table. The process usually takes the following three steps: 1. Create a new query object by calling the [[yii\db\ActiveRecord::find()]] method; 2. Build the query object by calling [query building methods](db-query-builder.md#building-queries); 3. Call a [query method](db-query-builder.md#query-methods) to retrieve data in terms of Active Record instances. As you can see, this is very similar to the procedure with [query builder](db-query-builder.md). The only difference is that instead of using the `new` operator to create a query object, you call [[yii\db\ActiveRecord::find()]] to return a new query object which is of class [[yii\db\ActiveQuery]]. Below are some examples showing how to use Active Query to query data: ```php // return a single customer whose ID is 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::find() ->where(['id' => 123]) ->one(); // return all active customers and order them by their IDs // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id` $customers = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->orderBy('id') ->all(); // return the number of active customers // SELECT COUNT(*) FROM `customer` WHERE `status` = 1 $count = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->count(); // return all customers in an array indexed by customer IDs // SELECT * FROM `customer` $customers = Customer::find() ->indexBy('id') ->all(); ``` In the above, `$customer` is a `Customer` object while `$customers` is an array of `Customer` objects. They are all populated with the data retrieved from the `customer` table. > Info: Because [[yii\db\ActiveQuery]] extends from [[yii\db\Query]], you can use *all* query building methods and query methods as described in the Section [Query Builder](db-query-builder.md). Because it is a common task to query by primary key values or a set of column values, Yii provides two shortcut methods for this purpose: - [[yii\db\ActiveRecord::findOne()]]: returns a single Active Record instance populated with the first row of the query result. - [[yii\db\ActiveRecord::findAll()]]: returns an array of Active Record instances populated with *all* query result. Both methods can take one of the following parameter formats: - a scalar value: the value is treated as the desired primary key value to be looked for. Yii will determine automatically which column is the primary key column by reading database schema information. - an array of scalar values: the array is treated as the desired primary key values to be looked for. - an associative array: the keys are column names and the values are the corresponding desired column values to be looked for. Please refer to [Hash Format](db-query-builder.md#hash-format) for more details. The following code shows how these methods can be used: ```php // returns a single customer whose ID is 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // returns customers whose ID is 100, 101, 123 or 124 // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124) $customers = Customer::findAll([100, 101, 123, 124]); // returns an active customer whose ID is 123 // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1 $customer = Customer::findOne([ 'id' => 123, 'status' => Customer::STATUS_ACTIVE, ]); // returns all inactive customers // SELECT * FROM `customer` WHERE `status` = 0 $customers = Customer::findAll([ 'status' => Customer::STATUS_INACTIVE, ]); ``` > Warning: If you need to pass user input to these methods, make sure the input value is scalar or in case of > array condition, make sure the array structure can not be changed from the outside: > > ```php > // yii\web\Controller ensures that $id is scalar > public function actionView($id) > { > $model = Post::findOne($id); > // ... > } > > // explicitly specifying the column to search, passing a scalar or array here will always result in finding a single record > $model = Post::findOne(['id' => Yii::$app->request->get('id')]); > > // do NOT use the following code! it is possible to inject an array condition to filter by arbitrary column values! > $model = Post::findOne(Yii::$app->request->get('id')); > ``` > Note: Neither [[yii\db\ActiveRecord::findOne()]] nor [[yii\db\ActiveQuery::one()]] will add `LIMIT 1` to the generated SQL statement. If your query may return many rows of data, you should call `limit(1)` explicitly to improve the performance, e.g., `Customer::find()->limit(1)->one()`. Besides using query building methods, you can also write raw SQLs to query data and populate the results into Active Record objects. You can do so by calling the [[yii\db\ActiveRecord::findBySql()]] method: ```php // returns all inactive customers $sql = 'SELECT * FROM customer WHERE status=:status'; $customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all(); ``` Do not call extra query building methods after calling [[yii\db\ActiveRecord::findBySql()|findBySql()]] as they will be ignored. ## Accessing Data As aforementioned, the data brought back from the database are populated into Active Record instances, and each row of the query result corresponds to a single Active Record instance. You can access the column values by accessing the attributes of the Active Record instances, for example, ```php // "id" and "email" are the names of columns in the "customer" table $customer = Customer::findOne(123); $id = $customer->id; $email = $customer->email; ``` > Note: The Active Record attributes are named after the associated table columns in a case-sensitive manner. Yii automatically defines an attribute in Active Record for every column of the associated table. You should NOT redeclare any of the attributes. Because Active Record attributes are named after table columns, you may find you are writing PHP code like `$customer->first_name`, which uses underscores to separate words in attribute names if your table columns are named in this way. If you are concerned about code style consistency, you should rename your table columns accordingly (to use camelCase, for example). ### Data Transformation It often happens that the data being entered and/or displayed are in a format which is different from the one used in storing the data in a database. For example, in the database you are storing customers' birthdays as UNIX timestamps (which is not a good design, though), while in most cases you would like to manipulate birthdays as strings in the format of `'YYYY/MM/DD'`. To achieve this goal, you can define *data transformation* methods in the `Customer` Active Record class like the following: ```php class Customer extends ActiveRecord { // ... public function getBirthdayText() { return date('Y/m/d', $this->birthday); } public function setBirthdayText($value) { $this->birthday = strtotime($value); } } ``` Now in your PHP code, instead of accessing `$customer->birthday`, you would access `$customer->birthdayText`, which will allow you to input and display customer birthdays in the format of `'YYYY/MM/DD'`. > Tip: The above example shows a generic way of transforming data in different formats. If you are working with > date values, you may use [DateValidator](tutorial-core-validators.md#date) and [[yii\jui\DatePicker|DatePicker]], > which is easier to use and more powerful. ### Retrieving Data in Arrays While retrieving data in terms of Active Record objects is convenient and flexible, it is not always desirable when you have to bring back a large amount of data due to the big memory footprint. In this case, you can retrieve data using PHP arrays by calling [[yii\db\ActiveQuery::asArray()|asArray()]] before executing a query method: ```php // return all customers // each customer is returned as an associative array $customers = Customer::find() ->asArray() ->all(); ``` > Note: While this method saves memory and improves performance, it is closer to the lower DB abstraction layer and you will lose most of the Active Record features. A very important distinction lies in the data type of the column values. When you return data in Active Record instances, column values will be automatically typecast according to the actual column types; on the other hand when you return data in arrays, column values will be strings (since they are the result of PDO without any processing), regardless their actual column types. ### Retrieving Data in Batches In [Query Builder](db-query-builder.md), we have explained that you may use *batch query* to minimize your memory usage when querying a large amount of data from the database. You may use the same technique in Active Record. For example, ```php // fetch 10 customers at a time foreach (Customer::find()->batch(10) as $customers) { // $customers is an array of 10 or fewer Customer objects } // fetch 10 customers at a time and iterate them one by one foreach (Customer::find()->each(10) as $customer) { // $customer is a Customer object } // batch query with eager loading foreach (Customer::find()->with('orders')->each() as $customer) { // $customer is a Customer object with the 'orders' relation populated } ``` ## Saving Data Using Active Record, you can easily save data to the database by taking the following steps: 1. Prepare an Active Record instance 2. Assign new values to Active Record attributes 3. Call [[yii\db\ActiveRecord::save()]] to save the data into database. For example, ```php // insert a new row of data $customer = new Customer(); $customer->name = 'James'; $customer->email = 'james@example.com'; $customer->save(); // update an existing row of data $customer = Customer::findOne(123); $customer->email = 'james@newexample.com'; $customer->save(); ``` The [[yii\db\ActiveRecord::save()|save()]] method can either insert or update a row of data, depending on the state of the Active Record instance. If the instance is newly created via the `new` operator, calling [[yii\db\ActiveRecord::save()|save()]] will cause insertion of a new row; If the instance is the result of a query method, calling [[yii\db\ActiveRecord::save()|save()]] will update the row associated with the instance. You can differentiate the two states of an Active Record instance by checking its [[yii\db\ActiveRecord::isNewRecord|isNewRecord]] property value. This property is also used by [[yii\db\ActiveRecord::save()|save()]] internally as follows: ```php public function save($runValidation = true, $attributeNames = null) { if ($this->getIsNewRecord()) { return $this->insert($runValidation, $attributeNames); } else { return $this->update($runValidation, $attributeNames) !== false; } } ``` > Tip: You can call [[yii\db\ActiveRecord::insert()|insert()]] or [[yii\db\ActiveRecord::update()|update()]] directly to insert or update a row. ### Data Validation Because [[yii\db\ActiveRecord]] extends from [[yii\base\Model]], it shares the same [data validation](input-validation.md) feature. You can declare validation rules by overriding the [[yii\db\ActiveRecord::rules()|rules()]] method and perform data validation by calling the [[yii\db\ActiveRecord::validate()|validate()]] method. When you call [[yii\db\ActiveRecord::save()|save()]], by default it will call [[yii\db\ActiveRecord::validate()|validate()]] automatically. Only when the validation passes, will it actually save the data; otherwise it will simply return `false`, and you can check the [[yii\db\ActiveRecord::errors|errors]] property to retrieve the validation error messages. > Tip: If you are certain that your data do not need validation (e.g., the data comes from trustable sources), you can call `save(false)` to skip the validation. ### Massive Assignment Like normal [models](structure-models.md), Active Record instances also enjoy the [massive assignment feature](structure-models.md#massive-assignment). Using this feature, you can assign values to multiple attributes of an Active Record instance in a single PHP statement, like shown below. Do remember that only [safe attributes](structure-models.md#safe-attributes) can be massively assigned, though. ```php $values = [ 'name' => 'James', 'email' => 'james@example.com', ]; $customer = new Customer(); $customer->attributes = $values; $customer->save(); ``` ### Updating Counters It is a common task to increment or decrement a column in a database table. We call these columns "counter columns". You can use [[yii\db\ActiveRecord::updateCounters()|updateCounters()]] to update one or multiple counter columns. For example, ```php $post = Post::findOne(100); // UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100 $post->updateCounters(['view_count' => 1]); ``` > Note: If you use [[yii\db\ActiveRecord::save()]] to update a counter column, you may end up with inaccurate result, because it is likely the same counter is being saved by multiple requests which read and write the same counter value. ### Dirty Attributes When you call [[yii\db\ActiveRecord::save()|save()]] to save an Active Record instance, only *dirty attributes* are being saved. An attribute is considered *dirty* if its value has been modified since it was loaded from DB or saved to DB most recently. Note that data validation will be performed regardless if the Active Record instance has dirty attributes or not. Active Record automatically maintains the list of dirty attributes. It does so by maintaining an older version of the attribute values and comparing them with the latest one. You can call [[yii\db\ActiveRecord::getDirtyAttributes()]] to get the attributes that are currently dirty. You can also call [[yii\db\ActiveRecord::markAttributeDirty()]] to explicitly mark an attribute as dirty. If you are interested in the attribute values prior to their most recent modification, you may call [[yii\db\ActiveRecord::getOldAttributes()|getOldAttributes()]] or [[yii\db\ActiveRecord::getOldAttribute()|getOldAttribute()]]. > Note: The comparison of old and new values will be done using the `===` operator so a value will be considered dirty > even if it has the same value but a different type. This is often the case when the model receives user input from > HTML forms where every value is represented as a string. > To ensure the correct type for e.g. integer values you may apply a [validation filter](input-validation.md#data-filtering): > `['attributeName', 'filter', 'filter' => 'intval']`. This works with all the typecasting functions of PHP like > [intval()](https://www.php.net/manual/en/function.intval.php), [floatval()](https://www.php.net/manual/en/function.floatval.php), > [boolval](https://www.php.net/manual/en/function.boolval.php), etc... ### Default Attribute Values Some of your table columns may have default values defined in the database. Sometimes, you may want to pre-populate your Web form for an Active Record instance with these default values. To avoid writing the same default values again, you can call [[yii\db\ActiveRecord::loadDefaultValues()|loadDefaultValues()]] to populate the DB-defined default values into the corresponding Active Record attributes: ```php $customer = new Customer(); $customer->loadDefaultValues(); // $customer->xyz will be assigned the default value declared when defining the "xyz" column ``` ### Attributes Typecasting Being populated by query results, [[yii\db\ActiveRecord]] performs automatic typecast for its attribute values, using information from [database table schema](db-dao.md#database-schema). This allows data retrieved from table column declared as integer to be populated in ActiveRecord instance with PHP integer, boolean with boolean and so on. However, typecasting mechanism has several limitations: * Float values are not be converted and will be represented as strings, otherwise they may loose precision. * Conversion of the integer values depends on the integer capacity of the operation system you use. In particular: values of column declared as 'unsigned integer' or 'big integer' will be converted to PHP integer only at 64-bit operation system, while on 32-bit ones - they will be represented as strings. Note that attribute typecast is performed only during populating ActiveRecord instance from query result. There is no automatic conversion for the values loaded from HTTP request or set directly via property access. The table schema will also be used while preparing SQL statements for the ActiveRecord data saving, ensuring values are bound to the query with correct type. However, ActiveRecord instance attribute values will not be converted during saving process. > Tip: you may use [[yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute values typecasting on ActiveRecord validation or saving. Since 2.0.14, Yii ActiveRecord supports complex data types, such as JSON or multidimensional arrays. #### JSON in MySQL and PostgreSQL After data population, the value from JSON column will be automatically decoded from JSON according to standard JSON decoding rules. To save attribute value to a JSON column, ActiveRecord will automatically create a [[yii\db\JsonExpression|JsonExpression]] object that will be encoded to a JSON string on [QueryBuilder](db-query-builder.md) level. #### Arrays in PostgreSQL After data population, the value from Array column will be automatically decoded from PgSQL notation to an [[yii\db\ArrayExpression|ArrayExpression]] object. It implements PHP `ArrayAccess` interface, so you can use it as an array, or call `->getValue()` to get the array itself. To save attribute value to an array column, ActiveRecord will automatically create an [[yii\db\ArrayExpression|ArrayExpression]] object that will be encoded by [QueryBuilder](db-query-builder.md) to an PgSQL string representation of array. You can also use conditions for JSON columns: ```php $query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])]) ``` To learn more about expressions building system read the [Query Builder – Adding custom Conditions and Expressions](db-query-builder.md#adding-custom-conditions-and-expressions) article. ### Updating Multiple Rows The methods described above all work on individual Active Record instances, causing inserting or updating of individual table rows. To update multiple rows simultaneously, you should call [[yii\db\ActiveRecord::updateAll()|updateAll()]], instead, which is a static method. ```php // UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%` Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']); ``` Similarly, you can call [[yii\db\ActiveRecord::updateAllCounters()|updateAllCounters()]] to update counter columns of multiple rows at the same time. ```php // UPDATE `customer` SET `age` = `age` + 1 Customer::updateAllCounters(['age' => 1]); ``` ## Deleting Data To delete a single row of data, first retrieve the Active Record instance corresponding to that row and then call the [[yii\db\ActiveRecord::delete()]] method. ```php $customer = Customer::findOne(123); $customer->delete(); ``` You can call [[yii\db\ActiveRecord::deleteAll()]] to delete multiple or all rows of data. For example, ```php Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]); ``` > Note: Be very careful when calling [[yii\db\ActiveRecord::deleteAll()|deleteAll()]] because it may totally erase all data from your table if you make a mistake in specifying the condition. ## Active Record Life Cycles It is important to understand the life cycles of Active Record when it is used for different purposes. During each life cycle, a certain sequence of methods will be invoked, and you can override these methods to get a chance to customize the life cycle. You can also respond to certain Active Record events triggered during a life cycle to inject your custom code. These events are especially useful when you are developing Active Record [behaviors](concept-behaviors.md) which need to customize Active Record life cycles. In the following, we will summarize the various Active Record life cycles and the methods/events that are involved in the life cycles. ### New Instance Life Cycle When creating a new Active Record instance via the `new` operator, the following life cycle will happen: 1. Class constructor. 2. [[yii\db\ActiveRecord::init()|init()]]: triggers an [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]] event. ### Querying Data Life Cycle When querying data through one of the [querying methods](#querying-data), each newly populated Active Record will undergo the following life cycle: 1. Class constructor. 2. [[yii\db\ActiveRecord::init()|init()]]: triggers an [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]] event. 3. [[yii\db\ActiveRecord::afterFind()|afterFind()]]: triggers an [[yii\db\ActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]] event. ### Saving Data Life Cycle When calling [[yii\db\ActiveRecord::save()|save()]] to insert or update an Active Record instance, the following life cycle will happen: 1. [[yii\db\ActiveRecord::beforeValidate()|beforeValidate()]]: triggers an [[yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]] event. If the method returns `false` or [[yii\base\ModelEvent::isValid]] is `false`, the rest of the steps will be skipped. 2. Performs data validation. If data validation fails, the steps after Step 3 will be skipped. 3. [[yii\db\ActiveRecord::afterValidate()|afterValidate()]]: triggers an [[yii\db\ActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]] event. 4. [[yii\db\ActiveRecord::beforeSave()|beforeSave()]]: triggers an [[yii\db\ActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]] or [[yii\db\ActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]] event. If the method returns `false` or [[yii\base\ModelEvent::isValid]] is `false`, the rest of the steps will be skipped. 5. Performs the actual data insertion or updating. 6. [[yii\db\ActiveRecord::afterSave()|afterSave()]]: triggers an [[yii\db\ActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] or [[yii\db\ActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]] event. ### Deleting Data Life Cycle When calling [[yii\db\ActiveRecord::delete()|delete()]] to delete an Active Record instance, the following life cycle will happen: 1. [[yii\db\ActiveRecord::beforeDelete()|beforeDelete()]]: triggers an [[yii\db\ActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]] event. If the method returns `false` or [[yii\base\ModelEvent::isValid]] is `false`, the rest of the steps will be skipped. 2. Performs the actual data deletion. 3. [[yii\db\ActiveRecord::afterDelete()|afterDelete()]]: triggers an [[yii\db\ActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]] event. > Note: Calling any of the following methods will NOT initiate any of the above life cycles because they work on the > database directly and not on a record basis: > > - [[yii\db\ActiveRecord::updateAll()]] > - [[yii\db\ActiveRecord::deleteAll()]] > - [[yii\db\ActiveRecord::updateCounters()]] > - [[yii\db\ActiveRecord::updateAllCounters()]] > Note: DI is not supported by default due to performance concerns. You can add support if needed by overriding > the [[yii\db\ActiveRecord::instantiate()|instantiate()]] method to instantiate the class via [[Yii::createObject()]]: > > ```php > public static function instantiate($row) > { > return Yii::createObject(static::class); > } > ``` ### Refreshing Data Life Cycle When calling [[yii\db\ActiveRecord::refresh()|refresh()]] to refresh an Active Record instance, the [[yii\db\ActiveRecord::EVENT_AFTER_REFRESH|EVENT_AFTER_REFRESH]] event is triggered if refresh is successful and the method returns `true`. ## Working with Transactions There are two ways of using [transactions](db-dao.md#performing-transactions) while working with Active Record. The first way is to explicitly enclose Active Record method calls in a transactional block, like shown below, ```php $customer = Customer::findOne(123); Customer::getDb()->transaction(function($db) use ($customer) { $customer->id = 200; $customer->save(); // ...other DB operations... }); // or alternatively $transaction = Customer::getDb()->beginTransaction(); try { $customer->id = 200; $customer->save(); // ...other DB operations... $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; } ``` > Note: in the above code we have two catch-blocks for compatibility > with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](https://www.php.net/manual/en/class.throwable.php) > since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher. The second way is to list the DB operations that require transactional support in the [[yii\db\ActiveRecord::transactions()]] method. For example, ```php class Customer extends ActiveRecord { public function transactions() { return [ 'admin' => self::OP_INSERT, 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, // the above is equivalent to the following: // 'api' => self::OP_ALL, ]; } } ``` The [[yii\db\ActiveRecord::transactions()]] method should return an array whose keys are [scenario](structure-models.md#scenarios) names and values are the corresponding operations that should be enclosed within transactions. You should use the following constants to refer to different DB operations: * [[yii\db\ActiveRecord::OP_INSERT|OP_INSERT]]: insertion operation performed by [[yii\db\ActiveRecord::insert()|insert()]]; * [[yii\db\ActiveRecord::OP_UPDATE|OP_UPDATE]]: update operation performed by [[yii\db\ActiveRecord::update()|update()]]; * [[yii\db\ActiveRecord::OP_DELETE|OP_DELETE]]: deletion operation performed by [[yii\db\ActiveRecord::delete()|delete()]]. Use the `|` operators to concatenate the above constants to indicate multiple operations. You may also use the shortcut constant [[yii\db\ActiveRecord::OP_ALL|OP_ALL]] to refer to all three operations above. Transactions that are created using this method will be started before calling [[yii\db\ActiveRecord::beforeSave()|beforeSave()]] and will be committed after [[yii\db\ActiveRecord::afterSave()|afterSave()]] has run. ## Optimistic Locks Optimistic locking is a way to prevent conflicts that may occur when a single row of data is being updated by multiple users. For example, both user A and user B are editing the same wiki article at the same time. After user A saves his edits, user B clicks on the "Save" button in an attempt to save his edits as well. Because user B was actually working on an outdated version of the article, it would be desirable to have a way to prevent him from saving the article and show him some hint message. Optimistic locking solves the above problem by using a column to record the version number of each row. When a row is being saved with an outdated version number, a [[yii\db\StaleObjectException]] exception will be thrown, which prevents the row from being saved. Optimistic locking is only supported when you update or delete an existing row of data using [[yii\db\ActiveRecord::update()]] or [[yii\db\ActiveRecord::delete()]], respectively. To use optimistic locking, 1. Create a column in the DB table associated with the Active Record class to store the version number of each row. The column should be of big integer type (in MySQL it would be `BIGINT DEFAULT 0`). 2. Override the [[yii\db\ActiveRecord::optimisticLock()]] method to return the name of this column. 3. Implement [[\yii\behaviors\OptimisticLockBehavior|OptimisticLockBehavior]] inside your model class to automatically parse its value from received requests. Remove the version attribute from validation rules as [[\yii\behaviors\OptimisticLockBehavior|OptimisticLockBehavior]] should handle it. 4. In the Web form that takes user inputs, add a hidden field to store the current version number of the row being updated. 5. In the controller action that updates the row using Active Record, try and catch the [[yii\db\StaleObjectException]] exception. Implement necessary business logic (e.g. merging the changes, prompting staled data) to resolve the conflict. For example, assume the version column is named as `version`. You can implement optimistic locking with the code like the following. ```php // ------ view code ------- use yii\helpers\Html; // ...other input fields echo Html::activeHiddenInput($model, 'version'); // ------ controller code ------- use yii\db\StaleObjectException; public function actionUpdate($id) { $model = $this->findModel($id); try { if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('update', [ 'model' => $model, ]); } } catch (StaleObjectException $e) { // logic to resolve the conflict } } // ------ model code ------- use yii\behaviors\OptimisticLockBehavior; public function behaviors() { return [ OptimisticLockBehavior::class, ]; } public function optimisticLock() { return 'version'; } ``` > Note: Because [[\yii\behaviors\OptimisticLockBehavior|OptimisticLockBehavior]] will ensure the record is only saved > if user submits a valid version number by directly parsing [[\yii\web\Request::getBodyParam()|getBodyParam()]], it > may be useful to extend your model class and do step 2 in parent model while attaching the behavior (step 3) to the child > class so you can have an instance dedicated to internal use while tying the other to controllers responsible of receiving > end user inputs. Alternatively, you can implement your own logic by configuring its [[\yii\behaviors\OptimisticLockBehavior::$value|value]] property. ## Working with Relational Data Besides working with individual database tables, Active Record is also capable of bringing together related data, making them readily accessible through the primary data. For example, the customer data is related with the order data because one customer may have placed one or multiple orders. With appropriate declaration of this relation, you'll be able to access a customer's order information using the expression `$customer->orders` which gives back the customer's order information in terms of an array of `Order` Active Record instances. ### Declaring Relations To work with relational data using Active Record, you first need to declare relations in Active Record classes. The task is as simple as declaring a *relation method* for every interested relation, like the following, ```php class Customer extends ActiveRecord { // ... public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id']); } } class Order extends ActiveRecord { // ... public function getCustomer() { return $this->hasOne(Customer::class, ['id' => 'customer_id']); } } ``` In the above code, we have declared an `orders` relation for the `Customer` class, and a `customer` relation for the `Order` class. Each relation method must be named as `getXyz`. We call `xyz` (the first letter is in lower case) the *relation name*. Note that relation names are *case sensitive*. While declaring a relation, you should specify the following information: - the multiplicity of the relation: specified by calling either [[yii\db\ActiveRecord::hasMany()|hasMany()]] or [[yii\db\ActiveRecord::hasOne()|hasOne()]]. In the above example you may easily read in the relation declarations that a customer has many orders while an order only has one customer. - the name of the related Active Record class: specified as the first parameter to either [[yii\db\ActiveRecord::hasMany()|hasMany()]] or [[yii\db\ActiveRecord::hasOne()|hasOne()]]. A recommended practice is to call `Xyz::class` to get the class name string so that you can receive IDE auto-completion support as well as error detection at compiling stage. - the link between the two types of data: specifies the column(s) through which the two types of data are related. The array values are the columns of the primary data (represented by the Active Record class that you are declaring relations), while the array keys are the columns of the related data. An easy rule to remember this is, as you see in the example above, you write the column that belongs to the related Active Record directly next to it. You see there that `customer_id` is a property of `Order` and `id` is a property of `Customer`. > Warning: Relation name `relation` is reserved. When used it will produce `ArgumentCountError`. ### Accessing Relational Data After declaring relations, you can access relational data through relation names. This is just like accessing an object [property](concept-properties.md) defined by the relation method. For this reason, we call it *relation property*. For example, ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 // $orders is an array of Order objects $orders = $customer->orders; ``` > Info: When you declare a relation named `xyz` via a getter method `getXyz()`, you will be able to access `xyz` like an [object property](concept-properties.md). Note that the name is case sensitive. If a relation is declared with [[yii\db\ActiveRecord::hasMany()|hasMany()]], accessing this relation property will return an array of the related Active Record instances; if a relation is declared with [[yii\db\ActiveRecord::hasOne()|hasOne()]], accessing the relation property will return the related Active Record instance or `null` if no related data is found. When you access a relation property for the first time, a SQL statement will be executed, like shown in the above example. If the same property is accessed again, the previous result will be returned without re-executing the SQL statement. To force re-executing the SQL statement, you should unset the relation property first: `unset($customer->orders)`. > Note: While this concept looks similar to the [object property](concept-properties.md) feature, there is an > important difference. For normal object properties the property value is of the same type as the defining getter method. > A relation method however returns an [[yii\db\ActiveQuery]] instance, while accessing a relation property will either > return a [[yii\db\ActiveRecord]] instance or an array of these. > > ```php > $customer->orders; // is an array of `Order` objects > $customer->getOrders(); // returns an ActiveQuery instance > ``` > > This is useful for creating customized queries, which is described in the next section. ### Dynamic Relational Query Because a relation method returns an instance of [[yii\db\ActiveQuery]], you can further build this query using query building methods before performing DB query. For example, ```php $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id` $orders = $customer->getOrders() ->where(['>', 'subtotal', 200]) ->orderBy('id') ->all(); ``` Unlike accessing a relation property, each time you perform a dynamic relational query via a relation method, a SQL statement will be executed, even if the same dynamic relational query was performed before. Sometimes you may even want to parametrize a relation declaration so that you can more easily perform dynamic relational query. For example, you may declare a `bigOrders` relation as follows, ```php class Customer extends ActiveRecord { public function getBigOrders($threshold = 100) { return $this->hasMany(Order::class, ['customer_id' => 'id']) ->where('subtotal > :threshold', [':threshold' => $threshold]) ->orderBy('id'); } } ``` Then you will be able to perform the following relational queries: ```php // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id` $orders = $customer->getBigOrders(200)->all(); // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id` $orders = $customer->bigOrders; ``` ### Relations via a Junction Table In database modelling, when the multiplicity between two related tables is many-to-many, a [junction table](https://en.wikipedia.org/wiki/Junction_table) is usually introduced. For example, the `order` table and the `item` table may be related via a junction table named `order_item`. One order will then correspond to multiple order items, while one product item will also correspond to multiple order items. When declaring such relations, you would call either [[yii\db\ActiveQuery::via()|via()]] or [[yii\db\ActiveQuery::viaTable()|viaTable()]] to specify the junction table. The difference between [[yii\db\ActiveQuery::via()|via()]] and [[yii\db\ActiveQuery::viaTable()|viaTable()]] is that the former specifies the junction table in terms of an existing relation name while the latter directly uses the junction table. For example, ```php class Order extends ActiveRecord { public function getItems() { return $this->hasMany(Item::class, ['id' => 'item_id']) ->viaTable('order_item', ['order_id' => 'id']); } } ``` or alternatively, ```php class Order extends ActiveRecord { public function getOrderItems() { return $this->hasMany(OrderItem::class, ['order_id' => 'id']); } public function getItems() { return $this->hasMany(Item::class, ['id' => 'item_id']) ->via('orderItems'); } } ``` The usage of relations declared with a junction table is the same as that of normal relations. For example, ```php // SELECT * FROM `order` WHERE `id` = 100 $order = Order::findOne(100); // SELECT * FROM `order_item` WHERE `order_id` = 100 // SELECT * FROM `item` WHERE `item_id` IN (...) // returns an array of Item objects $items = $order->items; ``` ### Chaining relation definitions via multiple tables Its further possible to define relations via multiple tables by chaining relation definitions using [[yii\db\ActiveQuery::via()|via()]]. Considering the examples above, we have classes `Customer`, `Order`, and `Item`. We can add a relation to the `Customer` class that lists all items from all the orders they placed, and name it `getPurchasedItems()`, the chaining of relations is show in the following code example: ```php class Customer extends ActiveRecord { // ... public function getPurchasedItems() { // customer's items, matching 'id' column of `Item` to 'item_id' in OrderItem return $this->hasMany(Item::class, ['id' => 'item_id']) ->via('orderItems'); } public function getOrderItems() { // customer's order items, matching 'id' column of `Order` to 'order_id' in OrderItem return $this->hasMany(OrderItem::class, ['order_id' => 'id']) ->via('orders'); } public function getOrders() { // same as above return $this->hasMany(Order::class, ['customer_id' => 'id']); } } ``` ### Lazy Loading and Eager Loading In [Accessing Relational Data](#accessing-relational-data), we explained that you can access a relation property of an Active Record instance like accessing a normal object property. A SQL statement will be executed only when you access the relation property the first time. We call such relational data accessing method *lazy loading*. For example, ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $orders = $customer->orders; // no SQL executed $orders2 = $customer->orders; ``` Lazy loading is very convenient to use. However, it may suffer from a performance issue when you need to access the same relation property of multiple Active Record instances. Consider the following code example. How many SQL statements will be executed? ```php // SELECT * FROM `customer` LIMIT 100 $customers = Customer::find()->limit(100)->all(); foreach ($customers as $customer) { // SELECT * FROM `order` WHERE `customer_id` = ... $orders = $customer->orders; } ``` As you can see from the code comment above, there are 101 SQL statements being executed! This is because each time you access the `orders` relation property of a different `Customer` object in the for-loop, a SQL statement will be executed. To solve this performance problem, you can use the so-called *eager loading* approach as shown below, ```php // SELECT * FROM `customer` LIMIT 100; // SELECT * FROM `orders` WHERE `customer_id` IN (...) $customers = Customer::find() ->with('orders') ->limit(100) ->all(); foreach ($customers as $customer) { // no SQL executed $orders = $customer->orders; } ``` By calling [[yii\db\ActiveQuery::with()]], you instruct Active Record to bring back the orders for the first 100 customers in one single SQL statement. As a result, you reduce the number of the executed SQL statements from 101 to 2! You can eagerly load one or multiple relations. You can even eagerly load *nested relations*. A nested relation is a relation that is declared within a related Active Record class. For example, `Customer` is related with `Order` through the `orders` relation, and `Order` is related with `Item` through the `items` relation. When querying for `Customer`, you can eagerly load `items` using the nested relation notation `orders.items`. The following code shows different usage of [[yii\db\ActiveQuery::with()|with()]]. We assume the `Customer` class has two relations `orders` and `country`, while the `Order` class has one relation `items`. ```php // eager loading both "orders" and "country" $customers = Customer::find()->with('orders', 'country')->all(); // equivalent to the array syntax below $customers = Customer::find()->with(['orders', 'country'])->all(); // no SQL executed $orders= $customers[0]->orders; // no SQL executed $country = $customers[0]->country; // eager loading "orders" and the nested relation "orders.items" $customers = Customer::find()->with('orders.items')->all(); // access the items of the first order of the first customer // no SQL executed $items = $customers[0]->orders[0]->items; ``` You can eagerly load deeply nested relations, such as `a.b.c.d`. All parent relations will be eagerly loaded. That is, when you call [[yii\db\ActiveQuery::with()|with()]] using `a.b.c.d`, you will eagerly load `a`, `a.b`, `a.b.c` and `a.b.c.d`. > Info: In general, when eagerly loading `N` relations among which `M` relations are defined with a [junction table](#junction-table), a total number of `N+M+1` SQL statements will be executed. Note that a nested relation `a.b.c.d` counts as 4 relations. When eagerly loading a relation, you can customize the corresponding relational query using an anonymous function. For example, ```php // find customers and bring back together their country and active orders // SELECT * FROM `customer` // SELECT * FROM `country` WHERE `id` IN (...) // SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1 $customers = Customer::find()->with([ 'country', 'orders' => function ($query) { $query->andWhere(['status' => Order::STATUS_ACTIVE]); }, ])->all(); ``` When customizing the relational query for a relation, you should specify the relation name as an array key and use an anonymous function as the corresponding array value. The anonymous function will receive a `$query` parameter which represents the [[yii\db\ActiveQuery]] object used to perform the relational query for the relation. In the code example above, we are modifying the relational query by appending an additional condition about order status. > Note: If you call [[yii\db\Query::select()|select()]] while eagerly loading relations, you have to make sure > the columns referenced in the relation declarations are being selected. Otherwise, the related models may not > be loaded properly. For example, > > ```php > $orders = Order::find()->select(['id', 'amount'])->with('customer')->all(); > // $orders[0]->customer is always `null`. To fix the problem, you should do the following: > $orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all(); > ``` ### Joining with Relations > Note: The content described in this subsection is only applicable to relational databases, such as MySQL, PostgreSQL, etc. The relational queries that we have described so far only reference the primary table columns when querying for the primary data. In reality we often need to reference columns in the related tables. For example, we may want to bring back the customers who have at least one active order. To solve this problem, we can build a join query like the following: ```php // SELECT `customer`.* FROM `customer` // LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` // WHERE `order`.`status` = 1 // // SELECT * FROM `order` WHERE `customer_id` IN (...) $customers = Customer::find() ->select('customer.*') ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`') ->where(['order.status' => Order::STATUS_ACTIVE]) ->with('orders') ->all(); ``` > Note: It is important to disambiguate column names when building relational queries involving JOIN SQL statements. A common practice is to prefix column names with their corresponding table names. However, a better approach is to exploit the existing relation declarations by calling [[yii\db\ActiveQuery::joinWith()]]: ```php $customers = Customer::find() ->joinWith('orders') ->where(['order.status' => Order::STATUS_ACTIVE]) ->all(); ``` Both approaches execute the same set of SQL statements. The latter approach is much cleaner and drier, though. By default, [[yii\db\ActiveQuery::joinWith()|joinWith()]] will use `LEFT JOIN` to join the primary table with the related table. You can specify a different join type (e.g. `RIGHT JOIN`) via its third parameter `$joinType`. If the join type you want is `INNER JOIN`, you can simply call [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]], instead. Calling [[yii\db\ActiveQuery::joinWith()|joinWith()]] will [eagerly load](#lazy-eager-loading) the related data by default. If you do not want to bring in the related data, you can specify its second parameter `$eagerLoading` as `false`. > Note: Even when using [[yii\db\ActiveQuery::joinWith()|joinWith()]] or [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]] with eager loading enabled the related data will **not** be populated from the result of the `JOIN` query. So there's still an extra query for each joined relation as explained in the section on [eager loading](#lazy-eager-loading). Like [[yii\db\ActiveQuery::with()|with()]], you can join with one or multiple relations; you may customize the relation queries on-the-fly; you may join with nested relations; and you may mix the use of [[yii\db\ActiveQuery::with()|with()]] and [[yii\db\ActiveQuery::joinWith()|joinWith()]]. For example, ```php $customers = Customer::find()->joinWith([ 'orders' => function ($query) { $query->andWhere(['>', 'subtotal', 100]); }, ])->with('country') ->all(); ``` Sometimes when joining two tables, you may need to specify some extra conditions in the `ON` part of the JOIN query. This can be done by calling the [[yii\db\ActiveQuery::onCondition()]] method like the following: ```php // SELECT `customer`.* FROM `customer` // LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 // // SELECT * FROM `order` WHERE `customer_id` IN (...) $customers = Customer::find()->joinWith([ 'orders' => function ($query) { $query->onCondition(['order.status' => Order::STATUS_ACTIVE]); }, ])->all(); ``` This above query brings back *all* customers, and for each customer it brings back all active orders. Note that this differs from our earlier example which only brings back customers who have at least one active order. > Info: When [[yii\db\ActiveQuery]] is specified with a condition via [[yii\db\ActiveQuery::onCondition()|onCondition()]], the condition will be put in the `ON` part if the query involves a JOIN query. If the query does not involve JOIN, the on-condition will be automatically appended to the `WHERE` part of the query. Thus it may only contain conditions including columns of the related table. #### Relation table aliases As noted before, when using JOIN in a query, we need to disambiguate column names. Therefore often an alias is defined for a table. Setting an alias for the relational query would be possible by customizing the relation query in the following way: ```php $query->joinWith([ 'orders' => function ($q) { $q->from(['o' => Order::tableName()]); }, ]) ``` This however looks very complicated and involves either hardcoding the related objects table name or calling `Order::tableName()`. Since version 2.0.7, Yii provides a shortcut for this. You may now define and use the alias for the relation table like the following: ```php // join the orders relation and sort the result by orders.id $query->joinWith(['orders o'])->orderBy('o.id'); ``` The above syntax works for simple relations. If you need an alias for an intermediate table when joining over nested relations, e.g. `$query->joinWith(['orders.product'])`, you need to nest the joinWith calls like in the following example: ```php $query->joinWith(['orders o' => function($q) { $q->joinWith('product p'); }]) ->where('o.amount > 100'); ``` ### Inverse Relations Relation declarations are often reciprocal between two Active Record classes. For example, `Customer` is related to `Order` via the `orders` relation, and `Order` is related back to `Customer` via the `customer` relation. ```php class Customer extends ActiveRecord { public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id']); } } class Order extends ActiveRecord { public function getCustomer() { return $this->hasOne(Customer::class, ['id' => 'customer_id']); } } ``` Now consider the following piece of code: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $order = $customer->orders[0]; // SELECT * FROM `customer` WHERE `id` = 123 $customer2 = $order->customer; // displays "not the same" echo $customer2 === $customer ? 'same' : 'not the same'; ``` We would think `$customer` and `$customer2` are the same, but they are not! Actually they do contain the same customer data, but they are different objects. When accessing `$order->customer`, an extra SQL statement is executed to populate a new object `$customer2`. To avoid the redundant execution of the last SQL statement in the above example, we should tell Yii that `customer` is an *inverse relation* of `orders` by calling the [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] method like shown below: ```php class Customer extends ActiveRecord { public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer'); } } ``` With this modified relation declaration, we will have: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $order = $customer->orders[0]; // No SQL will be executed $customer2 = $order->customer; // displays "same" echo $customer2 === $customer ? 'same' : 'not the same'; ``` > Note: Inverse relations cannot be defined for relations involving a [junction table](#junction-table). That is, if a relation is defined with [[yii\db\ActiveQuery::via()|via()]] or [[yii\db\ActiveQuery::viaTable()|viaTable()]], you should not call [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] further. ## Saving Relations When working with relational data, you often need to establish relationships between different data or destroy existing relationships. This requires setting proper values for the columns that define the relations. Using Active Record, you may end up writing the code like the following: ```php $customer = Customer::findOne(123); $order = new Order(); $order->subtotal = 100; // ... // setting the attribute that defines the "customer" relation in Order $order->customer_id = $customer->id; $order->save(); ``` Active Record provides the [[yii\db\ActiveRecord::link()|link()]] method that allows you to accomplish this task more nicely: ```php $customer = Customer::findOne(123); $order = new Order(); $order->subtotal = 100; // ... $order->link('customer', $customer); ``` The [[yii\db\ActiveRecord::link()|link()]] method requires you to specify the relation name and the target Active Record instance that the relationship should be established with. The method will modify the values of the attributes that link two Active Record instances and save them to the database. In the above example, it will set the `customer_id` attribute of the `Order` instance to be the value of the `id` attribute of the `Customer` instance and then save it to the database. > Note: You cannot link two newly created Active Record instances. The benefit of using [[yii\db\ActiveRecord::link()|link()]] is even more obvious when a relation is defined via a [junction table](#junction-table). For example, you may use the following code to link an `Order` instance with an `Item` instance: ```php $order->link('items', $item); ``` The above code will automatically insert a row in the `order_item` junction table to relate the order with the item. > Info: The [[yii\db\ActiveRecord::link()|link()]] method will NOT perform any data validation while saving the affected Active Record instance. It is your responsibility to validate any input data before calling this method. The opposite operation to [[yii\db\ActiveRecord::link()|link()]] is [[yii\db\ActiveRecord::unlink()|unlink()]] which breaks an existing relationship between two Active Record instances. For example, ```php $customer = Customer::find()->with('orders')->where(['id' => 123])->one(); $customer->unlink('orders', $customer->orders[0]); ``` By default, the [[yii\db\ActiveRecord::unlink()|unlink()]] method will set the foreign key value(s) that specify the existing relationship to be `null`. You may, however, choose to delete the table row that contains the foreign key value by passing the `$delete` parameter as `true` to the method. When a junction table is involved in a relation, calling [[yii\db\ActiveRecord::unlink()|unlink()]] will cause the foreign keys in the junction table to be cleared, or the deletion of the corresponding row in the junction table if `$delete` is `true`. ## Cross-Database Relations Active Record allows you to declare relations between Active Record classes that are powered by different databases. The databases can be of different types (e.g. MySQL and PostgreSQL, or MS SQL and MongoDB), and they can run on different servers. You can use the same syntax to perform relational queries. For example, ```php // Customer is associated with the "customer" table in a relational database (e.g. MySQL) class Customer extends \yii\db\ActiveRecord { public static function tableName() { return 'customer'; } public function getComments() { // a customer has many comments return $this->hasMany(Comment::class, ['customer_id' => 'id']); } } // Comment is associated with the "comment" collection in a MongoDB database class Comment extends \yii\mongodb\ActiveRecord { public static function collectionName() { return 'comment'; } public function getCustomer() { // a comment has one customer return $this->hasOne(Customer::class, ['id' => 'customer_id']); } } $customers = Customer::find()->with('comments')->all(); ``` You can use most of the relational query features that have been described in this section. > Note: Usage of [[yii\db\ActiveQuery::joinWith()|joinWith()]] is limited to databases that allow cross-database JOIN queries. For this reason, you cannot use this method in the above example because MongoDB does not support JOIN. ## Customizing Query Classes By default, all Active Record queries are supported by [[yii\db\ActiveQuery]]. To use a customized query class in an Active Record class, you should override the [[yii\db\ActiveRecord::find()]] method and return an instance of your customized query class. For example, ```php // file Comment.php namespace app\models; use yii\db\ActiveRecord; class Comment extends ActiveRecord { public static function find() { return new CommentQuery(get_called_class()); } } ``` Now whenever you are performing a query (e.g. `find()`, `findOne()`) or defining a relation (e.g. `hasOne()`) with `Comment`, you will be calling an instance of `CommentQuery` instead of `ActiveQuery`. You now have to define the `CommentQuery` class, which can be customized in many creative ways to improve your query building experience. For example, ```php // file CommentQuery.php namespace app\models; use yii\db\ActiveQuery; class CommentQuery extends ActiveQuery { // conditions appended by default (can be skipped) public function init() { $this->andOnCondition(['deleted' => false]); parent::init(); } // ... add customized query methods here ... public function active($state = true) { return $this->andOnCondition(['active' => $state]); } } ``` > Note: Instead of calling [[yii\db\ActiveQuery::onCondition()|onCondition()]], you usually should call [[yii\db\ActiveQuery::andOnCondition()|andOnCondition()]] or [[yii\db\ActiveQuery::orOnCondition()|orOnCondition()]] to append additional conditions when defining new query building methods so that any existing conditions are not overwritten. This allows you to write query building code like the following: ```php $comments = Comment::find()->active()->all(); $inactiveComments = Comment::find()->active(false)->all(); ``` > Tip: In big projects, it is recommended that you use customized query classes to hold most query-related code so that the Active Record classes can be kept clean. You can also use the new query building methods when defining relations about `Comment` or performing relational query: ```php class Customer extends \yii\db\ActiveRecord { public function getActiveComments() { return $this->hasMany(Comment::class, ['customer_id' => 'id'])->active(); } } $customers = Customer::find()->joinWith('activeComments')->all(); // or alternatively class Customer extends \yii\db\ActiveRecord { public function getComments() { return $this->hasMany(Comment::class, ['customer_id' => 'id']); } } $customers = Customer::find()->joinWith([ 'comments' => function($q) { $q->active(); } ])->all(); ``` > Info: In Yii 1.1, there is a concept called *scope*. Scope is no longer directly supported in Yii 2.0, and you should use customized query classes and query methods to achieve the same goal. ## Selecting extra fields When Active Record instance is populated from query results, its attributes are filled up by corresponding column values from received data set. You are able to fetch additional columns or values from query and store it inside the Active Record. For example, assume we have a table named `room`, which contains information about rooms available in the hotel. Each room stores information about its geometrical size using fields `length`, `width`, `height`. Imagine we need to retrieve list of all available rooms with their volume in descending order. So you can not calculate volume using PHP, because we need to sort the records by its value, but you also want `volume` to be displayed in the list. To achieve the goal, you need to declare an extra field in your `Room` Active Record class, which will store `volume` value: ```php class Room extends \yii\db\ActiveRecord { public $volume; // ... } ``` Then you need to compose a query, which calculates volume of the room and performs the sort: ```php $rooms = Room::find() ->select([ '{{room}}.*', // select all columns '([[length]] * [[width]] * [[height]]) AS volume', // calculate a volume ]) ->orderBy('volume DESC') // apply sort ->all(); foreach ($rooms as $room) { echo $room->volume; // contains value calculated by SQL } ``` Ability to select extra fields can be exceptionally useful for aggregation queries. Assume you need to display a list of customers with the count of orders they have made. First of all, you need to declare a `Customer` class with `orders` relation and extra field for count storage: ```php class Customer extends \yii\db\ActiveRecord { public $ordersCount; // ... public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id']); } } ``` Then you can compose a query, which joins the orders and calculates their count: ```php $customers = Customer::find() ->select([ '{{customer}}.*', // select all customer fields 'COUNT({{order}}.id) AS ordersCount' // calculate orders count ]) ->joinWith('orders') // ensure table junction ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works ->all(); ``` A disadvantage of using this method would be that, if the information isn't loaded on the SQL query - it has to be calculated separately. Thus, if you have found particular record via regular query without extra select statements, it will be unable to return actual value for the extra field. Same will happen for the newly saved record. ```php $room = new Room(); $room->length = 100; $room->width = 50; $room->height = 2; $room->volume; // this value will be `null`, since it was not declared yet ``` Using the [[yii\db\BaseActiveRecord::__get()|__get()]] and [[yii\db\BaseActiveRecord::__set()|__set()]] magic methods we can emulate the behavior of a property: ```php class Room extends \yii\db\ActiveRecord { private $_volume; public function setVolume($volume) { $this->_volume = (float) $volume; } public function getVolume() { if (empty($this->length) || empty($this->width) || empty($this->height)) { return null; } if ($this->_volume === null) { $this->setVolume( $this->length * $this->width * $this->height ); } return $this->_volume; } // ... } ``` When the select query doesn't provide the volume, the model will be able to calculate it automatically using the attributes of the model. You can calculate the aggregation fields as well using defined relations: ```php class Customer extends \yii\db\ActiveRecord { private $_ordersCount; public function setOrdersCount($count) { $this->_ordersCount = (int) $count; } public function getOrdersCount() { if ($this->isNewRecord) { return null; // this avoid calling a query searching for null primary keys } if ($this->_ordersCount === null) { $this->setOrdersCount($this->getOrders()->count()); // calculate aggregation on demand from relation } return $this->_ordersCount; } // ... public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id']); } } ``` With this code, in case 'ordersCount' is present in 'select' statement - `Customer::ordersCount` will be populated by query results, otherwise it will be calculated on demand using `Customer::orders` relation. This approach can be as well used for creation of the shortcuts for some relational data, especially for the aggregation. For example: ```php class Customer extends \yii\db\ActiveRecord { /** * Defines read-only virtual property for aggregation data. */ public function getOrdersCount() { if ($this->isNewRecord) { return null; // this avoid calling a query searching for null primary keys } return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted']; } /** * Declares normal 'orders' relation. */ public function getOrders() { return $this->hasMany(Order::class, ['customer_id' => 'id']); } /** * Declares new relation based on 'orders', which provides aggregation. */ public function getOrdersAggregation() { return $this->getOrders() ->select(['customer_id', 'counted' => 'count(*)']) ->groupBy('customer_id') ->asArray(true); } // ... } foreach (Customer::find()->with('ordersAggregation')->all() as $customer) { echo $customer->ordersCount; // outputs aggregation data from relation without extra query due to eager loading } $customer = Customer::findOne($pk); $customer->ordersCount; // output aggregation data from lazy loaded relation ``` ================================================ FILE: docs/guide/db-dao.md ================================================ Database Access Objects ======================= Built on top of [PDO](https://www.php.net/manual/en/book.pdo.php), Yii DAO (Database Access Objects) provides an object-oriented API for accessing relational databases. It is the foundation for other more advanced database access methods, including [query builder](db-query-builder.md) and [active record](db-active-record.md). When using Yii DAO, you mainly need to deal with plain SQLs and PHP arrays. As a result, it is the most efficient way to access databases. However, because SQL syntax may vary for different databases, using Yii DAO also means you have to take extra effort to create a database-agnostic application. In Yii 2.0, DAO supports the following databases out of the box: - [MySQL](https://www.mysql.com/) - [MariaDB](https://mariadb.com/) - [SQLite](https://sqlite.org/) - [PostgreSQL](https://www.postgresql.org/): version 8.4 or higher - [CUBRID](https://www.cubrid.org/): version 9.3 or higher. - [Oracle](https://www.oracle.com/database/) - [MSSQL](https://www.microsoft.com/en-us/sqlserver/default.aspx): version 2008 or higher. > Note: New version of pdo_oci for PHP 7 currently exists only as the source code. Follow [instruction provided by community](https://github.com/yiisoft/yii2/issues/10975#issuecomment-248479268) to compile it or use [PDO emulation layer](https://github.com/taq/pdooci). ## Creating DB Connections To access a database, you first need to connect to it by creating an instance of [[yii\db\Connection]]: ```php $db = new yii\db\Connection([ 'dsn' => 'mysql:host=localhost;dbname=example', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); ``` Because a DB connection often needs to be accessed in different places, a common practice is to configure it in terms of an [application component](structure-application-components.md) like the following: ```php return [ // ... 'components' => [ // ... 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=example', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], ], // ... ]; ``` You can then access the DB connection via the expression `Yii::$app->db`. > Tip: You can configure multiple DB application components if your application needs to access multiple databases. When configuring a DB connection, you should always specify its Data Source Name (DSN) via the [[yii\db\Connection::dsn|dsn]] property. The format of DSN varies for different databases. Please refer to the [PHP manual](https://www.php.net/manual/en/pdo.construct.php) for more details. Below are some examples: * MySQL, MariaDB: `mysql:host=localhost;dbname=mydatabase` * SQLite: `sqlite:/path/to/database/file` * PostgreSQL: `pgsql:host=localhost;port=5432;dbname=mydatabase` * CUBRID: `cubrid:dbname=demodb;host=localhost;port=33000` * MS SQL Server (via sqlsrv driver): `sqlsrv:Server=localhost;Database=mydatabase` * MS SQL Server (via dblib driver): `dblib:host=localhost;dbname=mydatabase` * MS SQL Server (via mssql driver): `mssql:host=localhost;dbname=mydatabase` * Oracle: `oci:dbname=//localhost:1521/mydatabase` Note that if you are connecting with a database via ODBC, you should configure the [[yii\db\Connection::driverName]] property so that Yii can know the actual database type. For example, ```php 'db' => [ 'class' => 'yii\db\Connection', 'driverName' => 'mysql', 'dsn' => 'odbc:Driver={MySQL};Server=localhost;Database=test', 'username' => 'root', 'password' => '', ], ``` Besides the [[yii\db\Connection::dsn|dsn]] property, you often need to configure [[yii\db\Connection::username|username]] and [[yii\db\Connection::password|password]]. Please refer to [[yii\db\Connection]] for the full list of configurable properties. > Info: When you create a DB connection instance, the actual connection to the database is not established until you execute the first SQL or you call the [[yii\db\Connection::open()|open()]] method explicitly. > Tip: Sometimes you may want to execute some queries right after the database connection is established to initialize > some environment variables (e.g., to set the timezone or character set). You can do so by registering an event handler > for the [[yii\db\Connection::EVENT_AFTER_OPEN|afterOpen]] event > of the database connection. You may register the handler directly in the application configuration like so: > > ```php > 'db' => [ > // ... > 'on afterOpen' => function($event) { > // $event->sender refers to the DB connection > $event->sender->createCommand("SET time_zone = 'UTC'")->execute(); > } > ], > ``` For MS SQL Server additional connection option is needed for proper binary data handling: ```php 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', 'attributes' => [ \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM ] ], ``` ## Executing SQL Queries Once you have a database connection instance, you can execute a SQL query by taking the following steps: 1. Create a [[yii\db\Command]] with a plain SQL query; 2. Bind parameters (optional); 3. Call one of the SQL execution methods in [[yii\db\Command]]. The following example shows various ways of fetching data from a database: ```php // return a set of rows. each row is an associative array of column names and values. // an empty array is returned if the query returned no results $posts = Yii::$app->db->createCommand('SELECT * FROM post') ->queryAll(); // return a single row (the first row) // false is returned if the query has no result $post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=1') ->queryOne(); // return a single column (the first column) // an empty array is returned if the query returned no results $titles = Yii::$app->db->createCommand('SELECT title FROM post') ->queryColumn(); // return a scalar value // false is returned if the query has no result $count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM post') ->queryScalar(); ``` > Note: To preserve precision, the data fetched from databases are all represented as strings, even if the corresponding database column types are numerical. ### Binding Parameters When creating a DB command from a SQL with parameters, you should almost always use the approach of binding parameters to prevent SQL injection attacks. For example, ```php $post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status') ->bindValue(':id', $_GET['id']) ->bindValue(':status', 1) ->queryOne(); ``` In the SQL statement, you can embed one or multiple parameter placeholders (e.g. `:id` in the above example). A parameter placeholder should be a string starting with a colon. You may then call one of the following parameter binding methods to bind the parameter values: * [[yii\db\Command::bindValue()|bindValue()]]: bind a single parameter value * [[yii\db\Command::bindValues()|bindValues()]]: bind multiple parameter values in one call * [[yii\db\Command::bindParam()|bindParam()]]: similar to [[yii\db\Command::bindValue()|bindValue()]] but also support binding parameter references. The following example shows alternative ways of binding parameters: ```php $params = [':id' => $_GET['id'], ':status' => 1]; $post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status') ->bindValues($params) ->queryOne(); $post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status', $params) ->queryOne(); ``` Parameter binding is implemented via [prepared statements](https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php). Besides preventing SQL injection attacks, it may also improve performance by preparing a SQL statement once and executing it multiple times with different parameters. For example, ```php $command = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id'); $post1 = $command->bindValue(':id', 1)->queryOne(); $post2 = $command->bindValue(':id', 2)->queryOne(); // ... ``` Because [[yii\db\Command::bindParam()|bindParam()]] supports binding parameters by references, the above code can also be written like the following: ```php $command = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id') ->bindParam(':id', $id); $id = 1; $post1 = $command->queryOne(); $id = 2; $post2 = $command->queryOne(); // ... ``` Notice that you bind the placeholder to the `$id` variable before the execution, and then change the value of that variable before each subsequent execution (this is often done with loops). Executing queries in this manner can be vastly more efficient than running a new query for every different parameter value. > Info: Parameter binding is only used in places where values need to be inserted into strings that contain plain SQL. > In many places in higher abstraction layers like [query builder](db-query-builder.md) and [active record](db-active-record.md) > you often specify an array of values which will be transformed into SQL. In these places parameter binding is done by Yii > internally, so there is no need to specify params manually. ### Executing Non-SELECT Queries The `queryXyz()` methods introduced in the previous sections all deal with SELECT queries which fetch data from databases. For queries that do not bring back data, you should call the [[yii\db\Command::execute()]] method instead. For example, ```php Yii::$app->db->createCommand('UPDATE post SET status=1 WHERE id=1') ->execute(); ``` The [[yii\db\Command::execute()]] method returns the number of rows affected by the SQL execution. For INSERT, UPDATE and DELETE queries, instead of writing plain SQLs, you may call [[yii\db\Command::insert()|insert()]], [[yii\db\Command::update()|update()]], [[yii\db\Command::delete()|delete()]], respectively, to build the corresponding SQLs. These methods will properly quote table and column names and bind parameter values. For example, ```php // INSERT (table name, column values) Yii::$app->db->createCommand()->insert('user', [ 'name' => 'Sam', 'age' => 30, ])->execute(); // UPDATE (table name, column values, condition) Yii::$app->db->createCommand()->update('user', ['status' => 1], 'age > 30')->execute(); // DELETE (table name, condition) Yii::$app->db->createCommand()->delete('user', 'status = 0')->execute(); ``` You may also call [[yii\db\Command::batchInsert()|batchInsert()]] to insert multiple rows in one shot, which is much more efficient than inserting one row at a time: ```php // table name, column names, column values Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [ ['Tom', 30], ['Jane', 20], ['Linda', 25], ])->execute(); ``` Another useful method is [[yii\db\Command::upsert()|upsert()]]. Upsert is an atomic operation that inserts rows into a database table if they do not already exist (matching unique constraints), or update them if they do: ```php Yii::$app->db->createCommand()->upsert('pages', [ 'name' => 'Front page', 'url' => 'https://example.com/', // url is unique 'visits' => 0, ], [ 'visits' => new \yii\db\Expression('visits + 1'), ], $params)->execute(); ``` The code above will either insert a new page record or increment its visit counter atomically. Note that the aforementioned methods only create the query and you always have to call [[yii\db\Command::execute()|execute()]] to actually run them. ## Quoting Table and Column Names When writing database-agnostic code, properly quoting table and column names is often a headache because different databases have different name quoting rules. To overcome this problem, you may use the following quoting syntax introduced by Yii: * `[[column name]]`: enclose a column name to be quoted in double square brackets; * `{{table name}}`: enclose a table name to be quoted in double curly brackets. Yii DAO will automatically convert such constructs into the corresponding quoted column or table names using the DBMS specific syntax. For example, ```php // executes this SQL for MySQL: SELECT COUNT(`id`) FROM `employee` $count = Yii::$app->db->createCommand("SELECT COUNT([[id]]) FROM {{employee}}") ->queryScalar(); ``` ### Using Table Prefix If most of your DB tables names share a common prefix, you may use the table prefix feature provided by Yii DAO. First, specify the table prefix via the [[yii\db\Connection::tablePrefix]] property in the application config: ```php return [ // ... 'components' => [ // ... 'db' => [ // ... 'tablePrefix' => 'tbl_', ], ], ]; ``` Then in your code, whenever you need to refer to a table whose name contains such a prefix, use the syntax `{{%table_name}}`. The percentage character will be automatically replaced with the table prefix that you have specified when configuring the DB connection. For example, ```php // executes this SQL for MySQL: SELECT COUNT(`id`) FROM `tbl_employee` $count = Yii::$app->db->createCommand("SELECT COUNT([[id]]) FROM {{%employee}}") ->queryScalar(); ``` ## Performing Transactions When running multiple related queries in a sequence, you may need to wrap them in a transaction to ensure the integrity and consistency of your database. If any of the queries fails, the database will be rolled back to the state as if none of these queries were executed. The following code shows a typical way of using transactions: ```php Yii::$app->db->transaction(function($db) { $db->createCommand($sql1)->execute(); $db->createCommand($sql2)->execute(); // ... executing other SQL statements ... }); ``` The above code is equivalent to the following, which gives you more control about the error handling code: ```php $db = Yii::$app->db; $transaction = $db->beginTransaction(); try { $db->createCommand($sql1)->execute(); $db->createCommand($sql2)->execute(); // ... executing other SQL statements ... $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; } ``` By calling the [[yii\db\Connection::beginTransaction()|beginTransaction()]] method, a new transaction is started. The transaction is represented as a [[yii\db\Transaction]] object stored in the `$transaction` variable. Then, the queries being executed are enclosed in a `try...catch...` block. If all queries are executed successfully, the [[yii\db\Transaction::commit()|commit()]] method is called to commit the transaction. Otherwise, if an exception will be triggered and caught, the [[yii\db\Transaction::rollBack()|rollBack()]] method is called to roll back the changes made by the queries prior to that failed query in the transaction. `throw $e` will then re-throw the exception as if we had not caught it, so the normal error handling process will take care of it. > Note: in the above code we have two catch-blocks for compatibility > with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](https://www.php.net/manual/en/class.throwable.php) > since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher. ### Specifying Isolation Levels Yii also supports setting [isolation levels] for your transactions. By default, when starting a new transaction, it will use the default isolation level set by your database system. You can override the default isolation level as follows, ```php $isolationLevel = \yii\db\Transaction::REPEATABLE_READ; Yii::$app->db->transaction(function ($db) { .... }, $isolationLevel); // or alternatively $transaction = Yii::$app->db->beginTransaction($isolationLevel); ``` Yii provides four constants for the most common isolation levels: - [[\yii\db\Transaction::READ_UNCOMMITTED]] - the weakest level, Dirty reads, non-repeatable reads and phantoms may occur. - [[\yii\db\Transaction::READ_COMMITTED]] - avoid dirty reads. - [[\yii\db\Transaction::REPEATABLE_READ]] - avoid dirty reads and non-repeatable reads. - [[\yii\db\Transaction::SERIALIZABLE]] - the strongest level, avoids all of the above named problems. Besides using the above constants to specify isolation levels, you may also use strings with a valid syntax supported by the DBMS that you are using. For example, in PostgreSQL, you may use `"SERIALIZABLE READ ONLY DEFERRABLE"`. Note that some DBMS allow setting the isolation level only for the whole connection. Any subsequent transactions will get the same isolation level even if you do not specify any. When using this feature you may need to set the isolation level for all transactions explicitly to avoid conflicting settings. At the time of this writing, only MSSQL and SQLite are affected by this limitation. > Note: SQLite only supports two isolation levels, so you can only use `READ UNCOMMITTED` and `SERIALIZABLE`. Usage of other levels will result in an exception being thrown. > Note: PostgreSQL does not allow setting the isolation level before the transaction starts so you can not specify the isolation level directly when starting the transaction. You have to call [[yii\db\Transaction::setIsolationLevel()]] in this case after the transaction has started. [isolation levels]: https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels ### Nesting Transactions If your DBMS supports Savepoint, you may nest multiple transactions like the following: ```php Yii::$app->db->transaction(function ($db) { // outer transaction $db->transaction(function ($db) { // inner transaction }); }); ``` Or alternatively, ```php $db = Yii::$app->db; $outerTransaction = $db->beginTransaction(); try { $db->createCommand($sql1)->execute(); $innerTransaction = $db->beginTransaction(); try { $db->createCommand($sql2)->execute(); $innerTransaction->commit(); } catch (\Exception $e) { $innerTransaction->rollBack(); throw $e; } catch (\Throwable $e) { $innerTransaction->rollBack(); throw $e; } $outerTransaction->commit(); } catch (\Exception $e) { $outerTransaction->rollBack(); throw $e; } catch (\Throwable $e) { $outerTransaction->rollBack(); throw $e; } ``` ## Replication and Read-Write Splitting Many DBMS support [database replication](https://en.wikipedia.org/wiki/Replication_(computing)#Database_replication) to get better database availability and faster server response time. With database replication, data are replicated from the so-called *master servers* to *slave servers*. All writes and updates must take place on the master servers, while reads may also take place on the slave servers. To take advantage of database replication and achieve read-write splitting, you can configure a [[yii\db\Connection]] component like the following: ```php [ 'class' => 'yii\db\Connection', // configuration for the master 'dsn' => 'dsn for master server', 'username' => 'master', 'password' => '', // common configuration for slaves 'slaveConfig' => [ 'username' => 'slave', 'password' => '', 'attributes' => [ // use a smaller connection timeout PDO::ATTR_TIMEOUT => 10, ], ], // list of slave configurations 'slaves' => [ ['dsn' => 'dsn for slave server 1'], ['dsn' => 'dsn for slave server 2'], ['dsn' => 'dsn for slave server 3'], ['dsn' => 'dsn for slave server 4'], ], ] ``` The above configuration specifies a setup with a single master and multiple slaves. One of the slaves will be connected and used to perform read queries, while the master will be used to perform write queries. Such read-write splitting is accomplished automatically with this configuration. For example, ```php // create a Connection instance using the above configuration Yii::$app->db = Yii::createObject($config); // query against one of the slaves $rows = Yii::$app->db->createCommand('SELECT * FROM user LIMIT 10')->queryAll(); // query against the master Yii::$app->db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute(); ``` > Info: Queries performed by calling [[yii\db\Command::execute()]] are considered as write queries, while all other queries done through one of the "query" methods of [[yii\db\Command]] are read queries. You can get the currently active slave connection via `Yii::$app->db->slave`. The `Connection` component supports load balancing and failover between slaves. When performing a read query for the first time, the `Connection` component will randomly pick a slave and try connecting to it. If the slave is found "dead", it will try another one. If none of the slaves is available, it will connect to the master. By configuring a [[yii\db\Connection::serverStatusCache|server status cache]], a "dead" server can be remembered so that it will not be tried again during a [[yii\db\Connection::serverRetryInterval|certain period of time]]. > Info: In the above configuration, a connection timeout of 10 seconds is specified for every slave. This means if a slave cannot be reached in 10 seconds, it is considered as "dead". You can adjust this parameter based on your actual environment. You can also configure multiple masters with multiple slaves. For example, ```php [ 'class' => 'yii\db\Connection', // common configuration for masters 'masterConfig' => [ 'username' => 'master', 'password' => '', 'attributes' => [ // use a smaller connection timeout PDO::ATTR_TIMEOUT => 10, ], ], // list of master configurations 'masters' => [ ['dsn' => 'dsn for master server 1'], ['dsn' => 'dsn for master server 2'], ], // common configuration for slaves 'slaveConfig' => [ 'username' => 'slave', 'password' => '', 'attributes' => [ // use a smaller connection timeout PDO::ATTR_TIMEOUT => 10, ], ], // list of slave configurations 'slaves' => [ ['dsn' => 'dsn for slave server 1'], ['dsn' => 'dsn for slave server 2'], ['dsn' => 'dsn for slave server 3'], ['dsn' => 'dsn for slave server 4'], ], ] ``` The above configuration specifies two masters and four slaves. The `Connection` component also supports load balancing and failover between masters just as it does between slaves. A difference is that when none of the masters are available an exception will be thrown. > Note: When you use the [[yii\db\Connection::masters|masters]] property to configure one or multiple masters, all other properties for specifying a database connection (e.g. `dsn`, `username`, `password`) with the `Connection` object itself will be ignored. By default, transactions use the master connection. And within a transaction, all DB operations will use the master connection. For example, ```php $db = Yii::$app->db; // the transaction is started on the master connection $transaction = $db->beginTransaction(); try { // both queries are performed against the master $rows = $db->createCommand('SELECT * FROM user LIMIT 10')->queryAll(); $db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute(); $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; } ``` If you want to start a transaction with the slave connection, you should explicitly do so, like the following: ```php $transaction = Yii::$app->db->slave->beginTransaction(); ``` Sometimes, you may want to force using the master connection to perform a read query. This can be achieved with the `useMaster()` method: ```php $rows = Yii::$app->db->useMaster(function ($db) { return $db->createCommand('SELECT * FROM user LIMIT 10')->queryAll(); }); ``` You may also directly set `Yii::$app->db->enableSlaves` to be `false` to direct all queries to the master connection. ## Working with Database Schema Yii DAO provides a whole set of methods to let you manipulate the database schema, such as creating new tables, dropping a column from a table, etc. These methods are listed as follows: * [[yii\db\Command::createTable()|createTable()]]: creating a table * [[yii\db\Command::renameTable()|renameTable()]]: renaming a table * [[yii\db\Command::dropTable()|dropTable()]]: removing a table * [[yii\db\Command::truncateTable()|truncateTable()]]: removing all rows in a table * [[yii\db\Command::addColumn()|addColumn()]]: adding a column * [[yii\db\Command::renameColumn()|renameColumn()]]: renaming a column * [[yii\db\Command::dropColumn()|dropColumn()]]: removing a column * [[yii\db\Command::alterColumn()|alterColumn()]]: altering a column * [[yii\db\Command::addPrimaryKey()|addPrimaryKey()]]: adding a primary key * [[yii\db\Command::dropPrimaryKey()|dropPrimaryKey()]]: removing a primary key * [[yii\db\Command::addForeignKey()|addForeignKey()]]: adding a foreign key * [[yii\db\Command::dropForeignKey()|dropForeignKey()]]: removing a foreign key * [[yii\db\Command::createIndex()|createIndex()]]: creating an index * [[yii\db\Command::dropIndex()|dropIndex()]]: removing an index These methods can be used like the following: ```php // CREATE TABLE Yii::$app->db->createCommand()->createTable('post', [ 'id' => 'pk', 'title' => 'string', 'text' => 'text', ]); ``` The above array describes the name and types of the columns to be created. For the column types, Yii provides a set of abstract data types, that allow you to define a database agnostic schema. These are converted to DBMS specific type definitions dependent on the database, the table is created in. Please refer to the API documentation of the [[yii\db\Command::createTable()|createTable()]]-method for more information. Besides changing the database schema, you can also retrieve the definition information about a table through the [[yii\db\Connection::getTableSchema()|getTableSchema()]] method of a DB connection. For example, ```php $table = Yii::$app->db->getTableSchema('post'); ``` The method returns a [[yii\db\TableSchema]] object which contains the information about the table's columns, primary keys, foreign keys, etc. All this information is mainly utilized by [query builder](db-query-builder.md) and [active record](db-active-record.md) to help you write database-agnostic code. ================================================ FILE: docs/guide/db-migrations.md ================================================ Database Migration ================== During the course of developing and maintaining a database-driven application, the structure of the database being used evolves just like the source code does. For example, during the development of an application, a new table may be found necessary; after the application is deployed to production, it may be discovered that an index should be created to improve the query performance; and so on. Because a database structure change often requires some source code changes, Yii supports the so-called *database migration* feature that allows you to keep track of database changes in terms of *database migrations* which are version-controlled together with the source code. The following steps show how database migration can be used by a team during development: 1. Tim creates a new migration (e.g. creates a new table, changes a column definition, etc.). 2. Tim commits the new migration into the source control system (e.g. Git, Mercurial). 3. Doug updates his repository from the source control system and receives the new migration. 4. Doug applies the migration to his local development database, thereby synchronizing his database to reflect the changes that Tim has made. And the following steps show how to deploy a new release with database migrations to production: 1. Scott creates a release tag for the project repository that contains some new database migrations. 2. Scott updates the source code on the production server to the release tag. 3. Scott applies any accumulated database migrations to the production database. Yii provides a set of migration command line tools that allow you to: * create new migrations; * apply migrations; * revert migrations; * re-apply migrations; * show migration history and status. All these tools are accessible through the command `yii migrate`. In this section we will describe in detail how to accomplish various tasks using these tools. You may also get the usage of each tool via the help command `yii help migrate`. > Tip: Migrations could affect not only database schema but adjust existing data to fit new schema, create RBAC hierarchy or clean up cache. > Note: When manipulating data using a migration you may find that using your [Active Record](db-active-record.md) classes > for this might be useful because some of the logic is already implemented there. Keep in mind however, that in contrast > to code written in the migrations, whose nature is to stay constant forever, application logic is subject to change. > So when using Active Record in migration code, changes to the logic in the Active Record layer may accidentally break > existing migrations. For this reason migration code should be kept independent of other application logic such > as Active Record classes. ## Creating Migrations To create a new migration, run the following command: ``` yii migrate/create ``` The required `name` argument gives a brief description about the new migration. For example, if the migration is about creating a new table named *news*, you may use the name `create_news_table` and run the following command: ``` yii migrate/create create_news_table ``` > Note: Because the `name` argument will be used as part of the generated migration class name, it should only contain letters, digits, and/or underscore characters. The above command will create a new PHP class file named `m150101_185401_create_news_table.php` in the `@app/migrations` directory. The file contains the following code which mainly declares a migration class `m150101_185401_create_news_table` with the skeleton code: ```php _`, where * `` refers to the UTC datetime at which the migration creation command is executed. * `` is the same as the value of the `name` argument that you provide to the command. In the migration class, you are expected to write code in the `up()` method that makes changes to the database structure. You may also want to write code in the `down()` method to revert the changes made by `up()`. The `up()` method is invoked when you upgrade the database with this migration, while the `down()` method is invoked when you downgrade the database. The following code shows how you may implement the migration class to create a `news` table: ```php createTable('news', [ 'id' => Schema::TYPE_PK, 'title' => Schema::TYPE_STRING . ' NOT NULL', 'content' => Schema::TYPE_TEXT, ]); } public function down() { $this->dropTable('news'); } } ``` > Info: Not all migrations are reversible. For example, if the `up()` method deletes a row of a table, you may not be able to recover this row in the `down()` method. Sometimes, you may be just too lazy to implement the `down()`, because it is not very common to revert database migrations. In this case, you should return `false` in the `down()` method to indicate that the migration is not reversible. The base migration class [[yii\db\Migration]] exposes a database connection via the [[yii\db\Migration::db|db]] property. You can use it to manipulate the database schema using the methods as described in [Working with Database Schema](db-dao.md#database-schema). Rather than using physical types, when creating a table or column you should use *abstract types* so that your migrations are independent of specific DBMS. The [[yii\db\Schema]] class defines a set of constants to represent the supported abstract types. These constants are named in the format of `TYPE_`. For example, `TYPE_PK` refers to auto-incremental primary key type; `TYPE_STRING` refers to a string type. When a migration is applied to a particular database, the abstract types will be translated into the corresponding physical types. In the case of MySQL, `TYPE_PK` will be turned into `int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY`, while `TYPE_STRING` becomes `varchar(255)`. You can append additional constraints when using abstract types. In the above example, ` NOT NULL` is appended to `Schema::TYPE_STRING` to specify that the column cannot be `null`. > Info: The mapping between abstract types and physical types is specified by the [[yii\db\QueryBuilder::$typeMap|$typeMap]] property in each concrete `QueryBuilder` class. Since version 2.0.6, you can make use of the newly introduced schema builder which provides more convenient way of defining column schema. So the migration above could be written like the following: ```php createTable('news', [ 'id' => $this->primaryKey(), 'title' => $this->string()->notNull(), 'content' => $this->text(), ]); } public function down() { $this->dropTable('news'); } } ``` A list of all available methods for defining the column types is available in the API documentation of [[yii\db\SchemaBuilderTrait]]. > Info: The generated file permissions and ownership will be determined by the current environment. This might lead to inaccessible files. This could, for example, happen when the migration is created within a docker container and the files are edited on the host. In this case the `newFileMode` and/or `newFileOwnership` of the MigrateController can be changed. E.g. in the application config: ```php [ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', 'newFileOwnership' => '1000:1000', # Default WSL user id 'newFileMode' => 0660, ], ], ]; ``` ## Generating Migrations Since version 2.0.7 migration console provides a convenient way to create migrations. If the migration name is of a special form, for example `create_xxx_table` or `drop_xxx_table` then the generated migration file will contain extra code, in this case for creating/dropping tables. In the following all variants of this feature are described. ### Create Table ``` yii migrate/create create_post_table ``` generates ```php /** * Handles the creation for table `post`. */ class m150811_220037_create_post_table extends Migration { /** * {@inheritdoc} */ public function up() { $this->createTable('post', [ 'id' => $this->primaryKey() ]); } /** * {@inheritdoc} */ public function down() { $this->dropTable('post'); } } ``` To create table fields right away, specify them via `--fields` option. ``` yii migrate/create create_post_table --fields="title:string,body:text" ``` generates ```php /** * Handles the creation for table `post`. */ class m150811_220037_create_post_table extends Migration { /** * {@inheritdoc} */ public function up() { $this->createTable('post', [ 'id' => $this->primaryKey(), 'title' => $this->string(), 'body' => $this->text(), ]); } /** * {@inheritdoc} */ public function down() { $this->dropTable('post'); } } ``` You can specify more field parameters. ``` yii migrate/create create_post_table --fields="title:string(12):notNull:unique,body:text" ``` generates ```php /** * Handles the creation for table `post`. */ class m150811_220037_create_post_table extends Migration { /** * {@inheritdoc} */ public function up() { $this->createTable('post', [ 'id' => $this->primaryKey(), 'title' => $this->string(12)->notNull()->unique(), 'body' => $this->text() ]); } /** * {@inheritdoc} */ public function down() { $this->dropTable('post'); } } ``` > Note: Primary key is added automatically and is named `id` by default. If you want to use another name you may > specify it explicitly like `--fields="name:primaryKey"`. #### Foreign keys Since 2.0.8 the generator supports foreign keys using the `foreignKey` keyword. ``` yii migrate/create create_post_table --fields="author_id:integer:notNull:foreignKey(user),category_id:integer:defaultValue(1):foreignKey,title:string,body:text" ``` generates ```php /** * Handles the creation for table `post`. * Has foreign keys to the tables: * * - `user` * - `category` */ class m160328_040430_create_post_table extends Migration { /** * {@inheritdoc} */ public function up() { $this->createTable('post', [ 'id' => $this->primaryKey(), 'author_id' => $this->integer()->notNull(), 'category_id' => $this->integer()->defaultValue(1), 'title' => $this->string(), 'body' => $this->text(), ]); // creates index for column `author_id` $this->createIndex( 'idx-post-author_id', 'post', 'author_id' ); // add foreign key for table `user` $this->addForeignKey( 'fk-post-author_id', 'post', 'author_id', 'user', 'id', 'CASCADE' ); // creates index for column `category_id` $this->createIndex( 'idx-post-category_id', 'post', 'category_id' ); // add foreign key for table `category` $this->addForeignKey( 'fk-post-category_id', 'post', 'category_id', 'category', 'id', 'CASCADE' ); } /** * {@inheritdoc} */ public function down() { // drops foreign key for table `user` $this->dropForeignKey( 'fk-post-author_id', 'post' ); // drops index for column `author_id` $this->dropIndex( 'idx-post-author_id', 'post' ); // drops foreign key for table `category` $this->dropForeignKey( 'fk-post-category_id', 'post' ); // drops index for column `category_id` $this->dropIndex( 'idx-post-category_id', 'post' ); $this->dropTable('post'); } } ``` The position of the `foreignKey` keyword in the column description doesn't change the generated code. That means: - `author_id:integer:notNull:foreignKey(user)` - `author_id:integer:foreignKey(user):notNull` - `author_id:foreignKey(user):integer:notNull` All generate the same code. The `foreignKey` keyword can take a parameter between parenthesis which will be the name of the related table for the generated foreign key. If no parameter is passed then the table name will be deduced from the column name. In the example above `author_id:integer:notNull:foreignKey(user)` will generate a column named `author_id` with a foreign key to the `user` table while `category_id:integer:defaultValue(1):foreignKey` will generate a column `category_id` with a foreign key to the `category` table. Since 2.0.11, `foreignKey` keyword accepts a second parameter, separated by whitespace. It accepts the name of the related column for the foreign key generated. If no second parameter is passed, the column name will be fetched from table schema. If no schema exists, primary key isn't set or is composite, default name `id` will be used. ### Drop Table ``` yii migrate/create drop_post_table --fields="title:string(12):notNull:unique,body:text" ``` generates ```php class m150811_220037_drop_post_table extends Migration { public function up() { $this->dropTable('post'); } public function down() { $this->createTable('post', [ 'id' => $this->primaryKey(), 'title' => $this->string(12)->notNull()->unique(), 'body' => $this->text() ]); } } ``` ### Add Column If the migration name is of the form `add_xxx_column_to_yyy_table` then the file content would contain `addColumn` and `dropColumn` statements necessary. To add column: ``` yii migrate/create add_position_column_to_post_table --fields="position:integer" ``` generates ```php class m150811_220037_add_position_column_to_post_table extends Migration { public function up() { $this->addColumn('post', 'position', $this->integer()); } public function down() { $this->dropColumn('post', 'position'); } } ``` You can specify multiple columns as follows: ``` yii migrate/create add_xxx_column_yyy_column_to_zzz_table --fields="xxx:integer,yyy:text" ``` ### Drop Column If the migration name is of the form `drop_xxx_column_from_yyy_table` then the file content would contain `addColumn` and `dropColumn` statements necessary. ```php yii migrate/create drop_position_column_from_post_table --fields="position:integer" ``` generates ```php class m150811_220037_drop_position_column_from_post_table extends Migration { public function up() { $this->dropColumn('post', 'position'); } public function down() { $this->addColumn('post', 'position', $this->integer()); } } ``` ### Add Junction Table If the migration name is of the form `create_junction_table_for_xxx_and_yyy_tables` or `create_junction_xxx_and_yyy_tables` then code necessary to create junction table will be generated. ``` yii migrate/create create_junction_table_for_post_and_tag_tables --fields="created_at:dateTime" ``` generates ```php /** * Handles the creation for table `post_tag`. * Has foreign keys to the tables: * * - `post` * - `tag` */ class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migration { /** * {@inheritdoc} */ public function up() { $this->createTable('post_tag', [ 'post_id' => $this->integer(), 'tag_id' => $this->integer(), 'created_at' => $this->dateTime(), 'PRIMARY KEY(post_id, tag_id)', ]); // creates index for column `post_id` $this->createIndex( 'idx-post_tag-post_id', 'post_tag', 'post_id' ); // add foreign key for table `post` $this->addForeignKey( 'fk-post_tag-post_id', 'post_tag', 'post_id', 'post', 'id', 'CASCADE' ); // creates index for column `tag_id` $this->createIndex( 'idx-post_tag-tag_id', 'post_tag', 'tag_id' ); // add foreign key for table `tag` $this->addForeignKey( 'fk-post_tag-tag_id', 'post_tag', 'tag_id', 'tag', 'id', 'CASCADE' ); } /** * {@inheritdoc} */ public function down() { // drops foreign key for table `post` $this->dropForeignKey( 'fk-post_tag-post_id', 'post_tag' ); // drops index for column `post_id` $this->dropIndex( 'idx-post_tag-post_id', 'post_tag' ); // drops foreign key for table `tag` $this->dropForeignKey( 'fk-post_tag-tag_id', 'post_tag' ); // drops index for column `tag_id` $this->dropIndex( 'idx-post_tag-tag_id', 'post_tag' ); $this->dropTable('post_tag'); } } ``` Since 2.0.11 foreign key column names for junction tables are fetched from table schema. In case table isn't defined in schema, or the primary key isn't set or is composite, default name `id` is used. ### Transactional Migrations While performing complex DB migrations, it is important to ensure each migration to either succeed or fail as a whole so that the database can maintain integrity and consistency. To achieve this goal, it is recommended that you enclose the DB operations of each migration in a [transaction](db-dao.md#performing-transactions). An even easier way of implementing transactional migrations is to put migration code in the `safeUp()` and `safeDown()` methods. These two methods differ from `up()` and `down()` in that they are enclosed implicitly in a transaction. As a result, if any operation in these methods fails, all prior operations will be rolled back automatically. In the following example, besides creating the `news` table we also insert an initial row into this table. ```php createTable('news', [ 'id' => $this->primaryKey(), 'title' => $this->string()->notNull(), 'content' => $this->text(), ]); $this->insert('news', [ 'title' => 'test 1', 'content' => 'content 1', ]); } public function safeDown() { $this->delete('news', ['id' => 1]); $this->dropTable('news'); } } ``` Note that usually when you perform multiple DB operations in `safeUp()`, you should reverse their execution order in `safeDown()`. In the above example we first create the table and then insert a row in `safeUp()`; while in `safeDown()` we first delete the row and then drop the table. > Note: Not all DBMS support transactions. And some DB queries cannot be put into a transaction. For some examples, please refer to [implicit commit](https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html). If this is the case, you should still implement `up()` and `down()`, instead. ### Database Accessing Methods The base migration class [[yii\db\Migration]] provides a set of methods to let you access and manipulate databases. You may find these methods are named similarly as the [DAO methods](db-dao.md) provided by the [[yii\db\Command]] class. For example, the [[yii\db\Migration::createTable()]] method allows you to create a new table, just like [[yii\db\Command::createTable()]] does. The benefit of using the methods provided by [[yii\db\Migration]] is that you do not need to explicitly create [[yii\db\Command]] instances and the execution of each method will automatically display useful messages telling you what database operations are done and how long they take. Below is the list of all these database accessing methods: * [[yii\db\Migration::execute()|execute()]]: executing a SQL statement * [[yii\db\Migration::insert()|insert()]]: inserting a single row * [[yii\db\Migration::batchInsert()|batchInsert()]]: inserting multiple rows * [[yii\db\Migration::update()|update()]]: updating rows * [[yii\db\Migration::upsert()|upsert()]]: inserting a single row or updating it if it exists (since 2.0.14) * [[yii\db\Migration::delete()|delete()]]: deleting rows * [[yii\db\Migration::createTable()|createTable()]]: creating a table * [[yii\db\Migration::renameTable()|renameTable()]]: renaming a table * [[yii\db\Migration::dropTable()|dropTable()]]: removing a table * [[yii\db\Migration::truncateTable()|truncateTable()]]: removing all rows in a table * [[yii\db\Migration::addColumn()|addColumn()]]: adding a column * [[yii\db\Migration::renameColumn()|renameColumn()]]: renaming a column * [[yii\db\Migration::dropColumn()|dropColumn()]]: removing a column * [[yii\db\Migration::alterColumn()|alterColumn()]]: altering a column * [[yii\db\Migration::addPrimaryKey()|addPrimaryKey()]]: adding a primary key * [[yii\db\Migration::dropPrimaryKey()|dropPrimaryKey()]]: removing a primary key * [[yii\db\Migration::addForeignKey()|addForeignKey()]]: adding a foreign key * [[yii\db\Migration::dropForeignKey()|dropForeignKey()]]: removing a foreign key * [[yii\db\Migration::createIndex()|createIndex()]]: creating an index * [[yii\db\Migration::dropIndex()|dropIndex()]]: removing an index * [[yii\db\Migration::addCommentOnColumn()|addCommentOnColumn()]]: adding comment to column * [[yii\db\Migration::dropCommentFromColumn()|dropCommentFromColumn()]]: dropping comment from column * [[yii\db\Migration::addCommentOnTable()|addCommentOnTable()]]: adding comment to table * [[yii\db\Migration::dropCommentFromTable()|dropCommentFromTable()]]: dropping comment from table > Info: [[yii\db\Migration]] does not provide a database query method. This is because you normally do not need > to display extra message about retrieving data from a database. It is also because you can use the powerful > [Query Builder](db-query-builder.md) to build and run complex queries. > Using Query Builder in a migration may look like this: > > ```php > // update status field for all users > foreach((new Query)->from('users')->each() as $user) { > $this->update('users', ['status' => 1], ['id' => $user['id']]); > } > ``` ## Applying Migrations To upgrade a database to its latest structure, you should apply all available new migrations using the following command: ``` yii migrate ``` This command will list all migrations that have not been applied so far. If you confirm that you want to apply these migrations, it will run the `up()` or `safeUp()` method in every new migration class, one after another, in the order of their timestamp values. If any of the migrations fails, the command will quit without applying the rest of the migrations. > Tip: In case you don't have command line at your server you may try [web shell](https://github.com/samdark/yii2-webshell) > extension. For each migration that has been successfully applied, the command will insert a row into a database table named `migration` to record the successful application of the migration. This will allow the migration tool to identify which migrations have been applied and which have not. > Info: The migration tool will automatically create the `migration` table in the database specified by the [[yii\console\controllers\MigrateController::db|db]] option of the command. By default, the database is specified by the `db` [application component](structure-application-components.md). Sometimes, you may only want to apply one or a few new migrations, instead of all available migrations. You can do so by specifying the number of migrations that you want to apply when running the command. For example, the following command will try to apply the next three available migrations: ``` yii migrate 3 ``` You can also explicitly specify a particular migration to which the database should be migrated by using the `migrate/to` command in one of the following formats: ``` yii migrate/to 150101_185401 # using timestamp to specify the migration yii migrate/to "2015-01-01 18:54:01" # using a string that can be parsed by strtotime() yii migrate/to m150101_185401_create_news_table # using full name yii migrate/to 1392853618 # using UNIX timestamp ``` If there are any unapplied migrations earlier than the specified one, they will all be applied before the specified migration is applied. If the specified migration has already been applied before, any later applied migrations will be reverted. ## Reverting Migrations To revert (undo) one or multiple migrations that have been applied before, you can run the following command: ``` yii migrate/down # revert the most recently applied migration yii migrate/down 3 # revert the most 3 recently applied migrations ``` > Note: Not all migrations are reversible. Trying to revert such migrations will cause an error and stop the entire reverting process. ## Redoing Migrations Redoing migrations means first reverting the specified migrations and then applying again. This can be done as follows: ``` yii migrate/redo # redo the last applied migration yii migrate/redo 3 # redo the last 3 applied migrations ``` > Note: If a migration is not reversible, you will not be able to redo it. ## Refreshing Migrations Since Yii 2.0.13 you can delete all tables and foreign keys from the database and apply all migrations from the beginning. ``` yii migrate/fresh # truncate the database and apply all migrations from the beginning ``` ## Listing Migrations To list which migrations have been applied and which are not, you may use the following commands: ``` yii migrate/history # showing the last 10 applied migrations yii migrate/history 5 # showing the last 5 applied migrations yii migrate/history all # showing all applied migrations yii migrate/new # showing the first 10 new migrations yii migrate/new 5 # showing the first 5 new migrations yii migrate/new all # showing all new migrations ``` ## Modifying Migration History Instead of actually applying or reverting migrations, sometimes you may simply want to mark that your database has been upgraded to a particular migration. This often happens when you manually change the database to a particular state and you do not want the migration(s) for that change to be re-applied later. You can achieve this goal with the following command: ``` yii migrate/mark 150101_185401 # using timestamp to specify the migration yii migrate/mark "2015-01-01 18:54:01" # using a string that can be parsed by strtotime() yii migrate/mark m150101_185401_create_news_table # using full name yii migrate/mark 1392853618 # using UNIX timestamp ``` The command will modify the `migration` table by adding or deleting certain rows to indicate that the database has been applied migrations to the specified one. No migrations will be applied or reverted by this command. ## Customizing Migrations There are several ways to customize the migration command. ### Using Command Line Options The migration command comes with a few command-line options that can be used to customize its behaviors: * `interactive`: boolean (defaults to `true`), specifies whether to perform migrations in an interactive mode. When this is `true`, the user will be prompted before the command performs certain actions. You may want to set this to `false` if the command is being used in a background process. * `migrationPath`: string|array (defaults to `@app/migrations`), specifies the directory storing all migration class files. This can be specified as either a directory path or a path [alias](concept-aliases.md). Note that the directory must exist, or the command may trigger an error. Since version 2.0.12 an array can be specified for loading migrations from multiple sources. * `migrationTable`: string (defaults to `migration`), specifies the name of the database table for storing migration history information. The table will be automatically created by the command if it does not exist. You may also manually create it using the structure `version varchar(255) primary key, apply_time integer`. * `db`: string (defaults to `db`), specifies the ID of the database [application component](structure-application-components.md). It represents the database that will be migrated using this command. * `templateFile`: string (defaults to `@yii/views/migration.php`), specifies the path of the template file that is used for generating skeleton migration class files. This can be specified as either a file path or a path [alias](concept-aliases.md). The template file is a PHP script in which you can use a predefined variable named `$className` to get the migration class name. * `generatorTemplateFiles`: array (defaults to `[ 'create_table' => '@yii/views/createTableMigration.php', 'drop_table' => '@yii/views/dropTableMigration.php', 'add_column' => '@yii/views/addColumnMigration.php', 'drop_column' => '@yii/views/dropColumnMigration.php', 'create_junction' => '@yii/views/createTableMigration.php' ]`), specifies template files for generating migration code. See "[Generating Migrations](#generating-migrations)" for more details. * `fields`: array of column definition strings used for creating migration code. Defaults to `[]`. The format of each definition is `COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR`. For example, `--fields=name:string(12):notNull` produces a string column of size 12 which is not `null`. The following example shows how you can use these options. For example, if we want to migrate a `forum` module whose migration files are located within the module's `migrations` directory, we can use the following command: ``` # migrate the migrations in a forum module non-interactively yii migrate --migrationPath=@app/modules/forum/migrations --interactive=0 ``` ### Configuring Command Globally Instead of entering the same option values every time you run the migration command, you may configure it once for all in the application configuration like shown below: ```php return [ 'controllerMap' => [ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationTable' => 'backend_migration', ], ], ]; ``` With the above configuration, each time you run the migration command, the `backend_migration` table will be used to record the migration history. You no longer need to specify it via the `migrationTable` command-line option. ### Namespaced Migrations Since 2.0.10 you can use namespaces for the migration classes. You can specify the list of the migration namespaces via [[yii\console\controllers\MigrateController::migrationNamespaces|migrationNamespaces]]. Using of the namespaces for migration classes allows you usage of the several source locations for the migrations. For example: ```php return [ 'controllerMap' => [ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationPath' => null, // disable non-namespaced migrations if app\migrations is listed below 'migrationNamespaces' => [ 'app\migrations', // Common migrations for the whole application 'module\migrations', // Migrations for the specific project's module                'some\extension\migrations', // Migrations for the specific extension ], ], ], ]; ``` > Note: Migrations applied from different namespaces will create a **single** migration history, e.g. you might be unable to apply or revert migrations from particular namespace only. While operating namespaced migrations: creating new, reverting and so on, you should specify full namespace before migration name. Note that backslash (`\`) symbol is usually considered a special character in the shell, so you need to escape it properly to avoid shell errors or incorrect behavior. For example: ``` yii migrate/create app\\migrations\\CreateUserTable ``` > Note: Migrations specified via [[yii\console\controllers\MigrateController::migrationPath|migrationPath]] can not contain a namespace, namespaced migration can be applied only via [[yii\console\controllers\MigrateController::migrationNamespaces]] property. Since version 2.0.12 the [[yii\console\controllers\MigrateController::migrationPath|migrationPath]] property also accepts an array for specifying multiple directories that contain migrations without a namespace. This is mainly added to be used in existing projects which use migrations from different locations. These migrations mainly come from external sources, like Yii extensions developed by other developers, which can not be changed to use namespaces easily when starting to use the new approach. #### Generating namespaced migrations Namespaced migrations follow "CamelCase" naming pattern `M` (for example `M190720100234CreateUserTable`). When generating such migration remember that table name will be converted from "CamelCase" format to "underscored". For example: ``` yii migrate/create app\\migrations\\DropGreenHotelTable ``` generates migration within namespace `app\migrations` dropping table `green_hotel` and ``` yii migrate/create app\\migrations\\CreateBANANATable ``` generates migration within namespace `app\migrations` creating table `b_a_n_a_n_a`. If table's name is mixed-cased (like `studentsExam`) you need to precede the name with underscore: ``` yii migrate/create app\\migrations\\Create_studentsExamTable ``` This generates migration within namespace `app\migrations` creating table `studentsExam`. ### Separated Migrations Sometimes using single migration history for all project migrations is not desirable. For example: you may install some 'blog' extension, which contains fully separated functionality and contain its own migrations, which should not affect the ones dedicated to main project functionality. If you want several migrations to be applied and tracked down completely separated from each other, you can configure multiple migration commands which will use different namespaces and migration history tables: ```php return [ 'controllerMap' => [ // Common migrations for the whole application 'migrate-app' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationNamespaces' => ['app\migrations'], 'migrationTable' => 'migration_app', 'migrationPath' => null, ], // Migrations for the specific project's module 'migrate-module' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationNamespaces' => ['module\migrations'], 'migrationTable' => 'migration_module', 'migrationPath' => null, ], // Migrations for the specific extension 'migrate-rbac' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationPath' => '@yii/rbac/migrations', 'migrationTable' => 'migration_rbac', ], ], ]; ``` Note that to synchronize database you now need to run multiple commands instead of one: ``` yii migrate-app yii migrate-module yii migrate-rbac ``` ## Migrating Multiple Databases By default, migrations are applied to the same database specified by the `db` [application component](structure-application-components.md). If you want them to be applied to a different database, you may specify the `db` command-line option like shown below, ``` yii migrate --db=db2 ``` The above command will apply migrations to the `db2` database. Sometimes it may happen that you want to apply *some* of the migrations to one database, while some others to another database. To achieve this goal, when implementing a migration class you should explicitly specify the DB component ID that the migration would use, like the following: ```php db = 'db2'; parent::init(); } } ``` The above migration will be applied to `db2`, even if you specify a different database through the `db` command-line option. Note that the migration history will still be recorded in the database specified by the `db` command-line option. If you have multiple migrations that use the same database, it is recommended that you create a base migration class with the above `init()` code. Then each migration class can extend from this base class. > Tip: Besides setting the [[yii\db\Migration::db|db]] property, you can also operate on different databases by creating new database connections to them in your migration classes. You then use the [DAO methods](db-dao.md) with these connections to manipulate different databases. Another strategy that you can take to migrate multiple databases is to keep migrations for different databases in different migration paths. Then you can migrate these databases in separate commands like the following: ``` yii migrate --migrationPath=@app/migrations/db1 --db=db1 yii migrate --migrationPath=@app/migrations/db2 --db=db2 ... ``` The first command will apply migrations in `@app/migrations/db1` to the `db1` database, the second command will apply migrations in `@app/migrations/db2` to `db2`, and so on. ================================================ FILE: docs/guide/db-query-builder.md ================================================ Query Builder ============= Built on top of [Database Access Objects](db-dao.md), query builder allows you to construct a SQL query in a programmatic and DBMS-agnostic way. Compared to writing raw SQL statements, using query builder will help you write more readable SQL-related code and generate more secure SQL statements. Using query builder usually involves two steps: 1. Build a [[yii\db\Query]] object to represent different parts (e.g. `SELECT`, `FROM`) of a SELECT SQL statement. 2. Execute a query method (e.g. `all()`) of [[yii\db\Query]] to retrieve data from the database. The following code shows a typical way of using query builder: ```php $rows = (new \yii\db\Query()) ->select(['id', 'email']) ->from('user') ->where(['last_name' => 'Smith']) ->limit(10) ->all(); ``` The above code generates and executes the following SQL query, where the `:last_name` parameter is bound with the string `'Smith'`. ```sql SELECT `id`, `email` FROM `user` WHERE `last_name` = :last_name LIMIT 10 ``` > Info: You usually mainly work with [[yii\db\Query]] instead of [[yii\db\QueryBuilder]]. The latter is invoked by the former implicitly when you call one of the query methods. [[yii\db\QueryBuilder]] is the class responsible for generating DBMS-dependent SQL statements (e.g. quoting table/column names differently) from DBMS-independent [[yii\db\Query]] objects. ## Building Queries To build a [[yii\db\Query]] object, you call different query building methods to specify different parts of a SQL query. The names of these methods resemble the SQL keywords used in the corresponding parts of the SQL statement. For example, to specify the `FROM` part of a SQL query, you would call the [[yii\db\Query::from()|from()]] method. All the query building methods return the query object itself, which allows you to chain multiple calls together. In the following, we will describe the usage of each query building method. ### [[yii\db\Query::select()|select()]] The [[yii\db\Query::select()|select()]] method specifies the `SELECT` fragment of a SQL statement. You can specify columns to be selected in either an array or a string, like the following. The column names being selected will be automatically quoted when the SQL statement is being generated from a query object. ```php $query->select(['id', 'email']); // equivalent to: $query->select('id, email'); ``` The column names being selected may include table prefixes and/or column aliases, like you do when writing raw SQL queries. For example, ```php $query->select(['user.id AS user_id', 'email']); // equivalent to: $query->select('user.id AS user_id, email'); ``` If you are using the array format to specify columns, you can also use the array keys to specify the column aliases. For example, the above code can be rewritten as follows, ```php $query->select(['user_id' => 'user.id', 'email']); ``` If you do not call the [[yii\db\Query::select()|select()]] method when building a query, `*` will be selected, which means selecting *all* columns. Besides column names, you can also select DB expressions. You must use the array format when selecting a DB expression that contains commas to avoid incorrect automatic name quoting. For example, ```php $query->select(["CONCAT(first_name, ' ', last_name) AS full_name", 'email']); ``` As with all places where raw SQL is involved, you may use the [DBMS agnostic quoting syntax](db-dao.md#quoting-table-and-column-names) for table and column names when writing DB expressions in select. Starting from version 2.0.1, you may also select sub-queries. You should specify each sub-query in terms of a [[yii\db\Query]] object. For example, ```php $subQuery = (new Query())->select('COUNT(*)')->from('user'); // SELECT `id`, (SELECT COUNT(*) FROM `user`) AS `count` FROM `post` $query = (new Query())->select(['id', 'count' => $subQuery])->from('post'); ``` To select distinct rows, you may call [[yii\db\Query::distinct()|distinct()]], like the following: ```php // SELECT DISTINCT `user_id` ... $query->select('user_id')->distinct(); ``` You can call [[yii\db\Query::addSelect()|addSelect()]] to select additional columns. For example, ```php $query->select(['id', 'username']) ->addSelect(['email']); ``` ### [[yii\db\Query::from()|from()]] The [[yii\db\Query::from()|from()]] method specifies the `FROM` fragment of a SQL statement. For example, ```php // SELECT * FROM `user` $query->from('user'); ``` You can specify the table(s) being selected from in either a string or an array. The table names may contain schema prefixes and/or table aliases, like you do when writing raw SQL statements. For example, ```php $query->from(['public.user u', 'public.post p']); // equivalent to: $query->from('public.user u, public.post p'); ``` If you are using the array format, you can also use the array keys to specify the table aliases, like the following: ```php $query->from(['u' => 'public.user', 'p' => 'public.post']); ``` Besides table names, you can also select from sub-queries by specifying them in terms of [[yii\db\Query]] objects. For example, ```php $subQuery = (new Query())->select('id')->from('user')->where('status=1'); // SELECT * FROM (SELECT `id` FROM `user` WHERE status=1) u $query->from(['u' => $subQuery]); ``` #### Prefixes Also a default [[yii\db\Connection::$tablePrefix|tablePrefix]] can be applied. Implementation instructions are in the ["Quoting Tables" section of the "Database Access Objects" guide](db-dao.md#quoting-table-and-column-names). ### [[yii\db\Query::where()|where()]] The [[yii\db\Query::where()|where()]] method specifies the `WHERE` fragment of a SQL query. You can use one of the four formats to specify a `WHERE` condition: - string format, e.g., `'status=1'` - hash format, e.g. `['status' => 1, 'type' => 2]` - operator format, e.g. `['like', 'name', 'test']` - object format, e.g. `new LikeCondition('name', 'LIKE', 'test')` #### String Format String format is best used to specify very simple conditions or if you need to use built-in functions of the DBMS. It works as if you are writing a raw SQL. For example, ```php $query->where('status=1'); // or use parameter binding to bind dynamic parameter values $query->where('status=:status', [':status' => $status]); // raw SQL using MySQL YEAR() function on a date field $query->where('YEAR(somedate) = 2015'); ``` Do NOT embed variables directly in the condition like the following, especially if the variable values come from end user inputs, because this will make your application subject to SQL injection attacks. ```php // Dangerous! Do NOT do this unless you are very certain $status must be an integer. $query->where("status=$status"); ``` When using `parameter binding`, you may call [[yii\db\Query::params()|params()]] or [[yii\db\Query::addParams()|addParams()]] to specify parameters separately. ```php $query->where('status=:status') ->addParams([':status' => $status]); ``` As with all places where raw SQL is involved, you may use the [DBMS agnostic quoting syntax](db-dao.md#quoting-table-and-column-names) for table and column names when writing conditions in string format. #### Hash Format Hash format is best used to specify multiple `AND`-concatenated sub-conditions each being a simple equality assertion. It is written as an array whose keys are column names and values the corresponding values that the columns should be. For example, ```php // ...WHERE (`status` = 10) AND (`type` IS NULL) AND (`id` IN (4, 8, 15)) $query->where([ 'status' => 10, 'type' => null, 'id' => [4, 8, 15], ]); ``` As you can see, the query builder is intelligent enough to properly handle values that are nulls or arrays. You can also use sub-queries with hash format like the following: ```php $userQuery = (new Query())->select('id')->from('user'); // ...WHERE `id` IN (SELECT `id` FROM `user`) $query->where(['id' => $userQuery]); ``` Using the Hash Format, Yii internally applies parameter binding for values, so in contrast to the [string format](#string-format), here you do not have to add parameters manually. However, note that Yii never escapes column names, so if you pass a variable obtained from user side as a column name without any additional checks, the application will become vulnerable to SQL injection attack. In order to keep the application secure, either do not use variables as column names or filter variable against allowlist. In case you need to get column name from user, read the [Filtering Data](output-data-widgets.md#filtering-data) guide article. For example the following code is vulnerable: ```php // Vulnerable code: $column = $request->get('column'); $value = $request->get('value'); $query->where([$column => $value]); // $value is safe, but $column name won't be encoded! ``` #### Operator Format Operator format allows you to specify arbitrary conditions in a programmatic way. It takes the following format: ```php [operator, operand1, operand2, ...] ``` where the operands can each be specified in string format, hash format or operator format recursively, while the operator can be one of the following: - `and`: the operands should be concatenated together using `AND`. For example, `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, it will be converted into a string using the rules described here. For example, `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. The method will NOT do any quoting or escaping. - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - `not`: requires only operand 1, which will be wrapped in `NOT()`. For example, `['not', 'id=1']` will generate `NOT (id=1)`. Operand 1 may also be an array to describe multiple expressions. For example `['not', ['status' => 'draft', 'name' => 'example']]` will generate `NOT ((status='draft') AND (name='example'))`. - `between`: operand 1 should be the column name, and operand 2 and 3 should be the starting and ending values of the range that the column is in. For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. In case you need to build a condition where value is between two columns (like `11 BETWEEN min_id AND max_id`), you should use [[yii\db\conditions\BetweenColumnsCondition|BetweenColumnsCondition]]. See [Conditions – Object Format](#object-format) chapter to learn more about object definition of conditions. - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` in the generated condition. - `in`: operand 1 should be a column or DB expression. Operand 2 can be either an array or a `Query` object. It will generate an `IN` condition. If Operand 2 is an array, it will represent the range of the values that the column or DB expression should be; If Operand 2 is a `Query` object, a sub-query will be generated and used as the range of the column or DB expression. For example, `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. The method will properly quote the column name and escape values in the range. The `in` operator also supports composite columns. In this case, operand 1 should be an array of the columns, while operand 2 should be an array of arrays or a `Query` object representing the range of the columns. For example, `['in', ['id', 'name'], [['id' => 1, 'name' => 'oy']]]` will generate `(id, name) IN ((1, 'oy'))`. - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing the values that the column or DB expression should be like. For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`. When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate `name LIKE '%test%' AND name LIKE '%sample%'`. You may also provide an optional third operand to specify how to escape special characters in the values. The operand should be an array of mappings from the special characters to their escaped counterparts. If this operand is not provided, a default escape mapping will be used. You may use `false` or an empty array to indicate the values are already escaped and no escape should be applied. Note that when using an escape mapping (or the third operand is not provided), the values will be automatically enclosed within a pair of percentage characters. > Note: When using PostgreSQL you may also use [`ilike`](https://www.postgresql.org/docs/8.3/static/functions-matching.html#FUNCTIONS-LIKE) > instead of `like` for case-insensitive matching. - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` predicates when operand 2 is an array. - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` in the generated condition. - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate the `NOT LIKE` predicates. - `exists`: requires one operand which must be an instance of [[yii\db\Query]] representing the sub-query. It will build an `EXISTS (sub-query)` expression. - `not exists`: similar to the `exists` operator and builds a `NOT EXISTS (sub-query)` expression. - `>`, `<=`, or any other valid DB operator that takes two operands: the first operand must be a column name while the second operand a value. For example, `['>', 'age', 10]` will generate `age>10`. Using the Operator Format, Yii internally uses parameter binding for values, so in contrast to the [string format](#string-format), here you do not have to add parameters manually. However, note that Yii never escapes column names, so if you pass a variable as a column name, the application will likely become vulnerable to SQL injection attack. In order to keep application secure, either do not use variables as column names or filter variable against allowlist. In case you need to get column name from user, read the [Filtering Data](output-data-widgets.md#filtering-data) guide article. For example the following code is vulnerable: ```php // Vulnerable code: $column = $request->get('column'); $value = $request->get('value'); $query->where(['=', $column, $value]); // $value is safe, but $column name won't be encoded! ``` #### Object Format Object Form is available since 2.0.14 and is both most powerful and most complex way to define conditions. You need to follow it either if you want to build your own abstraction over query builder or if you want to implement your own complex conditions. Instances of condition classes are immutable. Their only purpose is to store condition data and provide getters for condition builders. Condition builder is a class that holds the logic that transforms data stored in condition into the SQL expression. Internally the formats described above are implicitly converted to object format prior to building raw SQL, so it is possible to combine formats in a single condition: ```php $query->andWhere(new OrCondition([ new InCondition('type', 'in', $types), ['like', 'name', '%good%'], 'disabled=false' ])) ``` Conversion from operator format into object format is performed according to [[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]] property, that maps operators names to representative class names: - `AND`, `OR` -> `yii\db\conditions\ConjunctionCondition` - `NOT` -> `yii\db\conditions\NotCondition` - `IN`, `NOT IN` -> `yii\db\conditions\InCondition` - `BETWEEN`, `NOT BETWEEN` -> `yii\db\conditions\BetweenCondition` And so on. Using the object format makes it possible to create your own conditions or to change the way default ones are built. See [Adding Custom Conditions and Expressions](#adding-custom-conditions-and-expressions) chapter to learn more. #### Appending Conditions You can use [[yii\db\Query::andWhere()|andWhere()]] or [[yii\db\Query::orWhere()|orWhere()]] to append additional conditions to an existing one. You can call them multiple times to append multiple conditions separately. For example, ```php $status = 10; $search = 'yii'; $query->where(['status' => $status]); if (!empty($search)) { $query->andWhere(['like', 'title', $search]); } ``` If `$search` is not empty, the following `WHERE` condition will be generated: ```sql WHERE (`status` = 10) AND (`title` LIKE '%yii%') ``` #### Filter Conditions When building `WHERE` conditions based on input from end users, you usually want to ignore those input values, that are empty. For example, in a search form that allows you to search by username and email, you would like to ignore the username/email condition if the user does not enter anything in the username/email input field. You can achieve this goal by using the [[yii\db\Query::filterWhere()|filterWhere()]] method: ```php // $username and $email are from user inputs $query->filterWhere([ 'username' => $username, 'email' => $email, ]); ``` The only difference between [[yii\db\Query::filterWhere()|filterWhere()]] and [[yii\db\Query::where()|where()]] is that the former will ignore empty values provided in the condition in [hash format](#hash-format). So if `$email` is empty while `$username` is not, the above code will result in the SQL condition `WHERE username=:username`. > Info: A value is considered empty if it is `null`, an empty array, an empty string or a string consisting of whitespaces only. Like [[yii\db\Query::andWhere()|andWhere()]] and [[yii\db\Query::orWhere()|orWhere()]], you can use [[yii\db\Query::andFilterWhere()|andFilterWhere()]] and [[yii\db\Query::orFilterWhere()|orFilterWhere()]] to append additional filter conditions to the existing one. Additionally, there is [[yii\db\Query::andFilterCompare()]] that can intelligently determine operator based on what's in the value: ```php $query->andFilterCompare('name', 'John Doe'); $query->andFilterCompare('rating', '>9'); $query->andFilterCompare('value', '<=100'); ``` You can also specify operator explicitly: ```php $query->andFilterCompare('name', 'Doe', 'like'); ``` Since Yii 2.0.11 there are similar methods for `HAVING` condition: - [[yii\db\Query::filterHaving()|filterHaving()]] - [[yii\db\Query::andFilterHaving()|andFilterHaving()]] - [[yii\db\Query::orFilterHaving()|orFilterHaving()]] ### [[yii\db\Query::orderBy()|orderBy()]] The [[yii\db\Query::orderBy()|orderBy()]] method specifies the `ORDER BY` fragment of a SQL query. For example, ```php // ... ORDER BY `id` ASC, `name` DESC $query->orderBy([ 'id' => SORT_ASC, 'name' => SORT_DESC, ]); ``` In the above code, the array keys are column names while the array values are the corresponding order by directions. The PHP constant `SORT_ASC` specifies ascending sort and `SORT_DESC` descending sort. If `ORDER BY` only involves simple column names, you can specify it using a string, just like you do when writing raw SQL statements. For example, ```php $query->orderBy('id ASC, name DESC'); ``` > Note: You should use the array format if `ORDER BY` involves some DB expression. You can call [[yii\db\Query::addOrderBy()|addOrderBy()]] to add additional columns to the `ORDER BY` fragment. For example, ```php $query->orderBy('id ASC') ->addOrderBy('name DESC'); ``` ### [[yii\db\Query::groupBy()|groupBy()]] The [[yii\db\Query::groupBy()|groupBy()]] method specifies the `GROUP BY` fragment of a SQL query. For example, ```php // ... GROUP BY `id`, `status` $query->groupBy(['id', 'status']); ``` If `GROUP BY` only involves simple column names, you can specify it using a string, just like you do when writing raw SQL statements. For example, ```php $query->groupBy('id, status'); ``` > Note: You should use the array format if `GROUP BY` involves some DB expression. You can call [[yii\db\Query::addGroupBy()|addGroupBy()]] to add additional columns to the `GROUP BY` fragment. For example, ```php $query->groupBy(['id', 'status']) ->addGroupBy('age'); ``` ### [[yii\db\Query::having()|having()]] The [[yii\db\Query::having()|having()]] method specifies the `HAVING` fragment of a SQL query. It takes a condition which can be specified in the same way as that for [where()](#where). For example, ```php // ... HAVING `status` = 1 $query->having(['status' => 1]); ``` Please refer to the documentation for [where()](#where) for more details about how to specify a condition. You can call [[yii\db\Query::andHaving()|andHaving()]] or [[yii\db\Query::orHaving()|orHaving()]] to append additional conditions to the `HAVING` fragment. For example, ```php // ... HAVING (`status` = 1) AND (`age` > 30) $query->having(['status' => 1]) ->andHaving(['>', 'age', 30]); ``` ### [[yii\db\Query::limit()|limit()]] and [[yii\db\Query::offset()|offset()]] The [[yii\db\Query::limit()|limit()]] and [[yii\db\Query::offset()|offset()]] methods specify the `LIMIT` and `OFFSET` fragments of a SQL query. For example, ```php // ... LIMIT 10 OFFSET 20 $query->limit(10)->offset(20); ``` If you specify an invalid limit or offset (e.g. a negative value), it will be ignored. > Info: For DBMS that do not support `LIMIT` and `OFFSET` (e.g. MSSQL), query builder will generate a SQL statement that emulates the `LIMIT`/`OFFSET` behavior. ### [[yii\db\Query::join()|join()]] The [[yii\db\Query::join()|join()]] method specifies the `JOIN` fragment of a SQL query. For example, ```php // ... LEFT JOIN `post` ON `post`.`user_id` = `user`.`id` $query->join('LEFT JOIN', 'post', 'post.user_id = user.id'); ``` The [[yii\db\Query::join()|join()]] method takes four parameters: - `$type`: join type, e.g., `'INNER JOIN'`, `'LEFT JOIN'`. - `$table`: the name of the table to be joined. - `$on`: optional, the join condition, i.e., the `ON` fragment. Please refer to [where()](#where) for details about specifying a condition. Note, that the array syntax does **not** work for specifying a column based condition, e.g. `['user.id' => 'comment.userId']` will result in a condition where the user id must be equal to the string `'comment.userId'`. You should use the string syntax instead and specify the condition as `'user.id = comment.userId'`. - `$params`: optional, the parameters to be bound to the join condition. You can use the following shortcut methods to specify `INNER JOIN`, `LEFT JOIN` and `RIGHT JOIN`, respectively. - [[yii\db\Query::innerJoin()|innerJoin()]] - [[yii\db\Query::leftJoin()|leftJoin()]] - [[yii\db\Query::rightJoin()|rightJoin()]] For example, ```php $query->leftJoin('post', 'post.user_id = user.id'); ``` To join with multiple tables, call the above join methods multiple times, once for each table. Besides joining with tables, you can also join with sub-queries. To do so, specify the sub-queries to be joined as [[yii\db\Query]] objects. For example, ```php $subQuery = (new \yii\db\Query())->from('post'); $query->leftJoin(['u' => $subQuery], 'u.id = author_id'); ``` In this case, you should put the sub-query in an array and use the array key to specify the alias. ### [[yii\db\Query::union()|union()]] The [[yii\db\Query::union()|union()]] method specifies the `UNION` fragment of a SQL query. For example, ```php $query1 = (new \yii\db\Query()) ->select("id, category_id AS type, name") ->from('post') ->limit(10); $query2 = (new \yii\db\Query()) ->select('id, type, name') ->from('user') ->limit(10); $query1->union($query2); ``` You can call [[yii\db\Query::union()|union()]] multiple times to append more `UNION` fragments. ### [[yii\db\Query::withQuery()|withQuery()]] The [[yii\db\Query::withQuery()|withQuery()]] method specifies the `WITH` prefix of a SQL query. You can use it instead of subquery for more readability and some unique features (recursive CTE). Read more at [modern-sql](https://modern-sql.com/feature/with). For example, this query will select all nested permissions of `admin` with their children recursively, ```php $initialQuery = (new \yii\db\Query()) ->select(['parent', 'child']) ->from(['aic' => 'auth_item_child']) ->where(['parent' => 'admin']); $recursiveQuery = (new \yii\db\Query()) ->select(['aic.parent', 'aic.child']) ->from(['aic' => 'auth_item_child']) ->innerJoin('t1', 't1.child = aic.parent'); $mainQuery = (new \yii\db\Query()) ->select(['parent', 'child']) ->from('t1') ->withQuery($initialQuery->union($recursiveQuery), 't1', true); ``` [[yii\db\Query::withQuery()|withQuery()]] can be called multiple times to prepend more CTE's to main query. Queries will be prepend in same order as they attached. If one of query is recursive then whole CTE become recursive. ## Query Methods [[yii\db\Query]] provides a whole set of methods for different query purposes: - [[yii\db\Query::all()|all()]]: returns an array of rows with each row being an associative array of name-value pairs. - [[yii\db\Query::one()|one()]]: returns the first row of the result. - [[yii\db\Query::column()|column()]]: returns the first column of the result. - [[yii\db\Query::scalar()|scalar()]]: returns a scalar value located at the first row and first column of the result. - [[yii\db\Query::exists()|exists()]]: returns a value indicating whether the query contains any result. - [[yii\db\Query::count()|count()]]: returns the result of a `COUNT` query. - Other aggregation query methods, including [[yii\db\Query::sum()|sum($q)]], [[yii\db\Query::average()|average($q)]], [[yii\db\Query::max()|max($q)]], [[yii\db\Query::min()|min($q)]]. The `$q` parameter is mandatory for these methods and can be either a column name or a DB expression. For example, ```php // SELECT `id`, `email` FROM `user` $rows = (new \yii\db\Query()) ->select(['id', 'email']) ->from('user') ->all(); // SELECT * FROM `user` WHERE `username` LIKE `%test%` $row = (new \yii\db\Query()) ->from('user') ->where(['like', 'username', 'test']) ->one(); ``` > Note: The [[yii\db\Query::one()|one()]] method only returns the first row of the query result. It does NOT add `LIMIT 1` to the generated SQL statement. This is fine and preferred if you know the query will return only one or a few rows of data (e.g. if you are querying with some primary keys). However, if the query may potentially result in many rows of data, you should call `limit(1)` explicitly to improve the performance, e.g., `(new \yii\db\Query())->from('user')->limit(1)->one()`. All these query methods take an optional `$db` parameter representing the [[yii\db\Connection|DB connection]] that should be used to perform a DB query. If you omit this parameter, the `db` [application component](structure-application-components.md) will be used as the DB connection. Below is another example using the [[yii\db\Query::count()|count()]] query method: ```php // executes SQL: SELECT COUNT(*) FROM `user` WHERE `last_name`=:last_name $count = (new \yii\db\Query()) ->from('user') ->where(['last_name' => 'Smith']) ->count(); ``` When you call a query method of [[yii\db\Query]], it actually does the following work internally: * Call [[yii\db\QueryBuilder]] to generate a SQL statement based on the current construct of [[yii\db\Query]]; * Create a [[yii\db\Command]] object with the generated SQL statement; * Call a query method (e.g. [[yii\db\Command::queryAll()|queryAll()]]) of [[yii\db\Command]] to execute the SQL statement and retrieve the data. Sometimes, you may want to examine or use the SQL statement built from a [[yii\db\Query]] object. You can achieve this goal with the following code: ```php $command = (new \yii\db\Query()) ->select(['id', 'email']) ->from('user') ->where(['last_name' => 'Smith']) ->limit(10) ->createCommand(); // show the SQL statement echo $command->sql; // show the parameters to be bound print_r($command->params); // returns all rows of the query result $rows = $command->queryAll(); ``` ### Indexing Query Results When you call [[yii\db\Query::all()|all()]], it will return an array of rows which are indexed by consecutive integers. Sometimes you may want to index them differently, such as indexing by a particular column or expression values. You can achieve this goal by calling [[yii\db\Query::indexBy()|indexBy()]] before [[yii\db\Query::all()|all()]]. For example, ```php // returns [100 => ['id' => 100, 'username' => '...', ...], 101 => [...], 103 => [...], ...] $query = (new \yii\db\Query()) ->from('user') ->limit(10) ->indexBy('id') ->all(); ``` The column which name is passed into [[yii\db\Query::indexBy()|indexBy()]] method must be present in the result set in order for indexing to work - it is up to the developer to take care of it. To index by expression values, pass an anonymous function to the [[yii\db\Query::indexBy()|indexBy()]] method: ```php $query = (new \yii\db\Query()) ->from('user') ->indexBy(function ($row) { return $row['id'] . $row['username']; })->all(); ``` The anonymous function takes a parameter `$row` which contains the current row data and should return a scalar value which will be used as the index value for the current row. > Note: In contrast to query methods like [[yii\db\Query::groupBy()|groupBy()]] or [[yii\db\Query::orderBy()|orderBy()]] > which are converted to SQL and are part of the query, this method works after the data has been fetched from the database. > That means that only those column names can be used that have been part of SELECT in your query. > Also if you selected a column with table prefix, e.g. `customer.id`, the result set will only contain `id` so you have to call > `->indexBy('id')` without table prefix. ### Batch Query When working with large amounts of data, methods such as [[yii\db\Query::all()]] are not suitable because they require loading the whole query result into the client's memory. To solve this issue Yii provides batch query support. The server holds the query result, and the client uses a cursor to iterate over the result set one batch at a time. > Warning: There are known limitations and workarounds for the MySQL implementation of batch queries. See below. Batch query can be used like the following: ```php use yii\db\Query; $query = (new Query()) ->from('user') ->orderBy('id'); foreach ($query->batch() as $users) { // $users is an array of 100 or fewer rows from the user table } // or to iterate the row one by one foreach ($query->each() as $user) { // data is being fetched from the server in batches of 100, // but $user represents one row of data from the user table } ``` The method [[yii\db\Query::batch()]] and [[yii\db\Query::each()]] return an [[yii\db\BatchQueryResult]] object which implements the `Iterator` interface and thus can be used in the `foreach` construct. During the first iteration, a SQL query is made to the database. Data is then fetched in batches in the remaining iterations. By default, the batch size is 100, meaning 100 rows of data are being fetched in each batch. You can change the batch size by passing the first parameter to the `batch()` or `each()` method. Compared to the [[yii\db\Query::all()]], the batch query only loads 100 rows of data at a time into the memory. If you specify the query result to be indexed by some column via [[yii\db\Query::indexBy()]], the batch query will still keep the proper index. For example: ```php $query = (new \yii\db\Query()) ->from('user') ->indexBy('username'); foreach ($query->batch() as $users) { // $users is indexed by the "username" column } foreach ($query->each() as $username => $user) { // ... } ``` #### Limitations of batch query in MySQL MySQL implementation of batch queries relies on the PDO driver library. By default, MySQL queries are [`buffered`](https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php). This defeats the purpose of using the cursor to get the data, because it doesn't prevent the whole result set from being loaded into the client's memory by the driver. > Note: When `libmysqlclient` is used (typical of PHP5), PHP's memory limit won't count the memory used for result sets. It may seem that batch queries work correctly, but in reality the whole dataset is loaded into client's memory, and has the potential of using it up. To disable buffering and reduce client memory requirements, PDO connection property `PDO::MYSQL_ATTR_USE_BUFFERED_QUERY` must be set to `false`. However, until the whole dataset has been retrieved, no other query can be made through the same connection. This may prevent `ActiveRecord` from making a query to get the table schema when it needs to. If this is not a problem (the table schema is cached already), it is possible to switch the original connection into unbuffered mode, and then roll back when the batch query is done. ```php Yii::$app->db->pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); // Do batch query Yii::$app->db->pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); ``` > Note: In the case of MyISAM, for the duration of the batch query, the table may become locked, delaying or denying write access for other connections. When using unbuffered queries, try to keep the cursor open for as little time as possible. If the schema is not cached, or it is necessary to run other queries while the batch query is being processed, you can create a separate unbuffered connection to the database: ```php $unbufferedDb = new \yii\db\Connection([ 'dsn' => Yii::$app->db->dsn, 'username' => Yii::$app->db->username, 'password' => Yii::$app->db->password, 'charset' => Yii::$app->db->charset, ]); $unbufferedDb->open(); $unbufferedDb->pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); ``` If you want to ensure that the `$unbufferedDb` has exactly the same PDO attributes like the original buffered `$db` but the `PDO::MYSQL_ATTR_USE_BUFFERED_QUERY` is `false`, [consider a deep copy of `$db`](https://github.com/yiisoft/yii2/issues/8420#issuecomment-301423833), set it to false manually. Then, queries are created normally. The new connection is used to run batch queries and retrieve results either in batches or one by one: ```php // getting data in batches of 1000 foreach ($query->batch(1000, $unbufferedDb) as $users) { // ... } // data is fetched from server in batches of 1000, but is iterated one by one foreach ($query->each(1000, $unbufferedDb) as $user) { // ... } ``` When the connection is no longer necessary and the result set has been retrieved, it can be closed: ```php $unbufferedDb->close(); ``` > Note: unbuffered query uses less memory on the PHP-side, but can increase the load on the MySQL server. It is recommended to design your own code with your production practice for extra massive data, [for example, divide the range for integer keys, loop them with Unbuffered Queries](https://github.com/yiisoft/yii2/issues/8420#issuecomment-296109257). ### Adding custom Conditions and Expressions As it was mentioned in [Conditions – Object Format](#object-format) chapter, it is possible to create custom condition classes. For example, let's create a condition that will check that specific columns are less than some value. Using the operator format, it would look like the following: ```php [ 'and', ['>', 'posts', $minLimit], ['>', 'comments', $minLimit], ['>', 'reactions', $minLimit], ['>', 'subscriptions', $minLimit] ] ``` When such condition applied once, it is fine. In case it is used multiple times in a single query it can be optimized a lot. Let's create a custom condition object to demonstrate it. Yii has a [[yii\db\conditions\ConditionInterface|ConditionInterface]], that must be used to mark classes, that represent a condition. It requires `fromArrayDefinition()` method implementation, in order to make possible to create condition from array format. In case you don't need it, you can implement this method with exception throwing. Since we create our custom condition class, we can build API that suits our task the most. ```php namespace app\db\conditions; class AllGreaterCondition implements \yii\db\conditions\ConditionInterface { private $columns; private $value; /** * @param string[] $columns Array of columns that must be greater, than $value * @param mixed $value the value to compare each $column against. */ public function __construct(array $columns, $value) { $this->columns = $columns; $this->value = $value; } public static function fromArrayDefinition($operator, $operands) { throw new InvalidArgumentException('Not implemented yet, but we will do it later'); } public function getColumns() { return $this->columns; } public function getValue() { return $this->vaule; } } ``` So we can create a condition object: ```php $condition = new AllGreaterCondition(['col1', 'col2'], 42); ``` But `QueryBuilder` still does not know, to make an SQL condition out of this object. Now we need to create a builder for this condition. It must implement [[yii\db\ExpressionBuilderInterface]] that requires us to implement a `build()` method. ```php namespace app\db\conditions; class AllGreaterConditionBuilder implements \yii\db\ExpressionBuilderInterface { use \yii\db\ExpressionBuilderTrait; // Contains constructor and `queryBuilder` property. /** * @param ExpressionInterface $condition the condition to be built * @param array $params the binding parameters. * @return AllGreaterCondition */ public function build(ExpressionInterface $expression, array &$params = []) { $value = $condition->getValue(); $conditions = []; foreach ($expression->getColumns() as $column) { $conditions[] = new SimpleCondition($column, '>', $value); } return $this->queryBuilder->buildCondition(new AndCondition($conditions), $params); } } ``` Then simple let [[yii\db\QueryBuilder|QueryBuilder]] know about our new condition – add a mapping for it to the `expressionBuilders` array. It could be done right from the application configuration: ```php 'db' => [ 'class' => 'yii\db\mysql\Connection', // ... 'queryBuilder' => [ 'expressionBuilders' => [ 'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder', ], ], ], ``` Now we can use our condition in `where()`: ```php $query->andWhere(new AllGreaterCondition(['posts', 'comments', 'reactions', 'subscriptions'], $minValue)); ``` If we want to make it possible to create our custom condition using operator format, we should declare it in [[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]]: ```php 'db' => [ 'class' => 'yii\db\mysql\Connection', // ... 'queryBuilder' => [ 'expressionBuilders' => [ 'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder', ], 'conditionClasses' => [ 'ALL>' => 'app\db\conditions\AllGreaterCondition', ], ], ], ``` And create a real implementation of `AllGreaterCondition::fromArrayDefinition()` method in `app\db\conditions\AllGreaterCondition`: ```php namespace app\db\conditions; class AllGreaterCondition implements \yii\db\conditions\ConditionInterface { // ... see the implementation above public static function fromArrayDefinition($operator, $operands) { return new static($operands[0], $operands[1]); } } ``` After that, we can create our custom condition using shorter operator format: ```php $query->andWhere(['ALL>', ['posts', 'comments', 'reactions', 'subscriptions'], $minValue]); ``` You might notice, that there was two concepts used: Expressions and Conditions. There is a [[yii\db\ExpressionInterface]] that should be used to mark objects, that require an Expression Builder class, that implements [[yii\db\ExpressionBuilderInterface]] to be built. Also there is a [[yii\db\condition\ConditionInterface]], that extends [[yii\db\ExpressionInterface|ExpressionInterface]] and should be used to objects, that can be created from array definition as it was shown above, but require builder as well. To summarise: - Expression – is a Data Transfer Object (DTO) for a dataset, that can be somehow compiled to some SQL statement (an operator, string, array, JSON, etc). - Condition – is an Expression superset, that aggregates multiple Expressions (or scalar values) that can be compiled to a single SQL condition. You can create your own classes that implement [[yii\db\ExpressionInterface|ExpressionInterface]] to hide the complexity of transforming data to SQL statements. You will learn more about other examples of Expressions in the [next article](db-active-record.md); ================================================ FILE: docs/guide/glossary.md ================================================ # A ## alias Alias is a string that's used by Yii to refer to the class or directory such as `@app/vendor`. ## application The application is the central object during HTTP request. It contains a number of components and with these is getting info from request and dispatching it to an appropriate controller for further processing. The application object is instantiated as a singleton by the entry script. The application singleton can be accessed at any place via `\Yii::$app`. ## assets Asset refers to a resource file. Typically it contains JavaScript or CSS code but can be anything else that is accessed via HTTP. ## attribute An attribute is a model property (a class member variable or a magic property defined via `__get()`/`__set()`) that stores **business data**. # B ## bundle Bundle, known as package in Yii 1.1, refers to a number of assets and a configuration file that describes dependencies and lists assets. # C ## configuration Configuration may refer either to the process of setting properties of an object or to a configuration file that stores settings for an object or a class of objects. # E ## extension Extension is a set of classes, asset bundles and configurations that adds more features to the application. # I ## installation Installation is a process of preparing something to work either by following a readme file or by executing specially prepared script. In case of Yii it's setting permissions and fulfilling software requirements. # M ## module Module is a sub-application which contains MVC elements by itself, such as models, views, controllers, etc. and can be used withing the main application. Typically by forwarding requests to the module instead of handling it via controllers. # N ## namespace Namespace refers to a [PHP language feature](https://www.php.net/manual/en/language.namespaces.php) which is actively used in Yii 2. # P ## package [See bundle](#bundle). # V ## vendor Vendor is an organization or individual developer providing code in form of extensions, modules or libraries. ================================================ FILE: docs/guide/helper-array.md ================================================ ArrayHelper =========== Additionally to the [rich set of PHP array functions](https://www.php.net/manual/en/book.array.php), the Yii array helper provides extra static methods allowing you to deal with arrays more efficiently. ## Getting Values Retrieving values from an array, an object or a complex structure consisting of both using standard PHP is quite repetitive. You have to check if key exists with `isset` first, then if it does you're getting it, if not, providing default value: ```php class User { public $name = 'Alex'; } $array = [ 'foo' => [ 'bar' => new User(), ] ]; $value = isset($array['foo']['bar']->name) ? $array['foo']['bar']->name : null; ``` Yii provides a very convenient method to do it: ```php $value = ArrayHelper::getValue($array, 'foo.bar.name'); ``` First method argument is where we're getting value from. Second argument specifies how to get the data. It could be one of the following: - Name of array key or object property to retrieve value from. - Set of dot separated array keys or object property names. The one we've used in the example above. - A callback returning a value. The callback should be the following: ```php $fullName = ArrayHelper::getValue($user, function ($user, $defaultValue) { return $user->firstName . ' ' . $user->lastName; }); ``` Third optional argument is default value which is `null` if not specified. Could be used as follows: ```php $username = ArrayHelper::getValue($comment, 'user.username', 'Unknown'); ``` ## Setting values ```php $array = [ 'key' => [ 'in' => ['k' => 'value'] ] ]; ArrayHelper::setValue($array, 'key.in', ['arr' => 'val']); // the path to write the value in `$array` can be specified as an array ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']); ``` As a result, initial value of `$array['key']['in']` will be overwritten by new value ```php [ 'key' => [ 'in' => ['arr' => 'val'] ] ] ``` If the path contains a nonexistent key, it will be created ```php // if `$array['key']['in']['arr0']` is not empty, the value will be added to the array ArrayHelper::setValue($array, 'key.in.arr0.arr1', 'val'); // if you want to completely override the value `$array['key']['in']['arr0']` ArrayHelper::setValue($array, 'key.in.arr0', ['arr1' => 'val']); ``` The result will be ```php [ 'key' => [ 'in' => [ 'k' => 'value', 'arr0' => ['arr1' => 'val'] ] ] ] ``` ## Take a value from an array In case you want to get a value and then immediately remove it from an array you can use `remove` method: ```php $array = ['type' => 'A', 'options' => [1, 2]]; $type = ArrayHelper::remove($array, 'type'); ``` After executing the code `$array` will contain `['options' => [1, 2]]` and `$type` will be `A`. Note that unlike `getValue` method, `remove` supports simple key names only. ## Checking Existence of Keys `ArrayHelper::keyExists` works the same way as [array_key_exists](https://www.php.net/manual/en/function.array-key-exists.php) except that it also supports case-insensitive key comparison. For example, ```php $data1 = [ 'userName' => 'Alex', ]; $data2 = [ 'username' => 'Carsten', ]; if (!ArrayHelper::keyExists('username', $data1, false) || !ArrayHelper::keyExists('username', $data2, false)) { echo "Please provide username."; } ``` ## Retrieving Columns Often you need to get a column of values from array of data rows or objects. Common example is getting a list of IDs. ```php $array = [ ['id' => '123', 'data' => 'abc'], ['id' => '345', 'data' => 'def'], ]; $ids = ArrayHelper::getColumn($array, 'id'); ``` The result will be `['123', '345']`. If additional transformations are required or the way of getting value is complex, second argument could be specified as an anonymous function: ```php $result = ArrayHelper::getColumn($array, function ($element) { return $element['id']; }); ``` ## Re-indexing Arrays In order to index an array according to a specified key, the `index` method can be used. The input should be either multidimensional array or an array of objects. The `$key` can be either a key name of the sub-array, a property name of object, or an anonymous function that must return the value that will be used as a key. The `$groups` attribute is an array of keys, that will be used to group the input array into one or more sub-arrays based on keys specified. If the `$key` attribute or its value for the particular element is `null` and `$groups` is not defined, the array element will be discarded. Otherwise, if `$groups` is specified, array element will be added to the result array without any key. For example: ```php $array = [ ['id' => '123', 'data' => 'abc', 'device' => 'laptop'], ['id' => '345', 'data' => 'def', 'device' => 'tablet'], ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'], ]; $result = ArrayHelper::index($array, 'id'); ``` The result will be an associative array, where the key is the value of `id` attribute: ```php [ '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'], '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'] // The second element of an original array is overwritten by the last element because of the same id ] ``` Anonymous function, passed as a `$key`, gives the same result: ```php $result = ArrayHelper::index($array, function ($element) { return $element['id']; }); ``` Passing `id` as a third argument will group `$array` by `id`: ```php $result = ArrayHelper::index($array, null, 'id'); ``` The result will be a multidimensional array grouped by `id` on the first level and not indexed on the second level: ```php [ '123' => [ ['id' => '123', 'data' => 'abc', 'device' => 'laptop'] ], '345' => [ // all elements with this index are present in the result array ['id' => '345', 'data' => 'def', 'device' => 'tablet'], ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'], ] ] ``` An anonymous function can be used in the grouping array as well: ```php $result = ArrayHelper::index($array, 'data', [function ($element) { return $element['id']; }, 'device']); ``` The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level and indexed by `data` on the third level: ```php [ '123' => [ 'laptop' => [ 'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'] ] ], '345' => [ 'tablet' => [ 'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet'] ], 'smartphone' => [ 'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'] ] ] ] ``` ## Building Maps In order to build a map (key-value pairs) from a multidimensional array or an array of objects you can use `map` method. The `$from` and `$to` parameters specify the key names or property names to set up the map. Optionally, one can further group the map according to a grouping field `$group`. For example, ```php $array = [ ['id' => '123', 'name' => 'aaa', 'class' => 'x'], ['id' => '124', 'name' => 'bbb', 'class' => 'x'], ['id' => '345', 'name' => 'ccc', 'class' => 'y'], ]; $result = ArrayHelper::map($array, 'id', 'name'); // the result is: // [ // '123' => 'aaa', // '124' => 'bbb', // '345' => 'ccc', // ] $result = ArrayHelper::map($array, 'id', 'name', 'class'); // the result is: // [ // 'x' => [ // '123' => 'aaa', // '124' => 'bbb', // ], // 'y' => [ // '345' => 'ccc', // ], // ] ``` ## Multidimensional Sorting `multisort` method helps to sort an array of objects or nested arrays by one or several keys. For example, ```php $data = [ ['age' => 30, 'name' => 'Alexander'], ['age' => 30, 'name' => 'Brian'], ['age' => 19, 'name' => 'Barney'], ]; ArrayHelper::multisort($data, ['age', 'name'], [SORT_ASC, SORT_DESC]); ``` After sorting we'll get the following in `$data`: ```php [ ['age' => 19, 'name' => 'Barney'], ['age' => 30, 'name' => 'Brian'], ['age' => 30, 'name' => 'Alexander'], ]; ``` Second argument that specifies keys to sort by can be a string if it's a single key, an array in case of multiple keys or an anonymous function like the following one: ```php ArrayHelper::multisort($data, function($item) { // sort by age if it exists or by name otherwise return isset($item['age']) ? $item['age'] : $item['name']; }); ``` Third argument is direction. In case of sorting by a single key it could be either `SORT_ASC` or `SORT_DESC`. If sorting by multiple values you can sort each value differently by providing an array of sort direction. Last argument is PHP sort flag that could take the same values as the ones passed to PHP [sort()](https://www.php.net/manual/en/function.sort.php). ## Detecting Array Types It is handy to know whether an array is indexed or an associative. Here's an example: ```php // no keys specified $indexed = ['Qiang', 'Paul']; echo ArrayHelper::isIndexed($indexed); // all keys are strings $associative = ['framework' => 'Yii', 'version' => '2.0']; echo ArrayHelper::isAssociative($associative); ``` ## HTML Encoding and Decoding Values In order to encode or decode special characters in an array of strings into HTML entities you can use the following: ```php $encoded = ArrayHelper::htmlEncode($data); $decoded = ArrayHelper::htmlDecode($data); ``` Only values will be encoded by default. By passing second argument as `false` you can encode array's keys as well. Encoding will use application charset and could be changed via third argument. ## Merging Arrays You can use [[yii\helpers\ArrayHelper::merge()|ArrayHelper::merge()]] to merge two or more arrays into one recursively. If each array has an element with the same string key value, the latter will overwrite the former (different from [array_merge_recursive()](https://www.php.net/manual/en/function.array-merge-recursive.php)). Recursive merging will be conducted if both arrays have an element of array type and are having the same key. For integer-keyed elements, the elements from the latter array will be appended to the former array. You can use [[yii\helpers\UnsetArrayValue]] object to unset value from previous array or [[yii\helpers\ReplaceArrayValue]] to force replace former value instead of recursive merging. For example: ```php $array1 = [ 'name' => 'Yii', 'version' => '1.1', 'ids' => [ 1, ], 'validDomains' => [ 'example.com', 'www.example.com', ], 'emails' => [ 'admin' => 'admin@example.com', 'dev' => 'dev@example.com', ], ]; $array2 = [ 'version' => '2.0', 'ids' => [ 2, ], 'validDomains' => new \yii\helpers\ReplaceArrayValue([ 'yiiframework.com', 'www.yiiframework.com', ]), 'emails' => [ 'dev' => new \yii\helpers\UnsetArrayValue(), ], ]; $result = ArrayHelper::merge($array1, $array2); ``` The result will be: ```php [ 'name' => 'Yii', 'version' => '2.0', 'ids' => [ 1, 2, ], 'validDomains' => [ 'yiiframework.com', 'www.yiiframework.com', ], 'emails' => [ 'admin' => 'admin@example.com', ], ] ``` ## Converting Objects to Arrays Often you need to convert an object or an array of objects into an array. The most common case is converting active record models in order to serve data arrays via REST API or use it otherwise. The following code could be used to do it: ```php $posts = Post::find()->limit(10)->all(); $data = ArrayHelper::toArray($posts, [ 'app\models\Post' => [ 'id', 'title', // the key name in array result => property name 'createTime' => 'created_at', // the key name in array result => anonymous function 'length' => function ($post) { return strlen($post->content); }, ], ]); ``` The first argument contains the data we want to convert. In our case we're converting a `Post` AR model. The second argument is conversion mapping per class. We're setting a mapping for `Post` model. Each mapping array contains a set of mappings. Each mapping could be: - A field name to include as is. - A key-value pair of desired array key name and model column name to take value from. - A key-value pair of desired array key name and a callback which returns value. The result of conversion above for single model will be: ```php [ 'id' => 123, 'title' => 'test', 'createTime' => '2013-01-01 12:00AM', 'length' => 301, ] ``` It is possible to provide default way of converting object to array for a specific class by implementing [[yii\base\Arrayable|Arrayable]] interface in that class. ## Testing against Arrays Often you need to check if an element is in an array or a set of elements is a subset of another. While PHP offers `in_array()`, this does not support subsets or `\Traversable` objects. To aid these kinds of tests, [[yii\helpers\ArrayHelper]] provides [[yii\helpers\ArrayHelper::isIn()|isIn()]] and [[yii\helpers\ArrayHelper::isSubset()|isSubset()]] with the same signature as [in_array()](https://www.php.net/manual/en/function.in-array.php). ```php // true ArrayHelper::isIn('a', ['a']); // true ArrayHelper::isIn('a', new ArrayObject(['a'])); // true ArrayHelper::isSubset(new ArrayObject(['a', 'c']), new ArrayObject(['a', 'b', 'c'])); ``` ## Flattening Arrays The `ArrayHelper::flatten()` method allows you to convert a multi-dimensional array into a single-dimensional array by concatenating keys. ### Basic Usage To flatten a nested array, simply pass the array to the `flatten()` method: ```php $array = [ 'a' => [ 'b' => [ 'c' => 1, 'd' => 2, ], 'e' => 3, ], 'f' => 4, ]; $flattenedArray = ArrayHelper::flatten($array); // Result: // [ // 'a.b.c' => 1, // 'a.b.d' => 2, // 'a.e' => 3, // 'f' => 4, // ] ``` ### Custom Separator You can specify a custom separator to use when concatenating keys: ```php $array = [ 'a' => [ 'b' => [ 'c' => 1, 'd' => 2, ], 'e' => 3, ], 'f' => 4, ]; $flattenedArray = ArrayHelper::flatten($array, '_'); // Result: // [ // 'a_b_c' => 1, // 'a_b_d' => 2, // 'a_e' => 3, // 'f' => 4, // ] ``` ### Handling Special Characters in Keys The `flatten()` method can handle keys with special characters: ```php $array = [ 'a.b' => [ 'c.d' => 1, ], 'e.f' => 2, ]; $flattenedArray = ArrayHelper::flatten($array); // Result: // [ // 'a.b.c.d' => 1, // 'e.f' => 2, // ] ``` ### Mixed Data Types The `flatten()` method works with arrays containing different data types: ```php $array = [ 'a' => [ 'b' => 'string', 'c' => 123, 'd' => true, 'e' => null, ], 'f' => [1, 2, 3], ]; $flattenedArray = ArrayHelper::flatten($array); // Result: // [ // 'a.b' => 'string', // 'a.c' => 123, // 'a.d' => true, // 'a.e' => null, // 'f.0' => 1, // 'f.1' => 2, // 'f.2' => 3, // ] ``` ### Edge Cases The `flatten()` method handles various edge cases, such as empty arrays and non-array values: ```php // Empty array $array = []; $flattenedArray = ArrayHelper::flatten($array); // Result: [] // Non-array value $array = 'string'; $flattenedArray = ArrayHelper::flatten($array); // Result: // yii\base\InvalidArgumentException: Argument $array must be an array or implement Traversable ``` ### Key Collisions When keys collide, the `flatten()` method will overwrite the previous value: ```php $array = [ 'a' => [ 'b' => 1, ], 'a.b' => 2, ]; $flattenedArray = ArrayHelper::flatten($array); // Result: ['a.b' => 2] ``` ================================================ FILE: docs/guide/helper-html.md ================================================ Html helper =========== Every web application generates lots of HTML markup. If the markup is static, it can be done efficiently by [mixing PHP and HTML in a single file](https://www.php.net/manual/en/language.basic-syntax.phpmode.php), but when it is generated dynamically it starts to get tricky to handle it without extra help. Yii provides such help in the form of an Html helper, which provides a set of static methods for handling commonly used HTML tags, their options, and their content. > Note: If your markup is nearly static, it's better to use HTML directly. There's no need to wrap absolutely everything in Html helper calls. ## Basics Since building dynamic HTML by string concatenation can get messy very fast, Yii provides a set of methods to manipulate tag options and build tags based on these options. ### Generating Tags The code for generating a tag looks like the following: ```php name), ['class' => 'username']) ?> ``` The first argument is the tag name. The second one is the content to be enclosed between the start and end tags. Note that we are using `Html::encode` — that's because the content isn't encoded automatically to allow using HTML when needed. The third one is an array of HTML options, or in other words, tag attributes. In this array the key is the name of the attribute (such as `class`, `href` or `target`), and the value is its value. The code above will generate the following HTML: ```html

samdark

``` In case you need just an opening or closing tag, you can use the `Html::beginTag()` and `Html::endTag()` methods. Options are used in many methods of the Html helper and various widgets. In all these cases there is some extra handling to know about: - If a value is `null`, the corresponding attribute will not be rendered. - Attributes whose values are of boolean type will be treated as [boolean attributes](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributess). - The values of attributes will be HTML-encoded using [[yii\helpers\Html::encode()|Html::encode()]]. - If the value of an attribute is an array, it will be handled as follows: * If the attribute is a data attribute as listed in [[yii\helpers\Html::$dataAttributes]], such as `data` or `ng`, a list of attributes will be rendered, one for each element in the value array. For example, `'data' => ['id' => 1, 'name' => 'yii']` generates `data-id="1" data-name="yii"`; and `'data' => ['params' => ['id' => 1, 'name' => 'yii'], 'status' => 'ok']` generates `data-params='{"id":1,"name":"yii"}' data-status="ok"`. Note that in the latter example JSON format is used to render a sub-array. * If the attribute is NOT a data attribute, the value will be JSON-encoded. For example, `['params' => ['id' => 1, 'name' => 'yii']` generates `params='{"id":1,"name":"yii"}'`. ### Forming CSS Classes and Styles When building options for HTML tags we often start with defaults which we need to modify. In order to add or remove a CSS class you can use the following: ```php $options = ['class' => 'btn btn-default']; if ($type === 'success') { Html::removeCssClass($options, 'btn-default'); Html::addCssClass($options, 'btn-success'); } echo Html::tag('div', 'Pwede na', $options); // if the value of $type is 'success' it will render //
Pwede na
``` You may specify multiple CSS classes using the array style as well: ```php $options = ['class' => ['btn', 'btn-default']]; echo Html::tag('div', 'Save', $options); // renders '
Save
' ``` You may also use the array style when adding or removing classes: ```php $options = ['class' => 'btn']; if ($type === 'success') { Html::addCssClass($options, ['btn-success', 'btn-lg']); } echo Html::tag('div', 'Save', $options); // renders '
Save
' ``` `Html::addCssClass()` prevents duplication, so you don't need to worry about the same class appearing twice: ```php $options = ['class' => 'btn btn-default']; Html::addCssClass($options, 'btn-default'); // class 'btn-default' is already present echo Html::tag('div', 'Save', $options); // renders '
Save
' ``` If the CSS class option is specified using the array style, you may use a named key to mark the logical purpose of the class. In this case, a class with the same key in the array style will be ignored in `Html::addCssClass()`: ```php $options = [ 'class' => [ 'btn', 'theme' => 'btn-default', ] ]; Html::addCssClass($options, ['theme' => 'btn-success']); // 'theme' key is already taken echo Html::tag('div', 'Save', $options); // renders '
Save
' ``` CSS styles can be set up in similar way using the `style` attribute: ```php $options = ['style' => ['width' => '100px', 'height' => '100px']]; // gives style="width: 100px; height: 200px; position: absolute;" Html::addCssStyle($options, 'height: 200px; position: absolute;'); // gives style="position: absolute;" Html::removeCssStyle($options, ['width', 'height']); ``` When using [[yii\helpers\Html::addCssStyle()|addCssStyle()]], you can specify either an array of key-value pairs, corresponding to CSS property names and values, or a string such as `width: 100px; height: 200px;`. These formats can be converted from one to the other using [[yii\helpers\Html::cssStyleFromArray()|cssStyleFromArray()]] and [[yii\helpers\Html::cssStyleToArray()|cssStyleToArray()]]. The [[yii\helpers\Html::removeCssStyle()|removeCssStyle()]] method accepts an array of properties to remove. If it's a single property, it can be specified as a string. ### Encoding and Decoding Content In order for content to be displayed properly and securely in HTML, special characters in the content should be encoded. In PHP this is done with [htmlspecialchars](https://www.php.net/manual/en/function.htmlspecialchars.php) and [htmlspecialchars_decode](https://www.php.net/manual/en/function.htmlspecialchars-decode.php). The issue with using these methods directly is that you have to specify encoding and extra flags all the time. Since these flags are the same all the time and the encoding should match the one of the application in order to prevent security issues, Yii provides two compact and simple-to-use methods: ```php $userName = Html::encode($user->name); echo $userName; $decodedUserName = Html::decode($userName); ``` ## Forms Dealing with form markup is quite repetitive and error prone. Because of that, there is a group of methods to help dealing with them. > Note: consider using [[yii\widgets\ActiveForm|ActiveForm]] in case you're dealing with models and need validation. ### Creating Forms Forms can be opened with [[yii\helpers\Html::beginForm()|beginForm()]] method like the following: ```php $id], 'post', ['enctype' => 'multipart/form-data']) ?> ``` The first argument is the URL the form will be submitted to. It can be specified in the form of a Yii route and parameters accepted by [[yii\helpers\Url::to()|Url::to()]]. The second one is the method to use. `post` is the default. The third one is an array of options for the form tag. In this case we're changing the encoding of the form data in the POST request to `multipart/form-data`, which is required in order to upload files. Closing the form tag is simple: ```php ``` ### Buttons In order to generate buttons, you can use the following code: ```php 'teaser']) ?> 'submit']) ?> 'reset']) ?> ``` The first argument for all three methods is the button title, and the second one is an array of options. The title isn't encoded, so if you're displaying data from the end user, encode it with [[yii\helpers\Html::encode()|Html::encode()]]. ### Input Fields There are two groups of input methods. The ones starting with `active`, which are called active inputs, and the ones not starting with it. Active inputs take data from the model and attribute specified, while in the case of a regular input, data is specified directly. The most generic methods are: ```php type, input name, input value, options name, ['class' => $username]) ?> type, model, model attribute name, options $username]) ?> ``` If you know the input type in advance, it's more convenient to use the shortcut methods: - [[yii\helpers\Html::buttonInput()]] - [[yii\helpers\Html::submitInput()]] - [[yii\helpers\Html::resetInput()]] - [[yii\helpers\Html::textInput()]], [[yii\helpers\Html::activeTextInput()]] - [[yii\helpers\Html::hiddenInput()]], [[yii\helpers\Html::activeHiddenInput()]] - [[yii\helpers\Html::passwordInput()]] / [[yii\helpers\Html::activePasswordInput()]] - [[yii\helpers\Html::fileInput()]], [[yii\helpers\Html::activeFileInput()]] - [[yii\helpers\Html::textarea()]], [[yii\helpers\Html::activeTextarea()]] Radios and checkboxes are a bit different in terms of method signature: ```php 'I agree']) ?> 'agreement']) ?> 'I agree']) ?> 'agreement']) ?> ``` Dropdown lists and list boxes can be rendered like the following: ```php ``` The first argument is the name of the input, the second one is the value that's currently selected, and the third one is an array of key-value pairs, where the array key is the list value and the array value is the list label. If you want multiple choices to be selectable, you can use a checkbox list: ```php ``` If not, use radio list: ```php ``` ### Labels and Errors Same as inputs, there are two methods for generating form labels. Active, which takes data from the model, and non-active, which accepts data directly: ```php 'label username']) ?> 'label username']) ?> ``` In order to display form errors from a model or models as a summary, you could use: ```php 'errors']) ?> ``` To display an individual error: ```php 'error']) ?> ``` ### Input Names and Values There are methods to get names, ids and values for input fields based on the model. These are mainly used internally, but could be handy sometimes: ```php // Post[title] echo Html::getInputName($post, 'title'); // post-title echo Html::getInputId($post, 'title'); // my first post echo Html::getAttributeValue($post, 'title'); // $post->authors[0] echo Html::getAttributeValue($post, '[0]authors[0]'); ``` In the above, the first argument is the model, while the second one is the attribute expression. In its simplest form the expression is just an attribute name, but it can be an attribute name prefixed and/or suffixed with array indexes, which is mainly used for tabular input: - `[0]content` is used in tabular data input to represent the `content` attribute for the first model in tabular input; - `dates[0]` represents the first array element of the `dates` attribute; - `[0]dates[0]` represents the first array element of the `dates` attribute for the first model in tabular input. In order to get the attribute name without suffixes or prefixes, one can use the following: ```php // dates echo Html::getAttributeName('dates[0]'); ``` ## Styles and Scripts There are two methods to generate tags wrapping embedded styles and scripts: ```php 'print']) ?> Gives you Gives you ``` If you want to use an external style in a CSS file: ```php 'IE 5']) ?> generates ``` The first argument is the URL. The second one is an array of options. In addition to the regular options, you can specify: - `condition` to wrap `` tag so it will be included only when there's either no JavaScript support in the browser or it was disabled by the user. To link a JavaScript file: ```php ``` Same as with CSS, the first argument specifies the URL of the file to be included. Options can be passed as the second argument. In the options you can specify `condition` in the same way as in the options for `cssFile`. ## Hyperlinks There's a method to generate hyperlinks conveniently: ```php $id], ['class' => 'profile-link']) ?> ``` The first argument is the title. It's not encoded, so if you're using data entered by the user, you need to encode it with `Html::encode()`. The second argument is what will be in the `href` attribute of the ` ``` ## Images In order to generate an image tag, use the following: ```php 'My logo']) ?> generates My logo ``` Besides [aliases](concept-aliases.md), the first argument can accept routes, parameters and URLs, in the same way [Url::to()](helper-url.md) does. ## Lists Unordered list can be generated like the following: ```php function($item, $index) { return Html::tag( 'li', $this->render('post', ['item' => $item]), ['class' => 'post'] ); }]) ?> ``` In order to get ordered list, use `Html::ol()` instead. ================================================ FILE: docs/guide/helper-json.md ================================================ Json Helper ========== Json helper provides a set of static methods for encoding and decoding JSON. It handles encoding errors and the `[[yii\helpers\Json::encode()]]` method will not encode a JavaScript expression that is represented in terms of a `[[yii\web\JsExpression]]` object. By default, encoding is done with the `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE` options. Please see [PHP:json_encode](https://www.php.net/manual/en/function.json-encode.php) for more information. ## Pretty Print By default the `[[yii\helpers\Json::encode()]]` method will output unformatted JSON (e.g. without whitespaces). To make it more readable for humans you can turn on "pretty printing". > Note: Pretty Print can useful for debugging during development but is not recommended in a production environment. To enable pretty print in a single instance you can specify it as an option. E.g. ```php $data = ['a' => 1, 'b' => 2]; $json = yii\helpers\Json::encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); ``` You can also enable pretty printing of the JSON helper globally. For example in your config for or index.php: ```php yii\helpers\Json::$prettyPrint = YII_DEBUG; // use "pretty" output in debug mode ``` ================================================ FILE: docs/guide/helper-overview.md ================================================ Helpers ======= > Note: This section is under development. Yii provides many classes that help simplify common coding tasks, such as string or array manipulations, HTML code generation, and so on. These helper classes are organized under the `yii\helpers` namespace and are all static classes (meaning they contain only static properties and methods and should not be instantiated). You use a helper class by directly calling one of its static methods, like the following: ```php use yii\helpers\Html; echo Html::encode('Test > test'); ``` > Note: To support [customizing helper classes](#customizing-helper-classes), Yii breaks each core helper class into two classes: a base class (e.g. `BaseArrayHelper`) and a concrete class (e.g. `ArrayHelper`). When you use a helper, you should only use the concrete version and never use the base class. Core Helper Classes ------------------- The following core helper classes are provided in the Yii releases: - [ArrayHelper](helper-array.md) - Console - FileHelper - FormatConverter - [Html](helper-html.md) - HtmlPurifier - Imagine (provided by yii2-imagine extension) - Inflector - [Json](helper-json.md) - Markdown - StringHelper - [Url](helper-url.md) - VarDumper Customizing Helper Classes -------------------------- To customize a core helper class (e.g. [[yii\helpers\ArrayHelper]]), you should create a new class extending from the helpers corresponding base class (e.g. [[yii\helpers\BaseArrayHelper]]) and name your class the same as the corresponding concrete class (e.g. [[yii\helpers\ArrayHelper]]), including its namespace. This class will then be set up to replace the original implementation of the framework. The following example shows how to customize the [[yii\helpers\ArrayHelper::merge()|merge()]] method of the [[yii\helpers\ArrayHelper]] class: ```php There are two methods you can use to get common URLs: home URL and base URL of the current request. In order to get home URL, use the following: ```php $relativeHomeUrl = Url::home(); $absoluteHomeUrl = Url::home(true); $httpsAbsoluteHomeUrl = Url::home('https'); ``` If no parameter is passed, the generated URL is relative. You can either pass `true` to get an absolute URL for the current schema or specify a schema explicitly (`https`, `http`). To get the base URL of the current request use the following: ```php $relativeBaseUrl = Url::base(); $absoluteBaseUrl = Url::base(true); $httpsAbsoluteBaseUrl = Url::base('https'); ``` The only parameter of the method works exactly the same as for `Url::home()`. ## Creating URLs In order to create a URL to a given route use the `Url::toRoute()` method. The method uses [[\yii\web\UrlManager]] to create a URL: ```php $url = Url::toRoute(['product/view', 'id' => 42]); ``` You may specify the route as a string, e.g., `site/index`. You may also use an array if you want to specify additional query parameters for the URL being created. The array format must be: ```php // generates: /index.php?r=site%2Findex¶m1=value1¶m2=value2 ['site/index', 'param1' => 'value1', 'param2' => 'value2'] ``` If you want to create a URL with an anchor, you can use the array format with a `#` parameter. For example, ```php // generates: /index.php?r=site%2Findex¶m1=value1#name ['site/index', 'param1' => 'value1', '#' => 'name'] ``` A route may be either absolute or relative. An absolute route has a leading slash (e.g. `/site/index`) while a relative route has none (e.g. `site/index` or `index`). A relative route will be converted into an absolute one by the following rules: - If the route is an empty string, the current [[\yii\web\Controller::route|route]] will be used; - If the route contains no slashes at all (e.g. `index`), it is considered to be an action ID of the current controller and will be prepended with [[\yii\web\Controller::uniqueId]]; - If the route has no leading slash (e.g. `site/index`), it is considered to be a route relative to the current module and will be prepended with the module's [[\yii\base\Module::uniqueId|uniqueId]]. Starting from version 2.0.2, you may specify a route in terms of an [alias](concept-aliases.md). If this is the case, the alias will first be converted into the actual route which will then be turned into an absolute route according to the above rules. Below are some examples of using this method: ```php // /index.php?r=site%2Findex echo Url::toRoute('site/index'); // /index.php?r=site%2Findex&src=ref1#name echo Url::toRoute(['site/index', 'src' => 'ref1', '#' => 'name']); // /index.php?r=post%2Fedit&id=100 assume the alias "@postEdit" is defined as "post/edit" echo Url::toRoute(['@postEdit', 'id' => 100]); // https://www.example.com/index.php?r=site%2Findex echo Url::toRoute('site/index', true); // https://www.example.com/index.php?r=site%2Findex echo Url::toRoute('site/index', 'https'); ``` There's another method `Url::to()` that is very similar to [[toRoute()]]. The only difference is that this method requires a route to be specified as an array only. If a string is given, it will be treated as a URL. The first argument could be: - an array: [[toRoute()]] will be called to generate the URL. For example: `['site/index']`, `['post/index', 'page' => 2]`. Please refer to [[toRoute()]] for more details on how to specify a route. - a string with a leading `@`: it is treated as an alias, and the corresponding aliased string will be returned. - an empty string: the currently requested URL will be returned; - a normal string: it will be returned as is. When `$scheme` is specified (either a string or `true`), an absolute URL with host info (obtained from [[\yii\web\UrlManager::hostInfo]]) will be returned. If `$url` is already an absolute URL, its scheme will be replaced with the specified one. Below are some usage examples: ```php // /index.php?r=site%2Findex echo Url::to(['site/index']); // /index.php?r=site%2Findex&src=ref1#name echo Url::to(['site/index', 'src' => 'ref1', '#' => 'name']); // /index.php?r=post%2Fedit&id=100 assume the alias "@postEdit" is defined as "post/edit" echo Url::to(['@postEdit', 'id' => 100]); // the currently requested URL echo Url::to(); // /images/logo.gif echo Url::to('@web/images/logo.gif'); // images/logo.gif echo Url::to('images/logo.gif'); // https://www.example.com/images/logo.gif echo Url::to('@web/images/logo.gif', true); // https://www.example.com/images/logo.gif echo Url::to('@web/images/logo.gif', 'https'); ``` Starting from version 2.0.3, you may use [[yii\helpers\Url::current()]] to create a URL based on the currently requested route and GET parameters. You may modify or remove some of the GET parameters or add new ones by passing a `$params` parameter to the method. For example, ```php // assume $_GET = ['id' => 123, 'src' => 'google'], current route is "post/view" // /index.php?r=post%2Fview&id=123&src=google echo Url::current(); // /index.php?r=post%2Fview&id=123 echo Url::current(['src' => null]); // /index.php?r=post%2Fview&id=100&src=google echo Url::current(['id' => 100]); ``` ## Remember URLs There are cases when you need to remember URL and afterwards use it during processing of the one of sequential requests. It can be achieved in the following way: ```php // Remember current URL Url::remember(); // Remember URL specified. See Url::to() for argument format. Url::remember(['product/view', 'id' => 42]); // Remember URL specified with a name given Url::remember(['product/view', 'id' => 42], 'product'); ``` In the next request we can get URL remembered in the following way: ```php $url = Url::previous(); $productUrl = Url::previous('product'); ``` ## Checking Relative URLs To find out if URL is relative i.e. it doesn't have host info part, you can use the following code: ```php $isRelative = Url::isRelative('test/it'); ``` ================================================ FILE: docs/guide/images/application-lifecycle.graphml ================================================ Entry script (index.php or yii) Folder 4 Load application config Create application instance Folder 5 preInit() Register error handler Configure application properties init() bootstrap() Run application Folder 3 EVENT_BEFORE_REQUEST Handle request Folder 4 Resolve request into route and parameters Create module, controller and action Run action EVENT_AFTER_REQUEST Send response to end user Complete request processing Configuration array Exit status ================================================ FILE: docs/guide/images/application-structure.graphml ================================================ application component entry script application controller filter module view model widget asset bundle 1:1 0..* 0..* 1..* 0..* 0..* 0..* 0..* 0..* 0..* 0..* 0..* 0..* ================================================ FILE: docs/guide/images/rbac-access-check-1.graphml ================================================ admin author John, ID=2 Jane, ID=1 updatePost updateOwnPost createPost AuthorRule <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_18_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <radialGradient id="SVGID_2_" cx="22.6621" cy="21.707" r="17.7954" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#E55E03" d="M28.106,33.486c-8.112,0-12.688,4.313-12.688,10.438 c0,7.422,12.688,10.438,12.688,10.438s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.486,28.106,33.486z M26.288,53.051 c0,0-7.135-2.093-8.805-7.201c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_3_" cx="15.2056" cy="831.1875" r="32.3071" gradientTransform="matrix(1 0 0 1 0.0801 -773.6914)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_3_)" stroke="#E55E03" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.021,7.807-14.021,7.807s-10.472-2.483-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.946,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="SVGID_4_" cx="17.0723" cy="18.4907" r="11.8931" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_4_)" stroke="#E55E03" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397 c-0.514,1.027-1.669,4.084-1.669,5.148c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.334-2.359 c-3.601-1.419-4.071-3.063-5.89-4.854C12.523,47.135,12.878,45,13.404,44.173z"/> <radialGradient id="SVGID_5_" cx="31.8184" cy="19.3525" r="14.63" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_5_)" stroke="#E55E03" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617 c0.516,1.025,3.617,3.693,3.617,6.617c0,5.186-10.271,8.576-16.699,9.145c1.429,4.938,11.373,1.293,13.805-0.313 c3.563-2.354,4.563-5.133,7.854-3.705C47.754,49.045,48.006,46.574,45.777,43.924z"/> <radialGradient id="SVGID_6_" cx="30.4893" cy="4.8721" r="5.2028" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_6_)" stroke="#E55E03" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <radialGradient id="SVGID_7_" cx="23.2871" cy="5.3008" r="5.5143" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_7_)" stroke="#E55E03" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Young_Black_1_" fill="#5C5C5C" stroke="#353535" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="67px" viewBox="0 0 57 67" enable-background="new 0 0 57 67" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.199-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.5" y1="-12286.8594" x2="6492.1294" y2="-12380.3086" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3350.4617)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Female_1_Red_1_" fill="#FAE1AA" stroke="#E2B354" stroke-linecap="round" stroke-linejoin="round" d="M28.372,0.5 C17.537,0.5,8.269,7.748,9.153,26.125c0.563,6.563,5.862,12.042,9.366,13.531c-2.929-10.968-0.304-25.021-0.585-25.526 c-0.281-0.505,3.536,6.728,3.536,6.728l3.183-8.312c5.541,4.28,0.393,11.309,1.049,11.058c4.26-1.631,5.34-9.228,5.34-9.228 s2.729,3.657,2.701,5.504c-0.054,3.562,2.194-6.067,2.194-6.067l1.027,2.031c6.727,9.822,3.684,16.208,1.648,22.781 c15.666-0.703,12.291-10.48,9.66-18.407C43.59,6.092,39.206,0.5,28.372,0.5z"/> <linearGradient id="body_1_" gradientUnits="userSpaceOnUse" x1="95.9063" y1="-3134.2153" x2="31.5133" y2="-3134.2153" gradientTransform="matrix(0.9852 0 0 -0.9852 -34.4844 -3031.9851)"> <stop offset="0" style="stop-color:#49AD33"/> <stop offset="1" style="stop-color:#C2DA92"/> </linearGradient> <path id="body_8_" fill="url(#body_1_)" stroke="#008D33" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-8.244-5.146-8.244-5.146 c-1.444,6.983-8.555,8.786-13.007,8.786s-11.322-2.643-11.941-9.439c0,0-4.559,1.199-9.367,5.674 C1.378,56.689,0.5,62.768,0.5,62.768z"/> </g> </svg> ================================================ FILE: docs/guide/images/rbac-access-check-2.graphml ================================================ admin author John, ID=2 Jane, ID=1 updatePost updateOwnPost createPost AuthorRule <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_18_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <radialGradient id="SVGID_2_" cx="22.6621" cy="21.707" r="17.7954" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#E55E03" d="M28.106,33.486c-8.112,0-12.688,4.313-12.688,10.438 c0,7.422,12.688,10.438,12.688,10.438s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.486,28.106,33.486z M26.288,53.051 c0,0-7.135-2.093-8.805-7.201c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_3_" cx="15.2056" cy="831.1875" r="32.3071" gradientTransform="matrix(1 0 0 1 0.0801 -773.6914)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_3_)" stroke="#E55E03" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.021,7.807-14.021,7.807s-10.472-2.483-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.946,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="SVGID_4_" cx="17.0723" cy="18.4907" r="11.8931" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_4_)" stroke="#E55E03" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397 c-0.514,1.027-1.669,4.084-1.669,5.148c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.334-2.359 c-3.601-1.419-4.071-3.063-5.89-4.854C12.523,47.135,12.878,45,13.404,44.173z"/> <radialGradient id="SVGID_5_" cx="31.8184" cy="19.3525" r="14.63" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_5_)" stroke="#E55E03" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617 c0.516,1.025,3.617,3.693,3.617,6.617c0,5.186-10.271,8.576-16.699,9.145c1.429,4.938,11.373,1.293,13.805-0.313 c3.563-2.354,4.563-5.133,7.854-3.705C47.754,49.045,48.006,46.574,45.777,43.924z"/> <radialGradient id="SVGID_6_" cx="30.4893" cy="4.8721" r="5.2028" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_6_)" stroke="#E55E03" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <radialGradient id="SVGID_7_" cx="23.2871" cy="5.3008" r="5.5143" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_7_)" stroke="#E55E03" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Young_Black_1_" fill="#5C5C5C" stroke="#353535" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="67px" viewBox="0 0 57 67" enable-background="new 0 0 57 67" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.199-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.5" y1="-12286.8594" x2="6492.1294" y2="-12380.3086" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3350.4617)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Female_1_Red_1_" fill="#FAE1AA" stroke="#E2B354" stroke-linecap="round" stroke-linejoin="round" d="M28.372,0.5 C17.537,0.5,8.269,7.748,9.153,26.125c0.563,6.563,5.862,12.042,9.366,13.531c-2.929-10.968-0.304-25.021-0.585-25.526 c-0.281-0.505,3.536,6.728,3.536,6.728l3.183-8.312c5.541,4.28,0.393,11.309,1.049,11.058c4.26-1.631,5.34-9.228,5.34-9.228 s2.729,3.657,2.701,5.504c-0.054,3.562,2.194-6.067,2.194-6.067l1.027,2.031c6.727,9.822,3.684,16.208,1.648,22.781 c15.666-0.703,12.291-10.48,9.66-18.407C43.59,6.092,39.206,0.5,28.372,0.5z"/> <linearGradient id="body_1_" gradientUnits="userSpaceOnUse" x1="95.9063" y1="-3134.2153" x2="31.5133" y2="-3134.2153" gradientTransform="matrix(0.9852 0 0 -0.9852 -34.4844 -3031.9851)"> <stop offset="0" style="stop-color:#49AD33"/> <stop offset="1" style="stop-color:#C2DA92"/> </linearGradient> <path id="body_8_" fill="url(#body_1_)" stroke="#008D33" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-8.244-5.146-8.244-5.146 c-1.444,6.983-8.555,8.786-13.007,8.786s-11.322-2.643-11.941-9.439c0,0-4.559,1.199-9.367,5.674 C1.378,56.689,0.5,62.768,0.5,62.768z"/> </g> </svg> ================================================ FILE: docs/guide/images/rbac-access-check-3.graphml ================================================ admin author John, ID=2 Jane, ID=1 updatePost updateOwnPost createPost AuthorRule <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_18_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <radialGradient id="SVGID_2_" cx="22.6621" cy="21.707" r="17.7954" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#E55E03" d="M28.106,33.486c-8.112,0-12.688,4.313-12.688,10.438 c0,7.422,12.688,10.438,12.688,10.438s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.486,28.106,33.486z M26.288,53.051 c0,0-7.135-2.093-8.805-7.201c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_3_" cx="15.2056" cy="831.1875" r="32.3071" gradientTransform="matrix(1 0 0 1 0.0801 -773.6914)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_3_)" stroke="#E55E03" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.021,7.807-14.021,7.807s-10.472-2.483-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.946,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="SVGID_4_" cx="17.0723" cy="18.4907" r="11.8931" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_4_)" stroke="#E55E03" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397 c-0.514,1.027-1.669,4.084-1.669,5.148c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.334-2.359 c-3.601-1.419-4.071-3.063-5.89-4.854C12.523,47.135,12.878,45,13.404,44.173z"/> <radialGradient id="SVGID_5_" cx="31.8184" cy="19.3525" r="14.63" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_5_)" stroke="#E55E03" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617 c0.516,1.025,3.617,3.693,3.617,6.617c0,5.186-10.271,8.576-16.699,9.145c1.429,4.938,11.373,1.293,13.805-0.313 c3.563-2.354,4.563-5.133,7.854-3.705C47.754,49.045,48.006,46.574,45.777,43.924z"/> <radialGradient id="SVGID_6_" cx="30.4893" cy="4.8721" r="5.2028" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_6_)" stroke="#E55E03" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <radialGradient id="SVGID_7_" cx="23.2871" cy="5.3008" r="5.5143" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_7_)" stroke="#E55E03" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Young_Black_1_" fill="#5C5C5C" stroke="#353535" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="67px" viewBox="0 0 57 67" enable-background="new 0 0 57 67" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.199-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.5" y1="-12286.8594" x2="6492.1294" y2="-12380.3086" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3350.4617)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Female_1_Red_1_" fill="#FAE1AA" stroke="#E2B354" stroke-linecap="round" stroke-linejoin="round" d="M28.372,0.5 C17.537,0.5,8.269,7.748,9.153,26.125c0.563,6.563,5.862,12.042,9.366,13.531c-2.929-10.968-0.304-25.021-0.585-25.526 c-0.281-0.505,3.536,6.728,3.536,6.728l3.183-8.312c5.541,4.28,0.393,11.309,1.049,11.058c4.26-1.631,5.34-9.228,5.34-9.228 s2.729,3.657,2.701,5.504c-0.054,3.562,2.194-6.067,2.194-6.067l1.027,2.031c6.727,9.822,3.684,16.208,1.648,22.781 c15.666-0.703,12.291-10.48,9.66-18.407C43.59,6.092,39.206,0.5,28.372,0.5z"/> <linearGradient id="body_1_" gradientUnits="userSpaceOnUse" x1="95.9063" y1="-3134.2153" x2="31.5133" y2="-3134.2153" gradientTransform="matrix(0.9852 0 0 -0.9852 -34.4844 -3031.9851)"> <stop offset="0" style="stop-color:#49AD33"/> <stop offset="1" style="stop-color:#C2DA92"/> </linearGradient> <path id="body_8_" fill="url(#body_1_)" stroke="#008D33" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-8.244-5.146-8.244-5.146 c-1.444,6.983-8.555,8.786-13.007,8.786s-11.322-2.643-11.941-9.439c0,0-4.559,1.199-9.367,5.674 C1.378,56.689,0.5,62.768,0.5,62.768z"/> </g> </svg> ================================================ FILE: docs/guide/images/rbac-hierarchy-1.graphml ================================================ admin author John, ID=2 Jane, ID=1 updatePost createPost <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_18_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <radialGradient id="SVGID_2_" cx="22.6621" cy="21.707" r="17.7954" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#E55E03" d="M28.106,33.486c-8.112,0-12.688,4.313-12.688,10.438 c0,7.422,12.688,10.438,12.688,10.438s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.486,28.106,33.486z M26.288,53.051 c0,0-7.135-2.093-8.805-7.201c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_3_" cx="15.2056" cy="831.1875" r="32.3071" gradientTransform="matrix(1 0 0 1 0.0801 -773.6914)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_3_)" stroke="#E55E03" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.021,7.807-14.021,7.807s-10.472-2.483-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.946,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="SVGID_4_" cx="17.0723" cy="18.4907" r="11.8931" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_4_)" stroke="#E55E03" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397 c-0.514,1.027-1.669,4.084-1.669,5.148c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.334-2.359 c-3.601-1.419-4.071-3.063-5.89-4.854C12.523,47.135,12.878,45,13.404,44.173z"/> <radialGradient id="SVGID_5_" cx="31.8184" cy="19.3525" r="14.63" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_5_)" stroke="#E55E03" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617 c0.516,1.025,3.617,3.693,3.617,6.617c0,5.186-10.271,8.576-16.699,9.145c1.429,4.938,11.373,1.293,13.805-0.313 c3.563-2.354,4.563-5.133,7.854-3.705C47.754,49.045,48.006,46.574,45.777,43.924z"/> <radialGradient id="SVGID_6_" cx="30.4893" cy="4.8721" r="5.2028" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_6_)" stroke="#E55E03" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <radialGradient id="SVGID_7_" cx="23.2871" cy="5.3008" r="5.5143" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_7_)" stroke="#E55E03" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Young_Black_1_" fill="#5C5C5C" stroke="#353535" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="67px" viewBox="0 0 57 67" enable-background="new 0 0 57 67" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.199-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.5" y1="-12286.8594" x2="6492.1294" y2="-12380.3086" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3350.4617)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Female_1_Red_1_" fill="#FAE1AA" stroke="#E2B354" stroke-linecap="round" stroke-linejoin="round" d="M28.372,0.5 C17.537,0.5,8.269,7.748,9.153,26.125c0.563,6.563,5.862,12.042,9.366,13.531c-2.929-10.968-0.304-25.021-0.585-25.526 c-0.281-0.505,3.536,6.728,3.536,6.728l3.183-8.312c5.541,4.28,0.393,11.309,1.049,11.058c4.26-1.631,5.34-9.228,5.34-9.228 s2.729,3.657,2.701,5.504c-0.054,3.562,2.194-6.067,2.194-6.067l1.027,2.031c6.727,9.822,3.684,16.208,1.648,22.781 c15.666-0.703,12.291-10.48,9.66-18.407C43.59,6.092,39.206,0.5,28.372,0.5z"/> <linearGradient id="body_1_" gradientUnits="userSpaceOnUse" x1="95.9063" y1="-3134.2153" x2="31.5133" y2="-3134.2153" gradientTransform="matrix(0.9852 0 0 -0.9852 -34.4844 -3031.9851)"> <stop offset="0" style="stop-color:#49AD33"/> <stop offset="1" style="stop-color:#C2DA92"/> </linearGradient> <path id="body_8_" fill="url(#body_1_)" stroke="#008D33" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-8.244-5.146-8.244-5.146 c-1.444,6.983-8.555,8.786-13.007,8.786s-11.322-2.643-11.941-9.439c0,0-4.559,1.199-9.367,5.674 C1.378,56.689,0.5,62.768,0.5,62.768z"/> </g> </svg> ================================================ FILE: docs/guide/images/rbac-hierarchy-2.graphml ================================================ admin author John, ID=2 Jane, ID=1 updatePost updateOwnPost createPost AuthorRule <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_18_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <radialGradient id="SVGID_2_" cx="22.6621" cy="21.707" r="17.7954" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#E55E03" d="M28.106,33.486c-8.112,0-12.688,4.313-12.688,10.438 c0,7.422,12.688,10.438,12.688,10.438s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.486,28.106,33.486z M26.288,53.051 c0,0-7.135-2.093-8.805-7.201c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_3_" cx="15.2056" cy="831.1875" r="32.3071" gradientTransform="matrix(1 0 0 1 0.0801 -773.6914)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_3_)" stroke="#E55E03" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.021,7.807-14.021,7.807s-10.472-2.483-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.946,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="SVGID_4_" cx="17.0723" cy="18.4907" r="11.8931" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_4_)" stroke="#E55E03" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397 c-0.514,1.027-1.669,4.084-1.669,5.148c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.334-2.359 c-3.601-1.419-4.071-3.063-5.89-4.854C12.523,47.135,12.878,45,13.404,44.173z"/> <radialGradient id="SVGID_5_" cx="31.8184" cy="19.3525" r="14.63" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_5_)" stroke="#E55E03" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617 c0.516,1.025,3.617,3.693,3.617,6.617c0,5.186-10.271,8.576-16.699,9.145c1.429,4.938,11.373,1.293,13.805-0.313 c3.563-2.354,4.563-5.133,7.854-3.705C47.754,49.045,48.006,46.574,45.777,43.924z"/> <radialGradient id="SVGID_6_" cx="30.4893" cy="4.8721" r="5.2028" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_6_)" stroke="#E55E03" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <radialGradient id="SVGID_7_" cx="23.2871" cy="5.3008" r="5.5143" gradientTransform="matrix(1 0 0 -1 0.04 64.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FCB57A"/> <stop offset="1" style="stop-color:#FF8C36"/> </radialGradient> <path fill="url(#SVGID_7_)" stroke="#E55E03" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Young_Black_1_" fill="#5C5C5C" stroke="#353535" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="67px" viewBox="0 0 57 67" enable-background="new 0 0 57 67" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.199-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.5" y1="-12286.8594" x2="6492.1294" y2="-12380.3086" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3350.4617)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> <path id="Hair_Female_1_Red_1_" fill="#FAE1AA" stroke="#E2B354" stroke-linecap="round" stroke-linejoin="round" d="M28.372,0.5 C17.537,0.5,8.269,7.748,9.153,26.125c0.563,6.563,5.862,12.042,9.366,13.531c-2.929-10.968-0.304-25.021-0.585-25.526 c-0.281-0.505,3.536,6.728,3.536,6.728l3.183-8.312c5.541,4.28,0.393,11.309,1.049,11.058c4.26-1.631,5.34-9.228,5.34-9.228 s2.729,3.657,2.701,5.504c-0.054,3.562,2.194-6.067,2.194-6.067l1.027,2.031c6.727,9.822,3.684,16.208,1.648,22.781 c15.666-0.703,12.291-10.48,9.66-18.407C43.59,6.092,39.206,0.5,28.372,0.5z"/> <linearGradient id="body_1_" gradientUnits="userSpaceOnUse" x1="95.9063" y1="-3134.2153" x2="31.5133" y2="-3134.2153" gradientTransform="matrix(0.9852 0 0 -0.9852 -34.4844 -3031.9851)"> <stop offset="0" style="stop-color:#49AD33"/> <stop offset="1" style="stop-color:#C2DA92"/> </linearGradient> <path id="body_8_" fill="url(#body_1_)" stroke="#008D33" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-8.244-5.146-8.244-5.146 c-1.444,6.983-8.555,8.786-13.007,8.786s-11.322-2.643-11.941-9.439c0,0-4.559,1.199-9.367,5.674 C1.378,56.689,0.5,62.768,0.5,62.768z"/> </g> </svg> ================================================ FILE: docs/guide/images/request-lifecycle.graphml ================================================ user model database view controller Folder 1 create action perform filters action Folder 3 load model render view response component request component application Folder 2 resolve route create controller entry script Folder 4 load app config run application 11 3 1 4 9 10 2 8 6 5 7 <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="57px" height="66px" viewBox="0 0 57 66" enable-background="new 0 0 57 66" xml:space="preserve"> <g> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3799" y1="-2276.8809" x2="27.6209" y2="-2306.6792" gradientTransform="matrix(1 0 0 -1 0.2803 -2252.9199)"> <stop offset="0.2711" style="stop-color:#FFAB4F"/> <stop offset="1" style="stop-color:#FFD28F"/> </linearGradient> <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path id="body_13_" fill="#ECECEC" stroke="#9B9B9B" stroke-miterlimit="10" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494h48.51 c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 c-1.771,1.655-5.61,3.802-10.063,3.802c-4.453,0-8.292-2.146-10.063-3.802c0,0-5.755,0.586-11.189,6.021 C1.378,56.689,0.5,62.768,0.5,62.768z"/> <path fill="#2068A3" stroke="#2068A3" d="M28.106,33.487c-8.112,0-12.688,4.312-12.688,10.437c0,7.422,12.688,10.438,12.688,10.438 s14.688-3.016,14.688-10.438C42.793,38.75,36.215,33.487,28.106,33.487z M26.288,53.051c0,0-7.135-2.093-8.805-7.201 c-0.222-0.682,0.147-1.156,0.795-1.521V37.8h20.188v6.663c0.235,0.352,1.109,0.737,1.229,1.387 C40.445,49.917,26.288,53.051,26.288,53.051z"/> <radialGradient id="SVGID_2_" cx="14.2417" cy="9.1006" r="53.247" gradientTransform="matrix(1 0 0 -1 0.04 65.1543)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#74AEEE"/> <stop offset="1" style="stop-color:#2068A3"/> </radialGradient> <path fill="url(#SVGID_2_)" stroke="#2068A3" stroke-miterlimit="10" d="M49.529,51.225c-2.239-2.24-5.041-3.724-7.396-4.67 c-2.854,5.51-14.022,7.807-14.022,7.807s-10.472-2.484-12.387-8.514c-2.439,0.771-5.787,2.287-8.749,5.25 c-5.592,5.592-6.47,11.67-6.47,11.67c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492 C56.055,62.768,54.211,55.906,49.529,51.225z"/> <path fill="#5491CF" stroke="#2068A3" d="M13.404,44.173c1.15-1.81,2.039-3.832,3.332-5.397c-0.514,1.027-1.669,4.084-1.669,5.148 c0,5.186,10.366,9.079,14.688,10.438c-3.472,1.627-9.134-1.498-11.335-2.36c-3.601-1.419-4.071-3.063-5.89-4.854 C12.523,47.135,12.878,45,13.404,44.173z"/> <path fill="#5491CF" stroke="#2068A3" d="M45.777,43.924c-1.317-1.568-5.11-9.424-6.604-6.617c0.516,1.025,3.617,3.693,3.617,6.617 c0,5.186-10.27,8.576-16.698,9.145c1.429,4.938,11.372,1.293,13.804-0.313c3.563-2.354,4.563-5.133,7.854-3.705 C47.754,49.045,48.006,46.574,45.777,43.924z"/> <path fill="none" stroke="#2068A3" stroke-linecap="round" d="M30.777,54.167c0.357,0.836-0.153,1.983-0.352,2.813 c-0.256,1.084-0.072,2.104,0.102,3.186c0.164,1.02,0.156,2.107,0.25,3.167c0.082,0.916,0.482,1.849,0.357,2.75"/> <path fill="none" stroke="#2068A3" stroke-linecap="round" d="M23.695,53.417c-0.508,0.584-0.476,2.209-0.398,3 c0.116,1.183,0.456,2.099,0.333,3.333c-0.192,1.943,0.154,4.479-0.436,6.333"/> <radialGradient id="face_x5F_white_1_" cx="27.623" cy="-2278.646" r="23.425" fx="23.0534" fy="-2281.1357" gradientTransform="matrix(1 0 0 -1 0.2803 -2252.9199)" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FFD28F"/> <stop offset="1" style="stop-color:#FFAB4F"/> </radialGradient> <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 C36.627,4.945,43.59,13.158,43.676,23.357z"/> <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="5761.7578" y1="11330.6484" x2="5785.3872" y2="11424.0977" gradientTransform="matrix(0.275 0 0 0.2733 -1558.9874 -3088.4209)"> <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> </linearGradient> <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M27.958,6.333c-6.035,0.047-10.747,4.493-12.787,10.386 c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.083,13.952,36.271,6.268,27.958,6.333z"/> <path id="Hair_Young_Brown_1_" fill="#CC9869" stroke="#99724F" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 c3.417,4.333,9.333,6.917,9.333,6.917l-1.417-3.5c0,0,7.094,4.691,8.083,4.333c0.968-0.2-1.082-3.807-1.082-3.807 s3.138,1.795,4.854,3.969c1.803,2.28,4.285,3.504,4.285,3.504S47.027,2.719,27.289,2.744C8.278,2.709,12.058,27.678,12.058,27.678 L14.695,17c0,0,0.914,5.757,1.399,4.875C17.861,15.211,18.861,11.5,20.278,13.25z"/> <path fill="#4B4B4B" stroke="#4B4B4B" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M28.105,2 C22.464,2,20.2,4.246,18.13,5.533C29.753,2.865,41.152,10.375,44.46,20.5C44.459,16.875,44.459,2,28.105,2z"/> <path fill="#9B9B9B" stroke="#4B4B4B" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M11.151,17.751 C12.878,8.25,18.686,6.309,25.273,7.127C31.295,7.875,36.93,10.491,44.459,20.5C37.777,7.125,20.278-3.375,9.903,3.921 C5.569,6.97,4.903,13.375,11.151,17.751z"/> </g> </svg> <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="41px" height="48px" viewBox="-0.875 -0.887 41 48" enable-background="new -0.875 -0.887 41 48" xml:space="preserve"> <defs> </defs> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-979.1445" x2="682.0508" y2="-979.1445" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#3C89C9"/> <stop offset="0.1482" style="stop-color:#60A6DD"/> <stop offset="0.3113" style="stop-color:#81C1F0"/> <stop offset="0.4476" style="stop-color:#95D1FB"/> <stop offset="0.5394" style="stop-color:#9CD7FF"/> <stop offset="0.636" style="stop-color:#98D4FD"/> <stop offset="0.7293" style="stop-color:#8DCAF6"/> <stop offset="0.8214" style="stop-color:#79BBEB"/> <stop offset="0.912" style="stop-color:#5EA5DC"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_1_)" d="M19.625,36.763C8.787,36.763,0,34.888,0,32.575v10c0,2.313,8.787,4.188,19.625,4.188 c10.839,0,19.625-1.875,19.625-4.188v-10C39.25,34.888,30.464,36.763,19.625,36.763z"/> <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-973.1445" x2="682.0508" y2="-973.1445" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#9CD7FF"/> <stop offset="0.0039" style="stop-color:#9DD7FF"/> <stop offset="0.2273" style="stop-color:#BDE5FF"/> <stop offset="0.4138" style="stop-color:#D1EEFF"/> <stop offset="0.5394" style="stop-color:#D9F1FF"/> <stop offset="0.6155" style="stop-color:#D5EFFE"/> <stop offset="0.6891" style="stop-color:#C9E7FA"/> <stop offset="0.7617" style="stop-color:#B6DAF3"/> <stop offset="0.8337" style="stop-color:#9AC8EA"/> <stop offset="0.9052" style="stop-color:#77B0DD"/> <stop offset="0.9754" style="stop-color:#4D94CF"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_2_)" d="M19.625,36.763c10.839,0,19.625-1.875,19.625-4.188l-1.229-2c0,2.168-8.235,3.927-18.396,3.927 c-9.481,0-17.396-1.959-18.396-3.927l-1.229,2C0,34.888,8.787,36.763,19.625,36.763z"/> <path fill="#3C89C9" d="M19.625,26.468c10.16,0,19.625,2.775,19.625,2.775c-0.375,2.721-5.367,5.438-19.554,5.438 c-12.125,0-18.467-2.484-19.541-4.918C-0.127,29.125,9.465,26.468,19.625,26.468z"/> <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-965.6948" x2="682.0508" y2="-965.6948" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#3C89C9"/> <stop offset="0.1482" style="stop-color:#60A6DD"/> <stop offset="0.3113" style="stop-color:#81C1F0"/> <stop offset="0.4476" style="stop-color:#95D1FB"/> <stop offset="0.5394" style="stop-color:#9CD7FF"/> <stop offset="0.636" style="stop-color:#98D4FD"/> <stop offset="0.7293" style="stop-color:#8DCAF6"/> <stop offset="0.8214" style="stop-color:#79BBEB"/> <stop offset="0.912" style="stop-color:#5EA5DC"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_3_)" d="M19.625,23.313C8.787,23.313,0,21.438,0,19.125v10c0,2.313,8.787,4.188,19.625,4.188 c10.839,0,19.625-1.875,19.625-4.188v-10C39.25,21.438,30.464,23.313,19.625,23.313z"/> <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-959.6948" x2="682.0508" y2="-959.6948" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#9CD7FF"/> <stop offset="0.0039" style="stop-color:#9DD7FF"/> <stop offset="0.2273" style="stop-color:#BDE5FF"/> <stop offset="0.4138" style="stop-color:#D1EEFF"/> <stop offset="0.5394" style="stop-color:#D9F1FF"/> <stop offset="0.6155" style="stop-color:#D5EFFE"/> <stop offset="0.6891" style="stop-color:#C9E7FA"/> <stop offset="0.7617" style="stop-color:#B6DAF3"/> <stop offset="0.8337" style="stop-color:#9AC8EA"/> <stop offset="0.9052" style="stop-color:#77B0DD"/> <stop offset="0.9754" style="stop-color:#4D94CF"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_4_)" d="M19.625,23.313c10.839,0,19.625-1.875,19.625-4.188l-1.229-2c0,2.168-8.235,3.926-18.396,3.926 c-9.481,0-17.396-1.959-18.396-3.926l-1.229,2C0,21.438,8.787,23.313,19.625,23.313z"/> <path fill="#3C89C9" d="M19.476,13.019c10.161,0,19.625,2.775,19.625,2.775c-0.375,2.721-5.367,5.438-19.555,5.438 c-12.125,0-18.467-2.485-19.541-4.918C-0.277,15.674,9.316,13.019,19.476,13.019z"/> <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-952.4946" x2="682.0508" y2="-952.4946" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#3C89C9"/> <stop offset="0.1482" style="stop-color:#60A6DD"/> <stop offset="0.3113" style="stop-color:#81C1F0"/> <stop offset="0.4476" style="stop-color:#95D1FB"/> <stop offset="0.5394" style="stop-color:#9CD7FF"/> <stop offset="0.636" style="stop-color:#98D4FD"/> <stop offset="0.7293" style="stop-color:#8DCAF6"/> <stop offset="0.8214" style="stop-color:#79BBEB"/> <stop offset="0.912" style="stop-color:#5EA5DC"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_5_)" d="M19.625,10.113C8.787,10.113,0,8.238,0,5.925v10c0,2.313,8.787,4.188,19.625,4.188 c10.839,0,19.625-1.875,19.625-4.188v-10C39.25,8.238,30.464,10.113,19.625,10.113z"/> <linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="642.8008" y1="-946.4946" x2="682.0508" y2="-946.4946" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#9CD7FF"/> <stop offset="0.0039" style="stop-color:#9DD7FF"/> <stop offset="0.2273" style="stop-color:#BDE5FF"/> <stop offset="0.4138" style="stop-color:#D1EEFF"/> <stop offset="0.5394" style="stop-color:#D9F1FF"/> <stop offset="0.6155" style="stop-color:#D5EFFE"/> <stop offset="0.6891" style="stop-color:#C9E7FA"/> <stop offset="0.7617" style="stop-color:#B6DAF3"/> <stop offset="0.8337" style="stop-color:#9AC8EA"/> <stop offset="0.9052" style="stop-color:#77B0DD"/> <stop offset="0.9754" style="stop-color:#4D94CF"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <path fill="url(#SVGID_6_)" d="M19.625,10.113c10.839,0,19.625-1.875,19.625-4.188l-1.229-2c0,2.168-8.235,3.926-18.396,3.926 c-9.481,0-17.396-1.959-18.396-3.926L0,5.925C0,8.238,8.787,10.113,19.625,10.113z"/> <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="644.0293" y1="-943.4014" x2="680.8223" y2="-943.4014" gradientTransform="matrix(1 0 0 -1 -642.8008 -939.4756)"> <stop offset="0" style="stop-color:#9CD7FF"/> <stop offset="1" style="stop-color:#3C89C9"/> </linearGradient> <ellipse fill="url(#SVGID_7_)" cx="19.625" cy="3.926" rx="18.396" ry="3.926"/> <path opacity="0.24" fill="#FFFFFF" enable-background="new " d="M31.04,45.982c0,0-4.354,0.664-7.29,0.781 c-3.125,0.125-8.952,0-8.952,0l-2.384-10.292l0.044-2.108l-1.251-1.154L9.789,23.024l-0.082-0.119L9.5,20.529l-1.65-1.254 L5.329,8.793c0,0,4.213,0.903,7.234,1.07s8.375,0.25,8.375,0.25l3,9.875l-0.25,1.313l1.063,2.168l2.312,9.645l-0.521,1.416 l1.46,1.834L31.04,45.982z"/> </svg> ================================================ FILE: docs/guide/input-file-upload.md ================================================ Uploading Files =============== Uploading files in Yii is usually done with the help of [[yii\web\UploadedFile]] which encapsulates each uploaded file as an `UploadedFile` object. Combined with [[yii\widgets\ActiveForm]] and [models](structure-models.md), you can easily implement a secure file uploading mechanism. ## Creating Models Like working with plain text inputs, to upload a single file you would create a model class and use an attribute of the model to keep the uploaded file instance. You should also declare a validation rule to validate the file upload. For example, ```php namespace app\models; use yii\base\Model; use yii\web\UploadedFile; class UploadForm extends Model { /** * @var UploadedFile */ public $imageFile; public function rules() { return [ [['imageFile'], 'file', 'skipOnEmpty' => false, 'extensions' => 'png, jpg'], ]; } public function upload() { if ($this->validate()) { $this->imageFile->saveAs('uploads/' . $this->imageFile->baseName . '.' . $this->imageFile->extension); return true; } else { return false; } } } ``` In the code above, the `imageFile` attribute is used to keep the uploaded file instance. It is associated with a `file` validation rule which uses [[yii\validators\FileValidator]] to ensure a file with extension name `png` or `jpg` is uploaded. The `upload()` method will perform the validation and save the uploaded file on the server. The `file` validator allows you to check file extensions, size, MIME type, etc. Please refer to the [Core Validators](tutorial-core-validators.md#file) section for more details. > Tip: If you are uploading an image, you may consider using the `image` validator instead. The `image` validator is implemented via [[yii\validators\ImageValidator]] which verifies if an attribute has received a valid image that can be then either saved or processed using the [Imagine Extension](https://github.com/yiisoft/yii2-imagine). ## Rendering File Input Next, create a file input in a view: ```php ['enctype' => 'multipart/form-data']]) ?> field($model, 'imageFile')->fileInput() ?> ``` It is important to remember that you add the `enctype` option to the form so that the file can be properly uploaded. The `fileInput()` call will render a `` tag which will allow users to select a file to upload. > Tip: since version 2.0.8, [[yii\widgets\ActiveField::fileInput|fileInput]] adds `enctype` option to the form automatically when file input field is used. ## Wiring Up Now in a controller action, write the code to wire up the model and the view to implement file uploading: ```php namespace app\controllers; use Yii; use yii\web\Controller; use app\models\UploadForm; use yii\web\UploadedFile; class SiteController extends Controller { public function actionUpload() { $model = new UploadForm(); if (Yii::$app->request->isPost) { $model->imageFile = UploadedFile::getInstance($model, 'imageFile'); if ($model->upload()) { // file is uploaded successfully return; } } return $this->render('upload', ['model' => $model]); } } ``` In the above code, when the form is submitted, the [[yii\web\UploadedFile::getInstance()]] method is called to represent the uploaded file as an `UploadedFile` instance. We then rely on the model validation to make sure the uploaded file is valid and save the file on the server. ## Uploading Multiple Files You can also upload multiple files at once, with some adjustments to the code listed in the previous subsections. First you should adjust the model class by adding the `maxFiles` option in the `file` validation rule to limit the maximum number of files allowed to upload. Setting `maxFiles` to `0` means there is no limit on the number of files that can be uploaded simultaneously. The maximum number of files allowed to be uploaded simultaneously is also limited with PHP directive [`max_file_uploads`](https://www.php.net/manual/en/ini.core.php#ini.max-file-uploads), which defaults to 20. The `upload()` method should also be updated to save the uploaded files one by one. ```php namespace app\models; use yii\base\Model; use yii\web\UploadedFile; class UploadForm extends Model { /** * @var UploadedFile[] */ public $imageFiles; public function rules() { return [ [['imageFiles'], 'file', 'skipOnEmpty' => false, 'extensions' => 'png, jpg', 'maxFiles' => 4], ]; } public function upload() { if ($this->validate()) { foreach ($this->imageFiles as $file) { $file->saveAs('uploads/' . $file->baseName . '.' . $file->extension); } return true; } else { return false; } } } ``` In the view file, you should add the `multiple` option to the `fileInput()` call so that the file upload field can receive multiple files. You also need to change `imageFiles` to `imageFiles[]` so that the attribute values are submitted as an array: ```php ['enctype' => 'multipart/form-data']]) ?> field($model, 'imageFiles[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?> ``` And finally in the controller action, you should call `UploadedFile::getInstances()` instead of `UploadedFile::getInstance()` to assign an array of `UploadedFile` instances to `UploadForm::imageFiles`. ```php namespace app\controllers; use Yii; use yii\web\Controller; use app\models\UploadForm; use yii\web\UploadedFile; class SiteController extends Controller { public function actionUpload() { $model = new UploadForm(); if (Yii::$app->request->isPost) { $model->imageFiles = UploadedFile::getInstances($model, 'imageFiles'); if ($model->upload()) { // file is uploaded successfully return; } } return $this->render('upload', ['model' => $model]); } } ``` ================================================ FILE: docs/guide/input-form-javascript.md ================================================ Extending ActiveForm on the Client Side ======================================= The [[yii\widgets\ActiveForm]] widget comes with a set of JavaScript methods that are used for client validation. Its implementation is very flexible and allows you to extend it in different ways. In the following these are described. ## ActiveForm events ActiveForm triggers a series of dedicated events. Using the code like the following you can subscribe to these events and handle them: ```javascript $('#contact-form').on('beforeSubmit', function (e) { if (!confirm("Everything is correct. Submit?")) { return false; } return true; }); ``` In the following we'll review events available. ### `beforeValidate` `beforeValidate` is triggered before validating the whole form. The signature of the event handler should be: ```javascript function (event, messages, deferreds) ``` where - `event`: an Event object. - `messages`: an associative array with keys being attribute IDs and values being error message arrays for the corresponding attributes. - `deferreds`: an array of Deferred objects. You can use `deferreds.add(callback)` to add a new deferred validation. If the handler returns a boolean `false`, it will stop further form validation after this event. And as a result, `afterValidate` event will not be triggered. ### `afterValidate` `afterValidate` event is triggered after validating the whole form. The signature of the event handler should be: ```javascript function (event, messages, errorAttributes) ``` where - `event`: an Event object. - `messages`: an associative array with keys being attribute IDs and values being error message arrays for the corresponding attributes. - `errorAttributes`: an array of attributes that have validation errors. Please refer to `attributeDefaults` for the structure of this parameter. ### `beforeValidateAttribute` `beforeValidateAttribute` event is triggered before validating an attribute. The signature of the event handler should be: ```javascript function (event, attribute, messages, deferreds) ``` where - `event`: an Event object. - `attribute`: the attribute to be validated. Please refer to `attributeDefaults` for the structure of this parameter. - `messages`: an array to which you can add validation error messages for the specified attribute. - `deferreds`: an array of Deferred objects. You can use `deferreds.add(callback)` to add a new deferred validation. If the handler returns a boolean `false`, it will stop further validation of the specified attribute. And as a result, `afterValidateAttribute` event will not be triggered. ### `afterValidateAttribute` `afterValidateAttribute` event is triggered after validating the whole form and each attribute. The signature of the event handler should be: ```javascript function (event, attribute, messages) ``` where - `event`: an Event object. - `attribute`: the attribute being validated. Please refer to `attributeDefaults` for the structure of this parameter. - `messages`: an array to which you can add additional validation error messages for the specified attribute. ### `beforeSubmit` `beforeSubmit` event is triggered before submitting the form after all validations have passed. The signature of the event handler should be: ```javascript function (event) ``` where event is an Event object. If the handler returns a boolean `false`, it will stop form submission. ### `ajaxBeforeSend` `ajaxBeforeSend` event is triggered before sending an AJAX request for AJAX-based validation. The signature of the event handler should be: ```javascript function (event, jqXHR, settings) ``` where - `event`: an Event object. - `jqXHR`: a jqXHR object - `settings`: the settings for the AJAX request ### `ajaxComplete` `ajaxComplete` event is triggered after completing an AJAX request for AJAX-based validation. The signature of the event handler should be: ```javascript function (event, jqXHR, textStatus) ``` where - `event`: an Event object. - `jqXHR`: a jqXHR object - `textStatus`: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror"). ## Submitting the form via AJAX While validation can be made on client side or via AJAX request, the form submission itself is done as a normal request by default. If you want the form to be submitted via AJAX, you can achieve this by handling the `beforeSubmit` event of the form in the following way: ```javascript var $form = $('#formId'); $form.on('beforeSubmit', function() { var data = $form.serialize(); $.ajax({ url: $form.attr('action'), type: 'POST', data: data, success: function (data) { // Implement successful }, error: function(jqXHR, errMsg) { alert(errMsg); } }); return false; // prevent default submit }); ``` To learn more about the jQuery `ajax()` function, please refer to the [jQuery documentation](https://api.jquery.com/jQuery.ajax/). ## Adding fields dynamically In modern web applications you often have the need of changing a form after it has been displayed to the user. This can for example be the addition of new fields after click on a "plus"-icon. To enable client validation for these fields, they have to be registered with the ActiveForm JavaScript plugin. You have to add a field itself and then add it to validation list: ```javascript $('#contact-form').yiiActiveForm('add', { id: 'address', name: 'address', container: '.field-address', input: '#address', error: '.help-block', validate: function (attribute, value, messages, deferred, $form) { yii.validation.required(value, messages, {message: "Validation Message Here"}); } }); ``` To remove a field from validation list so it's not validated you can do the following: ```javascript $('#contact-form').yiiActiveForm('remove', 'address'); ``` ================================================ FILE: docs/guide/input-forms.md ================================================ Creating Forms ============== ActiveRecord based forms: ActiveForm ----------------------- The primary way of using forms in Yii is through [[yii\widgets\ActiveForm]]. This approach should be preferred when the form is based upon a model. Additionally, there are some useful methods in [[yii\helpers\Html]] that are typically used for adding buttons and help text to any form. A form, that is displayed on the client-side, will in most cases have a corresponding [model](structure-models.md) which is used to validate its input on the server-side (Check the [Validating Input](input-validation.md) section for more details on validation). When creating model-based forms, the first step is to define the model itself. The model can be either based upon an [Active Record](db-active-record.md) class, representing some data from the database, or a generic Model class (extending from [[yii\base\Model]]) to capture arbitrary input, for example a login form. > Tip: If the form fields are different from database columns or there are formatting and logic that is specific to that > form only, prefer creating a separate model extended from [[yii\base\Model]]. In the following example, we show how a generic model can be used for a login form: ```php 'login-form', 'options' => ['class' => 'form-horizontal'], ]) ?> field($model, 'username') ?> field($model, 'password')->passwordInput() ?>
'btn btn-primary']) ?>
``` ### Wrapping with `begin()` and `end()` In the above code, [[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]] not only creates a form instance, but also marks the beginning of the form. All of the content placed between [[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]] and [[yii\widgets\ActiveForm::end()|ActiveForm::end()]] will be wrapped within the HTML `
` tag. As with any widget, you can specify some options as to how the widget should be configured by passing an array to the `begin` method. In this case, an extra CSS class and identifying ID are passed to be used in the opening `` tag. For all available options, please refer to the API documentation of [[yii\widgets\ActiveForm]]. ### ActiveField In order to create a form element in the form, along with the element's label, and any applicable JavaScript validation, the [[yii\widgets\ActiveForm::field()|ActiveForm::field()]] method is called, which returns an instance of [[yii\widgets\ActiveField]]. When the result of this method is echoed directly, the result is a regular (text) input. To customize the output, you can chain additional methods of [[yii\widgets\ActiveField|ActiveField]] to this call: ```php // a password input field($model, 'password')->passwordInput() ?> // adding a hint and a customized label field($model, 'username')->textInput()->hint('Please enter your name')->label('Name') ?> // creating a HTML5 email input element field($model, 'email')->input('email') ?> ``` This will create all the `