From ff638ecd052e1a5a7a2a1f7e7d613e9466aed358 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 26 Dec 2025 00:55:07 +0300 Subject: [PATCH 1/7] Progress on getting started --- src/guide/start/creating-project.md | 4 +- src/guide/start/databases.md | 162 +++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 5 deletions(-) diff --git a/src/guide/start/creating-project.md b/src/guide/start/creating-project.md index 8e44c217..bd10c394 100644 --- a/src/guide/start/creating-project.md +++ b/src/guide/start/creating-project.md @@ -12,7 +12,7 @@ It can serve as a good starting point for your projects. You can create a new project from a template using the [Composer](https://getcomposer.org) package manager: -``` +```sh composer create-project yiisoft/app your_project ``` @@ -38,7 +38,7 @@ APP_ENV=dev ./yii serve --port=80 For Docker users, run: -``` +```sh make up ``` diff --git a/src/guide/start/databases.md b/src/guide/start/databases.md index 7c85ff74..0b2efd8a 100644 --- a/src/guide/start/databases.md +++ b/src/guide/start/databases.md @@ -331,19 +331,175 @@ final readonly class PageRepository ); } - public function delete(string $id): void + public function deleteBySlug(string $slug): void { - $this->connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute(); + $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); } } ``` ## Actions and routes -You need actions to: +You need to be able to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. 5. Edit a page. + +Let's tackle these one by one. + + +### List all pages + +Create `src/Web/Page/ListAction.php`: + +```php +viewRenderer->render(__DIR__ . '/list', [ + 'pages' => $this->pageRepository->findAll(), + ]); + } +} +``` + +Define list view in `src/Web/Page/list.php`: + +```php + $pages */ +?> + + +``` + +### View a page + +Create `src/Web/Page/ViewAction.php`: + +```php +pageRepository->findOneBySlug($slug); + if ($page === null || $page->isDeleted()) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + return $this->viewRenderer->render(__DIR__ . '/view', [ + 'page' => $page, + ]); + } +} +``` + +Now, a template in `src/Web/Page/view.php`: + +```php + + +

title) ?>

+ +

+ text) ?> +

+``` + +### Delete a page + +Create `src/Web/Page/DeleteAction.php`: + +```php +pageRepository->deleteBySlug($slug); + + return $this->responseFactory + ->createResponse() + ->withStatus(Status::PERMANENT_REDIRECT) + ->withHeader('Location', $this->urlGenerator->generate('page/list')); + } +} +``` + +### Create a page + + From 7d290acaf7c71fa73f929931915999ada1572ac8 Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:56:43 +0000 Subject: [PATCH 2/7] Update translation --- .../po/es/guide_start_creating-project.md.po | 18 +- .../po/es/guide_start_databases.md.po | 228 +++++++++++++++-- _translations/po/es/guide_start_hello.md.po | 16 +- .../po/es/guide_views_template-engines.md.po | 8 +- _translations/po/es/guide_views_view.md.po | 62 ++--- .../po/id/guide_start_creating-project.md.po | 14 +- .../po/id/guide_start_databases.md.po | 228 +++++++++++++++-- _translations/po/id/guide_start_hello.md.po | 16 +- .../po/id/guide_views_template-engines.md.po | 8 +- _translations/po/id/guide_views_view.md.po | 62 ++--- .../po/ru/guide_start_creating-project.md.po | 18 +- .../po/ru/guide_start_databases.md.po | 229 +++++++++++++++-- _translations/po/ru/guide_start_hello.md.po | 16 +- .../po/ru/guide_views_template-engines.md.po | 8 +- _translations/po/ru/guide_views_view.md.po | 62 ++--- .../pot/guide_start_creating-project.md.pot | 14 +- .../pot/guide_start_databases.md.pot | 230 ++++++++++++++++-- _translations/pot/guide_start_hello.md.pot | 16 +- .../pot/guide_views_template-engines.md.pot | 8 +- _translations/pot/guide_views_view.md.pot | 62 ++--- src/es/guide/start/creating-project.md | 11 +- src/es/guide/start/databases.md | 178 +++++++++++++- src/es/guide/start/hello.md | 12 +- src/es/guide/views/template-engines.md | 6 +- src/id/guide/start/creating-project.md | 11 +- src/id/guide/start/databases.md | 178 +++++++++++++- src/id/guide/start/hello.md | 12 +- src/id/guide/views/template-engines.md | 6 +- src/ru/guide/start/creating-project.md | 11 +- src/ru/guide/start/databases.md | 178 +++++++++++++- src/ru/guide/start/hello.md | 12 +- src/ru/guide/views/template-engines.md | 6 +- 32 files changed, 1627 insertions(+), 317 deletions(-) diff --git a/_translations/po/es/guide_start_creating-project.md.po b/_translations/po/es/guide_start_creating-project.md.po index d18ed6d0..9246e1fb 100644 --- a/_translations/po/es/guide_start_creating-project.md.po +++ b/_translations/po/es/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -33,7 +33,7 @@ msgstr "" msgid "" "> [!NOTE]\n" "> If you want to use another web server,\n" -"> see [\"Configuring web servers\"](../../../cookbook/en/configuring-webservers/general.md).\n" +"> see [\"Configuring web servers\"](../../cookbook/configuring-webservers/general.md).\n" msgstr "" #. type: Plain text @@ -46,7 +46,7 @@ msgstr "" msgid "You can create a new project from a template using the [Composer](https://getcomposer.org) package manager:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "composer create-project yiisoft/app your_project\n" @@ -54,13 +54,17 @@ msgstr "" #. type: Plain text #: ../src/guide/start/creating-project.md -msgid "Docker users can run the following command:" -msgstr "" +#, fuzzy +#| msgid "Events are raised like the following:" +msgid "Docker users can run the following commands:" +msgstr "Los eventos se lanzan de la siguiente forma:" #. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap -msgid "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +msgid "" +"docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +"sudo chown -R $(id -u):$(id -g) your_project\n" msgstr "" #. type: Plain text @@ -93,7 +97,7 @@ msgstr "" msgid "For Docker users, run:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "make up\n" diff --git a/_translations/po/es/guide_start_databases.md.po b/_translations/po/es/guide_start_databases.md.po index 884cabc0..d244da8f 100644 --- a/_translations/po/es/guide_start_databases.md.po +++ b/_translations/po/es/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -165,18 +165,13 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Let's use latest versions to be released. Change your `minimum-stability` to `dev` in `composer.json` first." -msgstr "" - -#. type: Plain text -#: ../src/guide/start/databases.md -msgid "Then we need a package to be installed:" +msgid "First we need a package to be installed:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, no-wrap -msgid "make composer require yiisoft/db-pgsql dev-master\n" +msgid "make composer require yiisoft/db-pgsql\n" msgstr "" #. type: Plain text @@ -260,7 +255,7 @@ msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, no-wrap -msgid "composer require yiisoft/db-migration dev-master\n" +msgid "make composer require yiisoft/db-migration\n" msgstr "" #. type: Plain text @@ -296,9 +291,6 @@ msgid "" "use Yiisoft\\Db\\Migration\\MigrationBuilder;\n" "use Yiisoft\\Db\\Migration\\RevertibleMigrationInterface;\n" "\n" -"/**\n" -" * Class M251102141707Page\n" -" */\n" "final class M251102141707Page implements RevertibleMigrationInterface\n" "{\n" " public function up(MigrationBuilder $b): void\n" @@ -350,6 +342,8 @@ msgstr "" msgid "" "connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute();\n" +" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" " }\n" "}\n" msgstr "" @@ -501,7 +497,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need actions to:" +msgid "You need to be able to:" msgstr "" #. type: Bullet: '1. ' @@ -528,3 +524,205 @@ msgstr "" #: ../src/guide/start/databases.md msgid "Edit a page." msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Let's tackle these one by one." +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "List all pages" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ListAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/list', [\n" +" 'pages' => $this->pageRepository->findAll(),\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Define list view in `src/Web/Page/list.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +" $pages */\n" +"?>\n" +"\n" +"\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "View a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ViewAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->findOneBySlug($slug);\n" +" if ($page === null || $page->isDeleted()) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/view', [\n" +" 'page' => $page,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/view.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"\n" +"\n" +"

title) ?>

\n" +"\n" +"

\n" +" text) ?>\n" +"

\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Delete a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/DeleteAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->deleteBySlug($slug);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse()\n" +" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" +" }\n" +"}\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Create a page" +msgstr "" diff --git a/_translations/po/es/guide_start_hello.md.po b/_translations/po/es/guide_start_hello.md.po index 3258e467..a0152381 100644 --- a/_translations/po/es/guide_start_hello.md.po +++ b/_translations/po/es/guide_start_hello.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -95,8 +95,10 @@ msgid "" " private ResponseFactoryInterface $responseFactory,\n" " ) {}\n" "\n" -" #[RouteArgument('message')]\n" -" public function __invoke(string $message = 'Hello!'): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('message')]\n" +" string $message = 'Hello!'\n" +" ): ResponseInterface\n" " {\n" " $response = $this->responseFactory->createResponse();\n" " $response->getBody()->write('The message is: ' . Html::encode($message));\n" @@ -226,7 +228,7 @@ msgid "To use the view, you need to change `src/Web/Echo/Action.php`:" msgstr "" #. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md +#: ../src/guide/start/hello.md #, no-wrap msgid "" "viewRenderer->render(__DIR__ . '/template', [\n" " 'message' => $message,\n" diff --git a/_translations/po/es/guide_views_template-engines.md.po b/_translations/po/es/guide_views_template-engines.md.po index 8ab61d0c..fd005287 100644 --- a/_translations/po/es/guide_views_template-engines.md.po +++ b/_translations/po/es/guide_views_template-engines.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -259,10 +259,12 @@ msgstr "" #: ../src/guide/views/template-engines.md #, no-wrap msgid "" +"use Yiisoft\\Container\\Reference;\n" +"\n" "// In configuration\n" "'yiisoft/view' => [\n" " 'renderers' => [\n" -" 'md' => App\\View\\MarkdownRenderer::class,\n" +" 'md' => Reference::to(App\\View\\MarkdownRenderer::class),\n" " ],\n" "],\n" msgstr "" @@ -286,7 +288,7 @@ msgid "" "\n" "Welcome, {{username}}!\n" "\n" -"This is a markdown template with **bold** and *italic* text.\n" +"This is a Markdown template with **bold** and *italic* text.\n" "\n" "- Feature 1\n" "- Feature 2\n" diff --git a/_translations/po/es/guide_views_view.md.po b/_translations/po/es/guide_views_view.md.po index cefb161f..987d16f6 100644 --- a/_translations/po/es/guide_views_view.md.po +++ b/_translations/po/es/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -44,36 +44,6 @@ msgid "" "

The message is:

\n" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -139,6 +109,36 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/po/id/guide_start_creating-project.md.po b/_translations/po/id/guide_start_creating-project.md.po index 35e11a50..cc93c5e1 100644 --- a/_translations/po/id/guide_start_creating-project.md.po +++ b/_translations/po/id/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -32,7 +32,7 @@ msgstr "" msgid "" "> [!NOTE]\n" "> If you want to use another web server,\n" -"> see [\"Configuring web servers\"](../../../cookbook/en/configuring-webservers/general.md).\n" +"> see [\"Configuring web servers\"](../../cookbook/configuring-webservers/general.md).\n" msgstr "" #. type: Plain text @@ -45,7 +45,7 @@ msgstr "" msgid "You can create a new project from a template using the [Composer](https://getcomposer.org) package manager:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "composer create-project yiisoft/app your_project\n" @@ -53,13 +53,15 @@ msgstr "" #. type: Plain text #: ../src/guide/start/creating-project.md -msgid "Docker users can run the following command:" +msgid "Docker users can run the following commands:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap -msgid "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +msgid "" +"docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +"sudo chown -R $(id -u):$(id -g) your_project\n" msgstr "" #. type: Plain text @@ -92,7 +94,7 @@ msgstr "" msgid "For Docker users, run:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "make up\n" diff --git a/_translations/po/id/guide_start_databases.md.po b/_translations/po/id/guide_start_databases.md.po index a8e888f8..5b883d9c 100644 --- a/_translations/po/id/guide_start_databases.md.po +++ b/_translations/po/id/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -164,19 +164,14 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Let's use latest versions to be released. Change your `minimum-stability` to `dev` in `composer.json` first." -msgstr "" - -#. type: Plain text -#: ../src/guide/start/databases.md -msgid "Then we need a package to be installed:" +msgid "First we need a package to be installed:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, fuzzy, no-wrap #| msgid "composer require yiisoft/cache\n" -msgid "make composer require yiisoft/db-pgsql dev-master\n" +msgid "make composer require yiisoft/db-pgsql\n" msgstr "composer require yiisoft/cache\n" #. type: Plain text @@ -261,7 +256,7 @@ msgstr "" #: ../src/guide/start/databases.md #, fuzzy, no-wrap #| msgid "composer require yiisoft/cache\n" -msgid "composer require yiisoft/db-migration dev-master\n" +msgid "make composer require yiisoft/db-migration\n" msgstr "composer require yiisoft/cache\n" #. type: Plain text @@ -297,9 +292,6 @@ msgid "" "use Yiisoft\\Db\\Migration\\MigrationBuilder;\n" "use Yiisoft\\Db\\Migration\\RevertibleMigrationInterface;\n" "\n" -"/**\n" -" * Class M251102141707Page\n" -" */\n" "final class M251102141707Page implements RevertibleMigrationInterface\n" "{\n" " public function up(MigrationBuilder $b): void\n" @@ -351,6 +343,8 @@ msgstr "" msgid "" "connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute();\n" +" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" " }\n" "}\n" msgstr "" @@ -502,7 +498,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need actions to:" +msgid "You need to be able to:" msgstr "" #. type: Bullet: '1. ' @@ -529,3 +525,205 @@ msgstr "" #: ../src/guide/start/databases.md msgid "Edit a page." msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Let's tackle these one by one." +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "List all pages" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ListAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/list', [\n" +" 'pages' => $this->pageRepository->findAll(),\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Define list view in `src/Web/Page/list.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +" $pages */\n" +"?>\n" +"\n" +"\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "View a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ViewAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->findOneBySlug($slug);\n" +" if ($page === null || $page->isDeleted()) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/view', [\n" +" 'page' => $page,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/view.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"\n" +"\n" +"

title) ?>

\n" +"\n" +"

\n" +" text) ?>\n" +"

\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Delete a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/DeleteAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->deleteBySlug($slug);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse()\n" +" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" +" }\n" +"}\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Create a page" +msgstr "" diff --git a/_translations/po/id/guide_start_hello.md.po b/_translations/po/id/guide_start_hello.md.po index 774d5730..b3a73020 100644 --- a/_translations/po/id/guide_start_hello.md.po +++ b/_translations/po/id/guide_start_hello.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -94,8 +94,10 @@ msgid "" " private ResponseFactoryInterface $responseFactory,\n" " ) {}\n" "\n" -" #[RouteArgument('message')]\n" -" public function __invoke(string $message = 'Hello!'): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('message')]\n" +" string $message = 'Hello!'\n" +" ): ResponseInterface\n" " {\n" " $response = $this->responseFactory->createResponse();\n" " $response->getBody()->write('The message is: ' . Html::encode($message));\n" @@ -225,7 +227,7 @@ msgid "To use the view, you need to change `src/Web/Echo/Action.php`:" msgstr "" #. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md +#: ../src/guide/start/hello.md #, no-wrap msgid "" "viewRenderer->render(__DIR__ . '/template', [\n" " 'message' => $message,\n" diff --git a/_translations/po/id/guide_views_template-engines.md.po b/_translations/po/id/guide_views_template-engines.md.po index 9e12bd10..676b7350 100644 --- a/_translations/po/id/guide_views_template-engines.md.po +++ b/_translations/po/id/guide_views_template-engines.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -259,10 +259,12 @@ msgstr "" #: ../src/guide/views/template-engines.md #, no-wrap msgid "" +"use Yiisoft\\Container\\Reference;\n" +"\n" "// In configuration\n" "'yiisoft/view' => [\n" " 'renderers' => [\n" -" 'md' => App\\View\\MarkdownRenderer::class,\n" +" 'md' => Reference::to(App\\View\\MarkdownRenderer::class),\n" " ],\n" "],\n" msgstr "" @@ -286,7 +288,7 @@ msgid "" "\n" "Welcome, {{username}}!\n" "\n" -"This is a markdown template with **bold** and *italic* text.\n" +"This is a Markdown template with **bold** and *italic* text.\n" "\n" "- Feature 1\n" "- Feature 2\n" diff --git a/_translations/po/id/guide_views_view.md.po b/_translations/po/id/guide_views_view.md.po index 6bcaff6e..0351d618 100644 --- a/_translations/po/id/guide_views_view.md.po +++ b/_translations/po/id/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -43,36 +43,6 @@ msgid "" "

The message is:

\n" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -139,6 +109,36 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/po/ru/guide_start_creating-project.md.po b/_translations/po/ru/guide_start_creating-project.md.po index 36466500..7cbc0c35 100644 --- a/_translations/po/ru/guide_start_creating-project.md.po +++ b/_translations/po/ru/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -33,7 +33,7 @@ msgstr "" msgid "" "> [!NOTE]\n" "> If you want to use another web server,\n" -"> see [\"Configuring web servers\"](../../../cookbook/en/configuring-webservers/general.md).\n" +"> see [\"Configuring web servers\"](../../cookbook/configuring-webservers/general.md).\n" msgstr "" #. type: Plain text @@ -46,7 +46,7 @@ msgstr "" msgid "You can create a new project from a template using the [Composer](https://getcomposer.org) package manager:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "composer create-project yiisoft/app your_project\n" @@ -54,13 +54,17 @@ msgstr "" #. type: Plain text #: ../src/guide/start/creating-project.md -msgid "Docker users can run the following command:" -msgstr "" +#, fuzzy +#| msgid "To install the packages, run the following command:" +msgid "Docker users can run the following commands:" +msgstr "Для установки пакетов выполните в консоли следующую команду:" #. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap -msgid "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +msgid "" +"docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +"sudo chown -R $(id -u):$(id -g) your_project\n" msgstr "" #. type: Plain text @@ -93,7 +97,7 @@ msgstr "" msgid "For Docker users, run:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "make up\n" diff --git a/_translations/po/ru/guide_start_databases.md.po b/_translations/po/ru/guide_start_databases.md.po index f9b3cd62..07bd2f73 100644 --- a/_translations/po/ru/guide_start_databases.md.po +++ b/_translations/po/ru/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -168,19 +168,14 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Let's use latest versions to be released. Change your `minimum-stability` to `dev` in `composer.json` first." -msgstr "" - -#. type: Plain text -#: ../src/guide/start/databases.md -msgid "Then we need a package to be installed:" +msgid "First we need a package to be installed:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, fuzzy, no-wrap #| msgid "composer install yiisoft/security\n" -msgid "make composer require yiisoft/db-pgsql dev-master\n" +msgid "make composer require yiisoft/db-pgsql\n" msgstr "composer install yiisoft/security\n" #. type: Plain text @@ -264,7 +259,7 @@ msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, fuzzy, no-wrap -msgid "composer require yiisoft/db-migration dev-master\n" +msgid "make composer require yiisoft/db-migration\n" msgstr "composer install yiisoft/security\n" #. type: Plain text @@ -300,9 +295,6 @@ msgid "" "use Yiisoft\\Db\\Migration\\MigrationBuilder;\n" "use Yiisoft\\Db\\Migration\\RevertibleMigrationInterface;\n" "\n" -"/**\n" -" * Class M251102141707Page\n" -" */\n" "final class M251102141707Page implements RevertibleMigrationInterface\n" "{\n" " public function up(MigrationBuilder $b): void\n" @@ -354,6 +346,8 @@ msgstr "" msgid "" "connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute();\n" +" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" " }\n" "}\n" msgstr "" @@ -505,7 +501,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need actions to:" +msgid "You need to be able to:" msgstr "" #. type: Bullet: '1. ' @@ -532,3 +528,206 @@ msgstr "" #: ../src/guide/start/databases.md msgid "Edit a page." msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Let's tackle these one by one." +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, fuzzy, no-wrap +#| msgid "Install the package" +msgid "List all pages" +msgstr "Установка пакета" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ListAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/list', [\n" +" 'pages' => $this->pageRepository->findAll(),\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Define list view in `src/Web/Page/list.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +" $pages */\n" +"?>\n" +"\n" +"\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "View a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ViewAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->findOneBySlug($slug);\n" +" if ($page === null || $page->isDeleted()) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/view', [\n" +" 'page' => $page,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/view.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"\n" +"\n" +"

title) ?>

\n" +"\n" +"

\n" +" text) ?>\n" +"

\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Delete a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/DeleteAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->deleteBySlug($slug);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse()\n" +" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" +" }\n" +"}\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Create a page" +msgstr "" diff --git a/_translations/po/ru/guide_start_hello.md.po b/_translations/po/ru/guide_start_hello.md.po index 98d4f13f..6de23ca2 100644 --- a/_translations/po/ru/guide_start_hello.md.po +++ b/_translations/po/ru/guide_start_hello.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -96,8 +96,10 @@ msgid "" " private ResponseFactoryInterface $responseFactory,\n" " ) {}\n" "\n" -" #[RouteArgument('message')]\n" -" public function __invoke(string $message = 'Hello!'): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('message')]\n" +" string $message = 'Hello!'\n" +" ): ResponseInterface\n" " {\n" " $response = $this->responseFactory->createResponse();\n" " $response->getBody()->write('The message is: ' . Html::encode($message));\n" @@ -230,7 +232,7 @@ msgid "To use the view, you need to change `src/Web/Echo/Action.php`:" msgstr "" #. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md +#: ../src/guide/start/hello.md #, no-wrap msgid "" "viewRenderer->render(__DIR__ . '/template', [\n" " 'message' => $message,\n" diff --git a/_translations/po/ru/guide_views_template-engines.md.po b/_translations/po/ru/guide_views_template-engines.md.po index 3d6ad8bd..9ed08c3f 100644 --- a/_translations/po/ru/guide_views_template-engines.md.po +++ b/_translations/po/ru/guide_views_template-engines.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -260,10 +260,12 @@ msgstr "" #: ../src/guide/views/template-engines.md #, no-wrap msgid "" +"use Yiisoft\\Container\\Reference;\n" +"\n" "// In configuration\n" "'yiisoft/view' => [\n" " 'renderers' => [\n" -" 'md' => App\\View\\MarkdownRenderer::class,\n" +" 'md' => Reference::to(App\\View\\MarkdownRenderer::class),\n" " ],\n" "],\n" msgstr "" @@ -287,7 +289,7 @@ msgid "" "\n" "Welcome, {{username}}!\n" "\n" -"This is a markdown template with **bold** and *italic* text.\n" +"This is a Markdown template with **bold** and *italic* text.\n" "\n" "- Feature 1\n" "- Feature 2\n" diff --git a/_translations/po/ru/guide_views_view.md.po b/_translations/po/ru/guide_views_view.md.po index f24271fc..3e653965 100644 --- a/_translations/po/ru/guide_views_view.md.po +++ b/_translations/po/ru/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -44,36 +44,6 @@ msgid "" "

The message is:

\n" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -142,6 +112,36 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/pot/guide_start_creating-project.md.pot b/_translations/pot/guide_start_creating-project.md.pot index caaab852..dff8895f 100644 --- a/_translations/pot/guide_start_creating-project.md.pot +++ b/_translations/pot/guide_start_creating-project.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -36,7 +36,7 @@ msgstr "" msgid "" "> [!NOTE]\n" "> If you want to use another web server,\n" -"> see [\"Configuring web servers\"](../../../cookbook/en/configuring-webservers/general.md).\n" +"> see [\"Configuring web servers\"](../../cookbook/configuring-webservers/general.md).\n" msgstr "" #. type: Plain text @@ -54,7 +54,7 @@ msgid "" "getcomposer.org) package manager:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "composer create-project yiisoft/app your_project\n" @@ -62,13 +62,15 @@ msgstr "" #. type: Plain text #: ../src/guide/start/creating-project.md -msgid "Docker users can run the following command:" +msgid "Docker users can run the following commands:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap -msgid "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +msgid "" +"docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" +"sudo chown -R $(id -u):$(id -g) your_project\n" msgstr "" #. type: Plain text @@ -104,7 +106,7 @@ msgstr "" msgid "For Docker users, run:" msgstr "" -#. type: Fenced code block +#. type: Fenced code block (sh) #: ../src/guide/start/creating-project.md #, no-wrap msgid "make up\n" diff --git a/_translations/pot/guide_start_databases.md.pot b/_translations/pot/guide_start_databases.md.pot index cc87dc97..5ddb9e6c 100644 --- a/_translations/pot/guide_start_databases.md.pot +++ b/_translations/pot/guide_start_databases.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -184,20 +184,13 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "" -"Let's use latest versions to be released. Change your `minimum-stability` to " -"`dev` in `composer.json` first." -msgstr "" - -#. type: Plain text -#: ../src/guide/start/databases.md -msgid "Then we need a package to be installed:" +msgid "First we need a package to be installed:" msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, no-wrap -msgid "make composer require yiisoft/db-pgsql dev-master\n" +msgid "make composer require yiisoft/db-pgsql\n" msgstr "" #. type: Plain text @@ -289,7 +282,7 @@ msgstr "" #. type: Fenced code block (sh) #: ../src/guide/start/databases.md #, no-wrap -msgid "composer require yiisoft/db-migration dev-master\n" +msgid "make composer require yiisoft/db-migration\n" msgstr "" #. type: Plain text @@ -329,9 +322,6 @@ msgid "" "use Yiisoft\\Db\\Migration\\MigrationBuilder;\n" "use Yiisoft\\Db\\Migration\\RevertibleMigrationInterface;\n" "\n" -"/**\n" -" * Class M251102141707Page\n" -" */\n" "final class M251102141707Page implements RevertibleMigrationInterface\n" "{\n" " public function up(MigrationBuilder $b): void\n" @@ -390,6 +380,8 @@ msgstr "" msgid "" "connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute();\n" +" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" " }\n" "}\n" msgstr "" @@ -543,7 +537,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need actions to:" +msgid "You need to be able to:" msgstr "" #. type: Bullet: '1. ' @@ -570,3 +564,205 @@ msgstr "" #: ../src/guide/start/databases.md msgid "Edit a page." msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Let's tackle these one by one." +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "List all pages" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ListAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/list', [\n" +" 'pages' => $this->pageRepository->findAll(),\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Define list view in `src/Web/Page/list.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +" $pages */\n" +"?>\n" +"\n" +"
    \n" +" \n" +"
  • \n" +" title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?>\n" +"
  • \n" +" \n" +"
\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "View a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/ViewAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->findOneBySlug($slug);\n" +" if ($page === null || $page->isDeleted()) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/view', [\n" +" 'page' => $page,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/view.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"\n" +"\n" +"

title) ?>

\n" +"\n" +"

\n" +" text) ?>\n" +"

\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Delete a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/DeleteAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"pageRepository->deleteBySlug($slug);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse()\n" +" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" +" }\n" +"}\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Create a page" +msgstr "" diff --git a/_translations/pot/guide_start_hello.md.pot b/_translations/pot/guide_start_hello.md.pot index 0e8bfff1..bb0d344b 100644 --- a/_translations/pot/guide_start_hello.md.pot +++ b/_translations/pot/guide_start_hello.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -105,8 +105,10 @@ msgid "" " private ResponseFactoryInterface $responseFactory,\n" " ) {}\n" "\n" -" #[RouteArgument('message')]\n" -" public function __invoke(string $message = 'Hello!'): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('message')]\n" +" string $message = 'Hello!'\n" +" ): ResponseInterface\n" " {\n" " $response = $this->responseFactory->createResponse();\n" " $response->getBody()->write('The message is: ' . Html::encode($message));\n" @@ -270,7 +272,7 @@ msgid "To use the view, you need to change `src/Web/Echo/Action.php`:" msgstr "" #. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md +#: ../src/guide/start/hello.md #, no-wrap msgid "" "viewRenderer->render(__DIR__ . '/template', [\n" " 'message' => $message,\n" diff --git a/_translations/pot/guide_views_template-engines.md.pot b/_translations/pot/guide_views_template-engines.md.pot index 72c6272b..7849e95d 100644 --- a/_translations/pot/guide_views_template-engines.md.pot +++ b/_translations/pot/guide_views_template-engines.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -268,10 +268,12 @@ msgstr "" #: ../src/guide/views/template-engines.md #, no-wrap msgid "" +"use Yiisoft\\Container\\Reference;\n" +"\n" "// In configuration\n" "'yiisoft/view' => [\n" " 'renderers' => [\n" -" 'md' => App\\View\\MarkdownRenderer::class,\n" +" 'md' => Reference::to(App\\View\\MarkdownRenderer::class),\n" " ],\n" "],\n" msgstr "" @@ -295,7 +297,7 @@ msgid "" "\n" "Welcome, {{username}}!\n" "\n" -"This is a markdown template with **bold** and *italic* text.\n" +"This is a Markdown template with **bold** and *italic* text.\n" "\n" "- Feature 1\n" "- Feature 2\n" diff --git a/_translations/pot/guide_views_view.md.pot b/_translations/pot/guide_views_view.md.pot index cf2ffa1e..76cf932c 100644 --- a/_translations/pot/guide_views_view.md.pot +++ b/_translations/pot/guide_views_view.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-25 21:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -44,36 +44,6 @@ msgid "" "

The message is:

\n" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/start/hello.md ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -160,6 +130,36 @@ msgid "" "with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Plain text #: ../src/guide/views/view.md msgid "" diff --git a/src/es/guide/start/creating-project.md b/src/es/guide/start/creating-project.md index efde5e36..b1b3f61c 100644 --- a/src/es/guide/start/creating-project.md +++ b/src/es/guide/start/creating-project.md @@ -6,7 +6,7 @@ dev server with everything installed locally. > [!NOTE] > If you want to use another web server, -> see ["Configuring web servers"](../../../cookbook/en/configuring-webservers/general.md). +> see ["Configuring web servers"](../../cookbook/configuring-webservers/general.md). We recommend starting with a project template that's a minimal working Yii project implementing some basic features. It can serve as a good starting @@ -15,14 +15,15 @@ point for your projects. You can create a new project from a template using the [Composer](https://getcomposer.org) package manager: -``` +```sh composer create-project yiisoft/app your_project ``` -Docker users can run the following command: - +Docker users can run the following commands: + ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project +sudo chown -R $(id -u):$(id -g) your_project ``` This installs the latest stable version of the Yii project template in a @@ -41,7 +42,7 @@ APP_ENV=dev ./yii serve --port=80 For Docker users, run: -``` +```sh make up ``` diff --git a/src/es/guide/start/databases.md b/src/es/guide/start/databases.md index b4be1d4b..22f7657a 100644 --- a/src/es/guide/start/databases.md +++ b/src/es/guide/start/databases.md @@ -86,13 +86,10 @@ list. Then rebuild PHP image with `make build && make down && make up`. Now that we have the database, it's time to define the connection. -Let's use latest versions to be released. Change your `minimum-stability` to -`dev` in `composer.json` first. - -Then we need a package to be installed: +First we need a package to be installed: ```sh -make composer require yiisoft/db-pgsql dev-master +make composer require yiisoft/db-pgsql ``` Now create `config/common/di/db-pgsql.php`: @@ -152,7 +149,7 @@ the current state and which migrations remain to be applied. To use migrations we need another package installed: ```sh -composer require yiisoft/db-migration dev-master +make composer require yiisoft/db-migration ``` Create a directory to store migrations `src/Migration` right in the project @@ -178,9 +175,6 @@ namespace App\Migration; use Yiisoft\Db\Migration\MigrationBuilder; use Yiisoft\Db\Migration\RevertibleMigrationInterface; -/** - * Class M251102141707Page - */ final class M251102141707Page implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void @@ -221,6 +215,8 @@ Now that you have a table it is time to define an entity in the code. Create ```php connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute(); + $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); } } ``` ## Actions and routes -You need actions to: +You need to be able to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. 5. Edit a page. + +Let's tackle these one by one. + + +### List all pages + +Create `src/Web/Page/ListAction.php`: + +```php +viewRenderer->render(__DIR__ . '/list', [ + 'pages' => $this->pageRepository->findAll(), + ]); + } +} +``` + +Define list view in `src/Web/Page/list.php`: + +```php + $pages */ +?> + +
    + +
  • + title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> +
  • + +
+``` + +### View a page + +Create `src/Web/Page/ViewAction.php`: + +```php +pageRepository->findOneBySlug($slug); + if ($page === null || $page->isDeleted()) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + return $this->viewRenderer->render(__DIR__ . '/view', [ + 'page' => $page, + ]); + } +} +``` + +Now, a template in `src/Web/Page/view.php`: + +```php + + +

title) ?>

+ +

+ text) ?> +

+``` + +### Delete a page + +Create `src/Web/Page/DeleteAction.php`: + +```php +pageRepository->deleteBySlug($slug); + + return $this->responseFactory + ->createResponse() + ->withStatus(Status::PERMANENT_REDIRECT) + ->withHeader('Location', $this->urlGenerator->generate('page/list')); + } +} +``` + +### Create a page + + diff --git a/src/es/guide/start/hello.md b/src/es/guide/start/hello.md index d00e41dd..bc9afce2 100644 --- a/src/es/guide/start/hello.md +++ b/src/es/guide/start/hello.md @@ -42,8 +42,10 @@ final readonly class Action private ResponseFactoryInterface $responseFactory, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { $response = $this->responseFactory->createResponse(); $response->getBody()->write('The message is: ' . Html::encode($message)); @@ -155,8 +157,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, diff --git a/src/es/guide/views/template-engines.md b/src/es/guide/views/template-engines.md index 08ab054c..708f6141 100644 --- a/src/es/guide/views/template-engines.md +++ b/src/es/guide/views/template-engines.md @@ -150,10 +150,12 @@ final class MarkdownRenderer implements TemplateRendererInterface Register your custom renderer: ```php +use Yiisoft\Container\Reference; + // In configuration 'yiisoft/view' => [ 'renderers' => [ - 'md' => App\View\MarkdownRenderer::class, + 'md' => Reference::to(App\View\MarkdownRenderer::class), ], ], ``` @@ -166,7 +168,7 @@ Now you can use `.md` template files: Welcome, {{username}}! -This is a markdown template with **bold** and *italic* text. +This is a Markdown template with **bold** and *italic* text. - Feature 1 - Feature 2 diff --git a/src/id/guide/start/creating-project.md b/src/id/guide/start/creating-project.md index efde5e36..b1b3f61c 100644 --- a/src/id/guide/start/creating-project.md +++ b/src/id/guide/start/creating-project.md @@ -6,7 +6,7 @@ dev server with everything installed locally. > [!NOTE] > If you want to use another web server, -> see ["Configuring web servers"](../../../cookbook/en/configuring-webservers/general.md). +> see ["Configuring web servers"](../../cookbook/configuring-webservers/general.md). We recommend starting with a project template that's a minimal working Yii project implementing some basic features. It can serve as a good starting @@ -15,14 +15,15 @@ point for your projects. You can create a new project from a template using the [Composer](https://getcomposer.org) package manager: -``` +```sh composer create-project yiisoft/app your_project ``` -Docker users can run the following command: - +Docker users can run the following commands: + ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project +sudo chown -R $(id -u):$(id -g) your_project ``` This installs the latest stable version of the Yii project template in a @@ -41,7 +42,7 @@ APP_ENV=dev ./yii serve --port=80 For Docker users, run: -``` +```sh make up ``` diff --git a/src/id/guide/start/databases.md b/src/id/guide/start/databases.md index b4be1d4b..22f7657a 100644 --- a/src/id/guide/start/databases.md +++ b/src/id/guide/start/databases.md @@ -86,13 +86,10 @@ list. Then rebuild PHP image with `make build && make down && make up`. Now that we have the database, it's time to define the connection. -Let's use latest versions to be released. Change your `minimum-stability` to -`dev` in `composer.json` first. - -Then we need a package to be installed: +First we need a package to be installed: ```sh -make composer require yiisoft/db-pgsql dev-master +make composer require yiisoft/db-pgsql ``` Now create `config/common/di/db-pgsql.php`: @@ -152,7 +149,7 @@ the current state and which migrations remain to be applied. To use migrations we need another package installed: ```sh -composer require yiisoft/db-migration dev-master +make composer require yiisoft/db-migration ``` Create a directory to store migrations `src/Migration` right in the project @@ -178,9 +175,6 @@ namespace App\Migration; use Yiisoft\Db\Migration\MigrationBuilder; use Yiisoft\Db\Migration\RevertibleMigrationInterface; -/** - * Class M251102141707Page - */ final class M251102141707Page implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void @@ -221,6 +215,8 @@ Now that you have a table it is time to define an entity in the code. Create ```php connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute(); + $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); } } ``` ## Actions and routes -You need actions to: +You need to be able to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. 5. Edit a page. + +Let's tackle these one by one. + + +### List all pages + +Create `src/Web/Page/ListAction.php`: + +```php +viewRenderer->render(__DIR__ . '/list', [ + 'pages' => $this->pageRepository->findAll(), + ]); + } +} +``` + +Define list view in `src/Web/Page/list.php`: + +```php + $pages */ +?> + +
    + +
  • + title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> +
  • + +
+``` + +### View a page + +Create `src/Web/Page/ViewAction.php`: + +```php +pageRepository->findOneBySlug($slug); + if ($page === null || $page->isDeleted()) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + return $this->viewRenderer->render(__DIR__ . '/view', [ + 'page' => $page, + ]); + } +} +``` + +Now, a template in `src/Web/Page/view.php`: + +```php + + +

title) ?>

+ +

+ text) ?> +

+``` + +### Delete a page + +Create `src/Web/Page/DeleteAction.php`: + +```php +pageRepository->deleteBySlug($slug); + + return $this->responseFactory + ->createResponse() + ->withStatus(Status::PERMANENT_REDIRECT) + ->withHeader('Location', $this->urlGenerator->generate('page/list')); + } +} +``` + +### Create a page + + diff --git a/src/id/guide/start/hello.md b/src/id/guide/start/hello.md index d00e41dd..bc9afce2 100644 --- a/src/id/guide/start/hello.md +++ b/src/id/guide/start/hello.md @@ -42,8 +42,10 @@ final readonly class Action private ResponseFactoryInterface $responseFactory, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { $response = $this->responseFactory->createResponse(); $response->getBody()->write('The message is: ' . Html::encode($message)); @@ -155,8 +157,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, diff --git a/src/id/guide/views/template-engines.md b/src/id/guide/views/template-engines.md index 08ab054c..708f6141 100644 --- a/src/id/guide/views/template-engines.md +++ b/src/id/guide/views/template-engines.md @@ -150,10 +150,12 @@ final class MarkdownRenderer implements TemplateRendererInterface Register your custom renderer: ```php +use Yiisoft\Container\Reference; + // In configuration 'yiisoft/view' => [ 'renderers' => [ - 'md' => App\View\MarkdownRenderer::class, + 'md' => Reference::to(App\View\MarkdownRenderer::class), ], ], ``` @@ -166,7 +168,7 @@ Now you can use `.md` template files: Welcome, {{username}}! -This is a markdown template with **bold** and *italic* text. +This is a Markdown template with **bold** and *italic* text. - Feature 1 - Feature 2 diff --git a/src/ru/guide/start/creating-project.md b/src/ru/guide/start/creating-project.md index efde5e36..b1b3f61c 100644 --- a/src/ru/guide/start/creating-project.md +++ b/src/ru/guide/start/creating-project.md @@ -6,7 +6,7 @@ dev server with everything installed locally. > [!NOTE] > If you want to use another web server, -> see ["Configuring web servers"](../../../cookbook/en/configuring-webservers/general.md). +> see ["Configuring web servers"](../../cookbook/configuring-webservers/general.md). We recommend starting with a project template that's a minimal working Yii project implementing some basic features. It can serve as a good starting @@ -15,14 +15,15 @@ point for your projects. You can create a new project from a template using the [Composer](https://getcomposer.org) package manager: -``` +```sh composer create-project yiisoft/app your_project ``` -Docker users can run the following command: - +Docker users can run the following commands: + ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project +sudo chown -R $(id -u):$(id -g) your_project ``` This installs the latest stable version of the Yii project template in a @@ -41,7 +42,7 @@ APP_ENV=dev ./yii serve --port=80 For Docker users, run: -``` +```sh make up ``` diff --git a/src/ru/guide/start/databases.md b/src/ru/guide/start/databases.md index b4be1d4b..22f7657a 100644 --- a/src/ru/guide/start/databases.md +++ b/src/ru/guide/start/databases.md @@ -86,13 +86,10 @@ list. Then rebuild PHP image with `make build && make down && make up`. Now that we have the database, it's time to define the connection. -Let's use latest versions to be released. Change your `minimum-stability` to -`dev` in `composer.json` first. - -Then we need a package to be installed: +First we need a package to be installed: ```sh -make composer require yiisoft/db-pgsql dev-master +make composer require yiisoft/db-pgsql ``` Now create `config/common/di/db-pgsql.php`: @@ -152,7 +149,7 @@ the current state and which migrations remain to be applied. To use migrations we need another package installed: ```sh -composer require yiisoft/db-migration dev-master +make composer require yiisoft/db-migration ``` Create a directory to store migrations `src/Migration` right in the project @@ -178,9 +175,6 @@ namespace App\Migration; use Yiisoft\Db\Migration\MigrationBuilder; use Yiisoft\Db\Migration\RevertibleMigrationInterface; -/** - * Class M251102141707Page - */ final class M251102141707Page implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void @@ -221,6 +215,8 @@ Now that you have a table it is time to define an entity in the code. Create ```php connection->createCommand()->delete('{{%page}}', ['id' => $id])->execute(); + $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); } } ``` ## Actions and routes -You need actions to: +You need to be able to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. 5. Edit a page. + +Let's tackle these one by one. + + +### List all pages + +Create `src/Web/Page/ListAction.php`: + +```php +viewRenderer->render(__DIR__ . '/list', [ + 'pages' => $this->pageRepository->findAll(), + ]); + } +} +``` + +Define list view in `src/Web/Page/list.php`: + +```php + $pages */ +?> + +
    + +
  • + title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> +
  • + +
+``` + +### View a page + +Create `src/Web/Page/ViewAction.php`: + +```php +pageRepository->findOneBySlug($slug); + if ($page === null || $page->isDeleted()) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + return $this->viewRenderer->render(__DIR__ . '/view', [ + 'page' => $page, + ]); + } +} +``` + +Now, a template in `src/Web/Page/view.php`: + +```php + + +

title) ?>

+ +

+ text) ?> +

+``` + +### Delete a page + +Create `src/Web/Page/DeleteAction.php`: + +```php +pageRepository->deleteBySlug($slug); + + return $this->responseFactory + ->createResponse() + ->withStatus(Status::PERMANENT_REDIRECT) + ->withHeader('Location', $this->urlGenerator->generate('page/list')); + } +} +``` + +### Create a page + + diff --git a/src/ru/guide/start/hello.md b/src/ru/guide/start/hello.md index d00e41dd..bc9afce2 100644 --- a/src/ru/guide/start/hello.md +++ b/src/ru/guide/start/hello.md @@ -42,8 +42,10 @@ final readonly class Action private ResponseFactoryInterface $responseFactory, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { $response = $this->responseFactory->createResponse(); $response->getBody()->write('The message is: ' . Html::encode($message)); @@ -155,8 +157,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, diff --git a/src/ru/guide/views/template-engines.md b/src/ru/guide/views/template-engines.md index 08ab054c..708f6141 100644 --- a/src/ru/guide/views/template-engines.md +++ b/src/ru/guide/views/template-engines.md @@ -150,10 +150,12 @@ final class MarkdownRenderer implements TemplateRendererInterface Register your custom renderer: ```php +use Yiisoft\Container\Reference; + // In configuration 'yiisoft/view' => [ 'renderers' => [ - 'md' => App\View\MarkdownRenderer::class, + 'md' => Reference::to(App\View\MarkdownRenderer::class), ], ], ``` @@ -166,7 +168,7 @@ Now you can use `.md` template files: Welcome, {{username}}! -This is a markdown template with **bold** and *italic* text. +This is a Markdown template with **bold** and *italic* text. - Feature 1 - Feature 2 From f3059a75ec25505381057baae0b8a825b373139c Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Fri, 26 Dec 2025 18:19:27 +0000 Subject: [PATCH 3/7] Update translation --- _translations/po/es/guide_views_view.md.po | 64 +++++++++++----------- _translations/po/id/guide_views_view.md.po | 64 +++++++++++----------- _translations/po/ru/guide_views_view.md.po | 64 +++++++++++----------- _translations/pot/guide_views_view.md.pot | 64 +++++++++++----------- src/es/guide/views/view.md | 6 +- src/id/guide/views/view.md | 6 +- src/ru/guide/views/view.md | 6 +- 7 files changed, 144 insertions(+), 130 deletions(-) diff --git a/_translations/po/es/guide_views_view.md.po b/_translations/po/es/guide_views_view.md.po index 987d16f6..538ea771 100644 --- a/_translations/po/es/guide_views_view.md.po +++ b/_translations/po/es/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:18+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -44,6 +44,38 @@ msgid "" "

The message is:

\n" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/start/hello.md ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -109,36 +141,6 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/po/id/guide_views_view.md.po b/_translations/po/id/guide_views_view.md.po index 0351d618..1bb81aa1 100644 --- a/_translations/po/id/guide_views_view.md.po +++ b/_translations/po/id/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:18+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -43,6 +43,38 @@ msgid "" "

The message is:

\n" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/start/hello.md ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -109,36 +141,6 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/po/ru/guide_views_view.md.po b/_translations/po/ru/guide_views_view.md.po index 3e653965..43b2a714 100644 --- a/_translations/po/ru/guide_views_view.md.po +++ b/_translations/po/ru/guide_views_view.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:18+0000\n" "PO-Revision-Date: 2025-12-24 08:02+0000\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -44,6 +44,38 @@ msgid "" "

The message is:

\n" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/start/hello.md ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -112,36 +144,6 @@ msgstr "" msgid "Here `$message` is a view data that is passed when you render a template with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Plain text #: ../src/guide/views/view.md msgid "First argument of the `render()` method is a path to the template file. In the `yiisoft/app`, template files are typically stored alongside their actions. The result is ready to be rendered to the browser so we return it immediately." diff --git a/_translations/pot/guide_views_view.md.pot b/_translations/pot/guide_views_view.md.pot index 76cf932c..302cf212 100644 --- a/_translations/pot/guide_views_view.md.pot +++ b/_translations/pot/guide_views_view.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:18+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -44,6 +44,38 @@ msgid "" "

The message is:

\n" msgstr "" +#. type: Fenced code block (php) +#: ../src/guide/start/hello.md ../src/guide/views/view.md +#, no-wrap +msgid "" +"viewRenderer->render(__DIR__ . '/template', [\n" +" 'message' => $message,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + #. type: Title ## #: ../src/guide/views/asset.md ../src/guide/views/view.md #: ../src/guide/views/widget.md @@ -130,36 +162,6 @@ msgid "" "with the help of `ViewRenderer`. For example, `src/Web/Echo/Action.php`:" msgstr "" -#. type: Fenced code block (php) -#: ../src/guide/views/view.md -#, no-wrap -msgid "" -"viewRenderer->render(__DIR__ . '/template', [\n" -" 'message' => $message,\n" -" ]);\n" -" }\n" -"}\n" -msgstr "" - #. type: Plain text #: ../src/guide/views/view.md msgid "" diff --git a/src/es/guide/views/view.md b/src/es/guide/views/view.md index 689a835b..3b256a94 100644 --- a/src/es/guide/views/view.md +++ b/src/es/guide/views/view.md @@ -76,8 +76,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, diff --git a/src/id/guide/views/view.md b/src/id/guide/views/view.md index 689a835b..3b256a94 100644 --- a/src/id/guide/views/view.md +++ b/src/id/guide/views/view.md @@ -76,8 +76,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, diff --git a/src/ru/guide/views/view.md b/src/ru/guide/views/view.md index d7ce03c5..927ffa35 100644 --- a/src/ru/guide/views/view.md +++ b/src/ru/guide/views/view.md @@ -76,8 +76,10 @@ final readonly class Action private ViewRenderer $viewRenderer, ) {} - #[RouteArgument('message')] - public function __invoke(string $message = 'Hello!'): ResponseInterface + public function __invoke( + #[RouteArgument('message')] + string $message = 'Hello!' + ): ResponseInterface { return $this->viewRenderer->render(__DIR__ . '/template', [ 'message' => $message, From 3707d201998e4baec028b9287a1507aec6ef1553 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 26 Dec 2025 21:29:14 +0300 Subject: [PATCH 4/7] Do make composer update with built docker to fix PHP version mismatch --- src/guide/start/creating-project.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/guide/start/creating-project.md b/src/guide/start/creating-project.md index bd10c394..b7eccf9a 100644 --- a/src/guide/start/creating-project.md +++ b/src/guide/start/creating-project.md @@ -21,6 +21,7 @@ Docker users can run the following commands: ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project sudo chown -R $(id -u):$(id -g) your_project +make composer update ``` This installs the latest stable version of the Yii project template in a directory named `your_project`. From 37fa2053e0d35b3c24501ed0c6f2e12fdc17a511 Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Fri, 26 Dec 2025 18:30:47 +0000 Subject: [PATCH 5/7] Update translation --- _translations/po/es/guide_start_creating-project.md.po | 3 ++- _translations/po/id/guide_start_creating-project.md.po | 3 ++- _translations/po/ru/guide_start_creating-project.md.po | 3 ++- _translations/pot/guide_start_creating-project.md.pot | 3 ++- src/es/guide/start/creating-project.md | 1 + src/id/guide/start/creating-project.md | 1 + src/ru/guide/start/creating-project.md | 1 + 7 files changed, 11 insertions(+), 4 deletions(-) diff --git a/_translations/po/es/guide_start_creating-project.md.po b/_translations/po/es/guide_start_creating-project.md.po index 9246e1fb..f79186dd 100644 --- a/_translations/po/es/guide_start_creating-project.md.po +++ b/_translations/po/es/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:30+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -65,6 +65,7 @@ msgstr "Los eventos se lanzan de la siguiente forma:" msgid "" "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" "sudo chown -R $(id -u):$(id -g) your_project\n" +"make composer update\n" msgstr "" #. type: Plain text diff --git a/_translations/po/id/guide_start_creating-project.md.po b/_translations/po/id/guide_start_creating-project.md.po index cc93c5e1..6f8b7490 100644 --- a/_translations/po/id/guide_start_creating-project.md.po +++ b/_translations/po/id/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:30+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -62,6 +62,7 @@ msgstr "" msgid "" "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" "sudo chown -R $(id -u):$(id -g) your_project\n" +"make composer update\n" msgstr "" #. type: Plain text diff --git a/_translations/po/ru/guide_start_creating-project.md.po b/_translations/po/ru/guide_start_creating-project.md.po index 7cbc0c35..e59dcb0c 100644 --- a/_translations/po/ru/guide_start_creating-project.md.po +++ b/_translations/po/ru/guide_start_creating-project.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:30+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -65,6 +65,7 @@ msgstr "Для установки пакетов выполните в конс msgid "" "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" "sudo chown -R $(id -u):$(id -g) your_project\n" +"make composer update\n" msgstr "" #. type: Plain text diff --git a/_translations/pot/guide_start_creating-project.md.pot b/_translations/pot/guide_start_creating-project.md.pot index dff8895f..31dd901a 100644 --- a/_translations/pot/guide_start_creating-project.md.pot +++ b/_translations/pot/guide_start_creating-project.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 18:30+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -71,6 +71,7 @@ msgstr "" msgid "" "docker run --rm -it -v \"$(pwd):/app\" composer/composer create-project yiisoft/app your_project\n" "sudo chown -R $(id -u):$(id -g) your_project\n" +"make composer update\n" msgstr "" #. type: Plain text diff --git a/src/es/guide/start/creating-project.md b/src/es/guide/start/creating-project.md index b1b3f61c..aaf0d4b4 100644 --- a/src/es/guide/start/creating-project.md +++ b/src/es/guide/start/creating-project.md @@ -24,6 +24,7 @@ Docker users can run the following commands: ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project sudo chown -R $(id -u):$(id -g) your_project +make composer update ``` This installs the latest stable version of the Yii project template in a diff --git a/src/id/guide/start/creating-project.md b/src/id/guide/start/creating-project.md index b1b3f61c..aaf0d4b4 100644 --- a/src/id/guide/start/creating-project.md +++ b/src/id/guide/start/creating-project.md @@ -24,6 +24,7 @@ Docker users can run the following commands: ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project sudo chown -R $(id -u):$(id -g) your_project +make composer update ``` This installs the latest stable version of the Yii project template in a diff --git a/src/ru/guide/start/creating-project.md b/src/ru/guide/start/creating-project.md index b1b3f61c..aaf0d4b4 100644 --- a/src/ru/guide/start/creating-project.md +++ b/src/ru/guide/start/creating-project.md @@ -24,6 +24,7 @@ Docker users can run the following commands: ```sh docker run --rm -it -v "$(pwd):/app" composer/composer create-project yiisoft/app your_project sudo chown -R $(id -u):$(id -g) your_project +make composer update ``` This installs the latest stable version of the Yii project template in a From ef1650c1d0ef9068511fdc37f6ca5e67ac39b295 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Dec 2025 01:36:47 +0300 Subject: [PATCH 6/7] Finished databases guide --- src/guide/runtime/response.md | 15 +- src/guide/start/databases.md | 298 ++++++++++++++++++++++++++++------ src/guide/start/forms.md | 2 + 3 files changed, 266 insertions(+), 49 deletions(-) diff --git a/src/guide/runtime/response.md b/src/guide/runtime/response.md index b67b7434..8e7c3d7e 100644 --- a/src/guide/runtime/response.md +++ b/src/guide/runtime/response.md @@ -97,9 +97,20 @@ return $response ->withHeader('Location', 'https://www.example.com'); ``` +Note that there are different statuses used for redirection: + +| Code | Usage | What is it for | +|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. | +| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. | +| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. | +| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. | +| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. | + ### Responding with JSON ```php +use Yiisoft\Http\Status; use Yiisoft\Json\Json; $data = [ @@ -109,6 +120,6 @@ $data = [ $response->getBody()->write(Json::encode($data)); return $response - ->withStatus(200) - ->withHeader('Content-Type', 'application/json'); + ->withStatus(Status::OK) + ->withHeader('Content-Type', 'application/json'); ``` diff --git a/src/guide/start/databases.md b/src/guide/start/databases.md index 0b2efd8a..1c7d49c9 100644 --- a/src/guide/start/databases.md +++ b/src/guide/start/databases.md @@ -176,11 +176,10 @@ final class M251102141707Page implements RevertibleMigrationInterface $b->createTable('page', [ 'id' => $cb::uuidPrimaryKey(), 'title' => $cb::string()->notNull(), - 'slug' => $cb::string()->notNull(), + 'slug' => $cb::string()->notNull()->unique(), 'text' => $cb::text()->notNull(), 'created_at' => $cb::dateTime(), 'updated_at' => $cb::dateTime(), - 'deleted_at' => $cb::dateTime(), ]); } @@ -191,11 +190,18 @@ final class M251102141707Page implements RevertibleMigrationInterface } ``` -Note that we use UUID as the primary key. While the storage space is a bit bigger than using int, the workflow with -such IDs is beneficial. You generate the ID yourself so you can define a set of related data and save it in a single -transaction. The entities that define this set of data in the code are often called an "aggregate". +Note that we use UUID as the primary key. We are going to generate these IDs ourselves instead of relying on database +so we'll need an extra compose package for that. -Apply it with `make yii migrate:up`. +```shell +make composer require ramsey/uuid +``` + +While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. Since you generate +the ID yourself so you can define a set of related data and save it in a single transaction. +The entities that define this set of data in the code are often called an "aggregate". + +Apply the migration with `make yii migrate:up`. ## An entity @@ -219,7 +225,6 @@ final readonly class Page public string $text, public DateTimeImmutable $createdAt, public DateTimeImmutable $updatedAt, - public ?DateTimeImmutable $deletedAt = null, ) {} public static function create( @@ -228,7 +233,6 @@ final readonly class Page string $text, ?DateTimeImmutable $createdAt = null, ?DateTimeImmutable $updatedAt = null, - ?DateTimeImmutable $deletedAt = null, ): self { return new self( id: $id, @@ -236,7 +240,6 @@ final readonly class Page text: $text, createdAt: $createdAt ?? new DateTimeImmutable(), updatedAt: $updatedAt ?? new DateTimeImmutable(), - deletedAt: $deletedAt, ); } @@ -244,11 +247,6 @@ final readonly class Page { return (new Inflector())->toSlug($this->title); } - - public function isDeleted(): bool - { - return $this->deletedAt !== null; - } } ``` @@ -278,30 +276,30 @@ final readonly class PageRepository public function save(Page $page): void { - $this->connection->createCommand()->upsert('{{%page}}', [ + $data = [ 'id' => $page->id, 'title' => $page->title, 'slug' => $page->getSlug(), 'text' => $page->text, 'created_at' => $page->createdAt, 'updated_at' => $page->updatedAt, - 'deleted_at' => $page->deletedAt, - ])->execute(); + ]; + + if ($this->exists($page->id)) { + $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute(); + } else { + $this->connection->createCommand()->insert('{{%page}}', $data)->execute(); + } } public function findOneBySlug(string $slug): ?Page { - $data = (new Query($this->connection)) + $query = (new Query($this->connection)) ->select('*') ->from('{{%page}}') - ->where('slug = :slug', ['slug' => $slug]) - ->one(); + ->where('slug = :slug', ['slug' => $slug]); - if ($data === null) { - return null; - } - - return $this->createPage($data); + return $this->createPage($query->one()); } /** @@ -319,37 +317,55 @@ final readonly class PageRepository } } - private function createPage(array $data): Page + private function createPage(?array $data): ?Page { + if ($data === null) { + return null; + } + return Page::create( id: $data['id'], title: $data['title'], text: $data['text'], createdAt: new DateTimeImmutable($data['created_at']), updatedAt: new DateTimeImmutable($data['updated_at']), - deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null, ); } public function deleteBySlug(string $slug): void { - $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); + $this->connection->createCommand()->delete( + '{{%page}}', + ['slug' => $slug], + )->execute(); + } + + public function exists(string $id): bool + { + return $this->connection->createQuery() + ->from('{{%page}}') + ->where(['id' => $id]) + ->exists(); } } ``` +In this repository there are both methods to get data and `save()` to do insert or update. DB returns raw data +as arrays but our repository automatically creates entities from this raw data so later we operate typed data. + ## Actions and routes -You need to be able to: +We need some actions to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. -5. Edit a page. +5. Update a page. -Let's tackle these one by one. +Then we need routing for all these. +Let's tackle these one by one. ### List all pages @@ -389,17 +405,21 @@ Define list view in `src/Web/Page/list.php`: $pages */ +/** @var UrlGeneratorInterface $urlGenerator */ ?>
  • - title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> + title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>
+ +generate('page/edit', ['slug' => 'new'])) ?> ``` ### View a page @@ -424,18 +444,15 @@ final readonly class ViewAction public function __construct( private ViewRenderer $viewRenderer, private PageRepository $pageRepository, - private ResponseFactoryInterface $responseFactory - ) - { - } + private ResponseFactoryInterface $responseFactory, + ) {} public function __invoke( #[RouteArgument('slug')] - string $slug - ): ResponseInterface - { + string $slug, + ): ResponseInterface { $page = $this->pageRepository->findOneBySlug($slug); - if ($page === null || $page->isDeleted()) { + if ($page === null) { return $this->responseFactory->createResponse(Status::NOT_FOUND); } @@ -452,17 +469,38 @@ Now, a template in `src/Web/Page/view.php`: -

title) ?>

+

generate('page/list')) ?> → title) ?>

text) ?>

+ +generate('page/edit', ['slug' => $page->getSlug()])) ?> | + + +post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()])) + ->csrf($csrf); +?> +open() ?> + +close() ?> ``` +In this view we have a form that submits a request for page deletion. Handing it with `GET` is common as well, +but it is very wrong. Since deletion changes data, it needs to be handled by one of the non-idempotent HTTP methods. +We use POST and a form in our example, but it could be `DELETE` and async request made with JavaScript. +The button could be later styled properly to look similar to the "Edit". + ### Delete a page Create `src/Web/Page/DeleteAction.php`: @@ -477,6 +515,7 @@ namespace App\Web\Page; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Yiisoft\Http\Status; +use Yiisoft\Router\HydratorAttribute\RouteArgument; use Yiisoft\Router\UrlGeneratorInterface; final readonly class DeleteAction @@ -485,21 +524,186 @@ final readonly class DeleteAction private PageRepository $pageRepository, private ResponseFactoryInterface $responseFactory, private UrlGeneratorInterface $urlGenerator, - ) - {} + ) {} - public function __invoke(string $slug): ResponseInterface + public function __invoke( + #[RouteArgument('slug')] + string $slug + ): ResponseInterface { $this->pageRepository->deleteBySlug($slug); return $this->responseFactory - ->createResponse() - ->withStatus(Status::PERMANENT_REDIRECT) + ->createResponse(Status::SEE_OTHER) ->withHeader('Location', $this->urlGenerator->generate('page/list')); } } ``` -### Create a page +### Create or update a page + +Create `src/Web/Page/EditAction.php`: + +```php +findOneBySlug($slug); + if ($page === null) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + $form->title = $page->title; + $form->text = $page->text; + } + + $this->formHydrator->populateFromPostAndValidate($form, $request); + + if ($form->isValid()) { + $id = $isNew ? Uuid::uuid7()->toString() : $page->id; + + $page = Page::create( + id: $id, + title: $form->title, + text: $form->text, + updatedAt: new DateTimeImmutable(), + ); + + $pageRepository->save($page); + + return $this->responseFactory + ->createResponse(Status::SEE_OTHER) + ->withHeader( + 'Location', + $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]), + ); + } + + return $this->viewRenderer->render(__DIR__ . '/edit', [ + 'form' => $form, + 'isNew' => $isNew, + 'slug' => $slug, + ]); + } +} +``` + +In the above we use a special slug in the URL for new pages so the URL looks like `http://localhost/pages/new`. If the +page isn't new, we pre-fill the form with the data from the database. Similar to how we did in [Working with forms](forms.md), +we handle the form submission. After successful save we redirect to the page view. + +Now, a template in `src/Web/Page/edit.php`: + +```php +post($urlGenerator->generate('page/edit', ['slug' => $slug])) + ->csrf($csrf); +?> + +open() ?> + required() ?> + required() ?> + +close() ?> +``` + +### Routing + +Adjust `config/common/routes.php`: + +```php +routes( + Route::get('/') + ->action(Web\HomePage\Action::class) + ->name('home'), + Route::methods([Method::GET, Method::POST], '/say') + ->action(Web\Echo\Action::class) + ->name('echo/say'), + + Group::create('/pages')->routes( + Route::get('') + ->action(Web\Page\ListAction::class) + ->name('page/list'), + Route::get('/{slug}') + ->action(Web\Page\ViewAction::class) + ->name('page/view'), + Route::methods([Method::GET, Method::POST], '/{slug}/edit') + ->action(Web\Page\EditAction::class) + ->name('page/edit'), + Route::post('/{slug}/delete') + ->action(Web\Page\DeleteAction::class) + ->name('page/delete'), + ), + ), +]; +``` + +Note that we've grouped all page-related routes with a group under `/pages` prefix. That is a convenient way to both not +to repeat yourself and add some extra middleware, such as authentication, to the whole group. + +## Trying it out +Now try it out by opening `http://localhost/pages` in your browser. diff --git a/src/guide/start/forms.md b/src/guide/start/forms.md index c662caa4..235e3e7a 100644 --- a/src/guide/start/forms.md +++ b/src/guide/start/forms.md @@ -30,6 +30,8 @@ saved in the file `/src/App/Web/Echo/Form.php`: ```php Date: Fri, 26 Dec 2025 22:38:16 +0000 Subject: [PATCH 7/7] Update translation --- .../po/es/guide_runtime_response.md.po | 78 ++-- .../po/es/guide_start_databases.md.po | 337 +++++++++++++--- _translations/po/es/guide_start_forms.md.po | 4 +- .../po/id/guide_runtime_response.md.po | 78 ++-- .../po/id/guide_start_databases.md.po | 339 +++++++++++++--- _translations/po/id/guide_start_forms.md.po | 4 +- .../po/ru/guide_runtime_response.md.po | 78 ++-- .../po/ru/guide_start_databases.md.po | 338 +++++++++++++--- _translations/po/ru/guide_start_forms.md.po | 4 +- .../pot/guide_runtime_response.md.pot | 78 ++-- .../pot/guide_start_databases.md.pot | 365 +++++++++++++++--- _translations/pot/guide_start_forms.md.pot | 4 +- src/es/guide/runtime/response.md | 15 +- src/es/guide/start/databases.md | 309 ++++++++++++--- src/es/guide/start/forms.md | 2 + src/id/guide/runtime/response.md | 15 +- src/id/guide/start/databases.md | 309 ++++++++++++--- src/id/guide/start/forms.md | 2 + src/ru/guide/runtime/response.md | 15 +- src/ru/guide/start/databases.md | 309 ++++++++++++--- src/ru/guide/start/forms.md | 2 + 21 files changed, 2228 insertions(+), 457 deletions(-) diff --git a/_translations/po/es/guide_runtime_response.md.po b/_translations/po/es/guide_runtime_response.md.po index f58451d8..84fbf935 100644 --- a/_translations/po/es/guide_runtime_response.md.po +++ b/_translations/po/es/guide_runtime_response.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-09-04 11:19+0500\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,30 +17,31 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md #, no-wrap msgid "Headers" msgstr "" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md +#: ../src/internals/006-git-commit-messages.md #, no-wrap msgid "Body" msgstr "" #. type: Title # -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Response" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "HTTP response has status code and message, a set of headers and a body:" msgstr "" #. type: Fenced code block -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "HTTP/1.1 200 OK\n" @@ -55,17 +56,17 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Yii uses [PSR-7 `Response`](https://www.php-fig.org/psr/psr-7/) in the web application to represent response." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "The object should be constructed and returned as a result of the execution of controller actions or other middleware. Usually, the middleware has a response factory injected into its constructor." msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" @@ -90,18 +91,18 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Status code" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set a status code like the following:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -110,55 +111,55 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Majority of status codes are available from `Status` class for convenience and readability." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set headers like this:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withHeader('Content-type', 'application/json');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "If there is a need to append a header value to the existing header:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withAddedHeader('Set-Cookie', 'qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "And, if needed, headers could be removed:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withoutHeader('Set-Cookie');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Response body is an object implementing `Psr\\Http\\Message\\StreamInterface`." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can write to it via the interface itself:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "$body = $response->getBody();\n" @@ -166,19 +167,19 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Examples" msgstr "" #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Redirecting" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -188,16 +189,35 @@ msgid "" " ->withHeader('Location', 'https://www.example.com'); \n" msgstr "" +#. type: Plain text +#: ../src/guide/runtime/response.md +msgid "Note that there are different statuses used for redirection:" +msgstr "" + +#. type: Plain text +#: ../src/guide/runtime/response.md +#, no-wrap +msgid "" +"| Code | Usage | What is it for |\n" +"|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n" +"| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. |\n" +"| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. |\n" +"| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. |\n" +"| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. |\n" +"| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. |\n" +msgstr "" + #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Responding with JSON" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" +"use Yiisoft\\Http\\Status;\n" "use Yiisoft\\Json\\Json;\n" "\n" "$data = [\n" @@ -207,6 +227,6 @@ msgid "" "\n" "$response->getBody()->write(Json::encode($data));\n" "return $response\n" -" ->withStatus(200)\n" -" ->withHeader('Content-Type', 'application/json');\n" +" ->withStatus(Status::OK)\n" +" ->withHeader('Content-Type', 'application/json');\n" msgstr "" diff --git a/_translations/po/es/guide_start_databases.md.po b/_translations/po/es/guide_start_databases.md.po index d244da8f..dc14e8c5 100644 --- a/_translations/po/es/guide_start_databases.md.po +++ b/_translations/po/es/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -300,11 +300,10 @@ msgid "" " $b->createTable('page', [\n" " 'id' => $cb::uuidPrimaryKey(),\n" " 'title' => $cb::string()->notNull(),\n" -" 'slug' => $cb::string()->notNull(),\n" +" 'slug' => $cb::string()->notNull()->unique(),\n" " 'text' => $cb::text()->notNull(),\n" " 'created_at' => $cb::dateTime(),\n" " 'updated_at' => $cb::dateTime(),\n" -" 'deleted_at' => $cb::dateTime(),\n" " ]);\n" " }\n" "\n" @@ -317,12 +316,23 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Note that we use UUID as the primary key. While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. You generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgid "Note that we use UUID as the primary key. We are going to generate these IDs ourselves instead of relying on database so we'll need an extra compose package for that." +msgstr "" + +#. type: Fenced code block (shell) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "make composer require ramsey/uuid\n" msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Apply it with `make yii migrate:up`." +msgid "While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. Since you generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Apply the migration with `make yii migrate:up`." msgstr "" #. type: Title ## @@ -357,7 +367,6 @@ msgid "" " public string $text,\n" " public DateTimeImmutable $createdAt,\n" " public DateTimeImmutable $updatedAt,\n" -" public ?DateTimeImmutable $deletedAt = null,\n" " ) {}\n" "\n" " public static function create(\n" @@ -366,7 +375,6 @@ msgid "" " string $text,\n" " ?DateTimeImmutable $createdAt = null,\n" " ?DateTimeImmutable $updatedAt = null,\n" -" ?DateTimeImmutable $deletedAt = null,\n" " ): self {\n" " return new self(\n" " id: $id,\n" @@ -374,7 +382,6 @@ msgid "" " text: $text,\n" " createdAt: $createdAt ?? new DateTimeImmutable(),\n" " updatedAt: $updatedAt ?? new DateTimeImmutable(),\n" -" deletedAt: $deletedAt,\n" " );\n" " }\n" "\n" @@ -382,11 +389,6 @@ msgid "" " {\n" " return (new Inflector())->toSlug($this->title);\n" " }\n" -"\n" -" public function isDeleted(): bool\n" -" {\n" -" return $this->deletedAt !== null;\n" -" }\n" "}\n" msgstr "" @@ -429,30 +431,30 @@ msgid "" "\n" " public function save(Page $page): void\n" " {\n" -" $this->connection->createCommand()->upsert('{{%page}}', [\n" +" $data = [\n" " 'id' => $page->id,\n" " 'title' => $page->title,\n" " 'slug' => $page->getSlug(),\n" " 'text' => $page->text,\n" " 'created_at' => $page->createdAt,\n" " 'updated_at' => $page->updatedAt,\n" -" 'deleted_at' => $page->deletedAt,\n" -" ])->execute();\n" +" ];\n" +"\n" +" if ($this->exists($page->id)) {\n" +" $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute();\n" +" } else {\n" +" $this->connection->createCommand()->insert('{{%page}}', $data)->execute();\n" +" }\n" " }\n" "\n" " public function findOneBySlug(string $slug): ?Page\n" " {\n" -" $data = (new Query($this->connection))\n" +" $query = (new Query($this->connection))\n" " ->select('*')\n" " ->from('{{%page}}')\n" -" ->where('slug = :slug', ['slug' => $slug])\n" -" ->one();\n" -"\n" -" if ($data === null) {\n" -" return null;\n" -" }\n" +" ->where('slug = :slug', ['slug' => $slug]);\n" "\n" -" return $this->createPage($data);\n" +" return $this->createPage($query->one());\n" " }\n" "\n" " /**\n" @@ -470,25 +472,44 @@ msgid "" " }\n" " }\n" "\n" -" private function createPage(array $data): Page\n" +" private function createPage(?array $data): ?Page\n" " {\n" +" if ($data === null) {\n" +" return null;\n" +" }\n" +"\n" " return Page::create(\n" " id: $data['id'],\n" " title: $data['title'],\n" " text: $data['text'],\n" " createdAt: new DateTimeImmutable($data['created_at']),\n" " updatedAt: new DateTimeImmutable($data['updated_at']),\n" -" deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,\n" " );\n" " }\n" "\n" " public function deleteBySlug(string $slug): void\n" " {\n" -" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" +" $this->connection->createCommand()->delete(\n" +" '{{%page}}',\n" +" ['slug' => $slug],\n" +" )->execute();\n" +" }\n" +"\n" +" public function exists(string $id): bool\n" +" {\n" +" return $this->connection->createQuery()\n" +" ->from('{{%page}}')\n" +" ->where(['id' => $id])\n" +" ->exists();\n" " }\n" "}\n" msgstr "" +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this repository there are both methods to get data and `save()` to do insert or update. DB returns raw data as arrays but our repository automatically creates entities from this raw data so later we operate typed data." +msgstr "" + #. type: Title ## #: ../src/guide/start/databases.md #, no-wrap @@ -497,7 +518,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need to be able to:" +msgid "We need some actions to:" msgstr "" #. type: Bullet: '1. ' @@ -522,7 +543,12 @@ msgstr "" #. type: Bullet: '5. ' #: ../src/guide/start/databases.md -msgid "Edit a page." +msgid "Update a page." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Then we need routing for all these." msgstr "" #. type: Plain text @@ -584,17 +610,21 @@ msgid "" " $pages */\n" +"/** @var UrlGeneratorInterface $urlGenerator */\n" "?>\n" "\n" "
    \n" " \n" "
  • \n" -" title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?>\n" +" title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>\n" "
  • \n" " \n" "
\n" +"\n" +"generate('page/edit', ['slug' => 'new'])) ?>\n" msgstr "" #. type: Title ### @@ -629,18 +659,15 @@ msgid "" " public function __construct(\n" " private ViewRenderer $viewRenderer,\n" " private PageRepository $pageRepository,\n" -" private ResponseFactoryInterface $responseFactory\n" -" )\n" -" {\n" -" }\n" +" private ResponseFactoryInterface $responseFactory,\n" +" ) {}\n" "\n" " public function __invoke(\n" " #[RouteArgument('slug')]\n" -" string $slug\n" -" ): ResponseInterface\n" -" {\n" +" string $slug,\n" +" ): ResponseInterface {\n" " $page = $this->pageRepository->findOneBySlug($slug);\n" -" if ($page === null || $page->isDeleted()) {\n" +" if ($page === null) {\n" " return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" " }\n" "\n" @@ -663,15 +690,36 @@ msgid "" "\n" "\n" -"

title) ?>

\n" +"

generate('page/list')) ?> → title) ?>

\n" "\n" "

\n" " text) ?>\n" "

\n" +"\n" +"generate('page/edit', ['slug' => $page->getSlug()])) ?> |\n" +"\n" +"\n" +"post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()]))\n" +" ->csrf($csrf);\n" +"?>\n" +"open() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this view we have a form that submits a request for page deletion. Handing it with `GET` is common as well, but it is very wrong. Since deletion changes data, it needs to be handled by one of the non-idempotent HTTP methods. We use POST and a form in our example, but it could be `DELETE` and async request made with JavaScript. The button could be later styled properly to look similar to the \"Edit\"." msgstr "" #. type: Title ### @@ -698,6 +746,7 @@ msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" "use Psr\\Http\\Message\\ResponseInterface;\n" "use Yiisoft\\Http\\Status;\n" +"use Yiisoft\\Router\\HydratorAttribute\\RouteArgument;\n" "use Yiisoft\\Router\\UrlGeneratorInterface;\n" "\n" "final readonly class DeleteAction\n" @@ -706,16 +755,17 @@ msgid "" " private PageRepository $pageRepository,\n" " private ResponseFactoryInterface $responseFactory,\n" " private UrlGeneratorInterface $urlGenerator,\n" -" )\n" -" {}\n" +" ) {}\n" "\n" -" public function __invoke(string $slug): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('slug')]\n" +" string $slug\n" +" ): ResponseInterface\n" " {\n" " $this->pageRepository->deleteBySlug($slug);\n" "\n" " return $this->responseFactory\n" -" ->createResponse()\n" -" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->createResponse(Status::SEE_OTHER)\n" " ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" " }\n" "}\n" @@ -724,5 +774,202 @@ msgstr "" #. type: Title ### #: ../src/guide/start/databases.md #, no-wrap -msgid "Create a page" +msgid "Create or update a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/EditAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"findOneBySlug($slug);\n" +" if ($page === null) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" $form->title = $page->title;\n" +" $form->text = $page->text;\n" +" }\n" +"\n" +" $this->formHydrator->populateFromPostAndValidate($form, $request);\n" +"\n" +" if ($form->isValid()) {\n" +" $id = $isNew ? Uuid::uuid7()->toString() : $page->id;\n" +"\n" +" $page = Page::create(\n" +" id: $id,\n" +" title: $form->title,\n" +" text: $form->text,\n" +" updatedAt: new DateTimeImmutable(),\n" +" );\n" +"\n" +" $pageRepository->save($page);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse(Status::SEE_OTHER)\n" +" ->withHeader(\n" +" 'Location',\n" +" $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]),\n" +" );\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/edit', [\n" +" 'form' => $form,\n" +" 'isNew' => $isNew,\n" +" 'slug' => $slug,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In the above we use a special slug in the URL for new pages so the URL looks like `http://localhost/pages/new`. If the page isn't new, we pre-fill the form with the data from the database. Similar to how we did in [Working with forms](forms.md), we handle the form submission. After successful save we redirect to the page view." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/edit.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"post($urlGenerator->generate('page/edit', ['slug' => $slug]))\n" +" ->csrf($csrf);\n" +"?>\n" +"\n" +"open() ?>\n" +" required() ?>\n" +" required() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Routing" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Adjust `config/common/routes.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"routes(\n" +" Route::get('/')\n" +" ->action(Web\\HomePage\\Action::class)\n" +" ->name('home'),\n" +" Route::methods([Method::GET, Method::POST], '/say')\n" +" ->action(Web\\Echo\\Action::class)\n" +" ->name('echo/say'),\n" +"\n" +" Group::create('/pages')->routes(\n" +" Route::get('')\n" +" ->action(Web\\Page\\ListAction::class)\n" +" ->name('page/list'),\n" +" Route::get('/{slug}')\n" +" ->action(Web\\Page\\ViewAction::class)\n" +" ->name('page/view'),\n" +" Route::methods([Method::GET, Method::POST], '/{slug}/edit')\n" +" ->action(Web\\Page\\EditAction::class)\n" +" ->name('page/edit'),\n" +" Route::post('/{slug}/delete')\n" +" ->action(Web\\Page\\DeleteAction::class)\n" +" ->name('page/delete'),\n" +" ),\n" +" ),\n" +"];\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Note that we've grouped all page-related routes with a group under `/pages` prefix. That is a convenient way to both not to repeat yourself and add some extra middleware, such as authentication, to the whole group." +msgstr "" + +#. type: Title ## +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Trying it out" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now try it out by opening `http://localhost/pages` in your browser." msgstr "" diff --git a/_translations/po/es/guide_start_forms.md.po b/_translations/po/es/guide_start_forms.md.po index 3360d664..2c5f8df1 100644 --- a/_translations/po/es/guide_start_forms.md.po +++ b/_translations/po/es/guide_start_forms.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -92,6 +92,8 @@ msgstr "" msgid "" "withHeader('Content-type', 'application/json');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "If there is a need to append a header value to the existing header:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withAddedHeader('Set-Cookie', 'qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "And, if needed, headers could be removed:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withoutHeader('Set-Cookie');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Response body is an object implementing `Psr\\Http\\Message\\StreamInterface`." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can write to it via the interface itself:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "$body = $response->getBody();\n" @@ -165,19 +166,19 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Examples" msgstr "" #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Redirecting" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -187,16 +188,35 @@ msgid "" " ->withHeader('Location', 'https://www.example.com'); \n" msgstr "" +#. type: Plain text +#: ../src/guide/runtime/response.md +msgid "Note that there are different statuses used for redirection:" +msgstr "" + +#. type: Plain text +#: ../src/guide/runtime/response.md +#, no-wrap +msgid "" +"| Code | Usage | What is it for |\n" +"|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n" +"| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. |\n" +"| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. |\n" +"| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. |\n" +"| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. |\n" +"| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. |\n" +msgstr "" + #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Responding with JSON" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" +"use Yiisoft\\Http\\Status;\n" "use Yiisoft\\Json\\Json;\n" "\n" "$data = [\n" @@ -206,6 +226,6 @@ msgid "" "\n" "$response->getBody()->write(Json::encode($data));\n" "return $response\n" -" ->withStatus(200)\n" -" ->withHeader('Content-Type', 'application/json');\n" +" ->withStatus(Status::OK)\n" +" ->withHeader('Content-Type', 'application/json');\n" msgstr "" diff --git a/_translations/po/id/guide_start_databases.md.po b/_translations/po/id/guide_start_databases.md.po index 5b883d9c..2494b8e4 100644 --- a/_translations/po/id/guide_start_databases.md.po +++ b/_translations/po/id/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -301,11 +301,10 @@ msgid "" " $b->createTable('page', [\n" " 'id' => $cb::uuidPrimaryKey(),\n" " 'title' => $cb::string()->notNull(),\n" -" 'slug' => $cb::string()->notNull(),\n" +" 'slug' => $cb::string()->notNull()->unique(),\n" " 'text' => $cb::text()->notNull(),\n" " 'created_at' => $cb::dateTime(),\n" " 'updated_at' => $cb::dateTime(),\n" -" 'deleted_at' => $cb::dateTime(),\n" " ]);\n" " }\n" "\n" @@ -318,12 +317,24 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Note that we use UUID as the primary key. While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. You generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgid "Note that we use UUID as the primary key. We are going to generate these IDs ourselves instead of relying on database so we'll need an extra compose package for that." msgstr "" +#. type: Fenced code block (shell) +#: ../src/guide/start/databases.md +#, fuzzy, no-wrap +#| msgid "composer require yiisoft/cache\n" +msgid "make composer require ramsey/uuid\n" +msgstr "composer require yiisoft/cache\n" + #. type: Plain text #: ../src/guide/start/databases.md -msgid "Apply it with `make yii migrate:up`." +msgid "While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. Since you generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Apply the migration with `make yii migrate:up`." msgstr "" #. type: Title ## @@ -358,7 +369,6 @@ msgid "" " public string $text,\n" " public DateTimeImmutable $createdAt,\n" " public DateTimeImmutable $updatedAt,\n" -" public ?DateTimeImmutable $deletedAt = null,\n" " ) {}\n" "\n" " public static function create(\n" @@ -367,7 +377,6 @@ msgid "" " string $text,\n" " ?DateTimeImmutable $createdAt = null,\n" " ?DateTimeImmutable $updatedAt = null,\n" -" ?DateTimeImmutable $deletedAt = null,\n" " ): self {\n" " return new self(\n" " id: $id,\n" @@ -375,7 +384,6 @@ msgid "" " text: $text,\n" " createdAt: $createdAt ?? new DateTimeImmutable(),\n" " updatedAt: $updatedAt ?? new DateTimeImmutable(),\n" -" deletedAt: $deletedAt,\n" " );\n" " }\n" "\n" @@ -383,11 +391,6 @@ msgid "" " {\n" " return (new Inflector())->toSlug($this->title);\n" " }\n" -"\n" -" public function isDeleted(): bool\n" -" {\n" -" return $this->deletedAt !== null;\n" -" }\n" "}\n" msgstr "" @@ -430,30 +433,30 @@ msgid "" "\n" " public function save(Page $page): void\n" " {\n" -" $this->connection->createCommand()->upsert('{{%page}}', [\n" +" $data = [\n" " 'id' => $page->id,\n" " 'title' => $page->title,\n" " 'slug' => $page->getSlug(),\n" " 'text' => $page->text,\n" " 'created_at' => $page->createdAt,\n" " 'updated_at' => $page->updatedAt,\n" -" 'deleted_at' => $page->deletedAt,\n" -" ])->execute();\n" +" ];\n" +"\n" +" if ($this->exists($page->id)) {\n" +" $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute();\n" +" } else {\n" +" $this->connection->createCommand()->insert('{{%page}}', $data)->execute();\n" +" }\n" " }\n" "\n" " public function findOneBySlug(string $slug): ?Page\n" " {\n" -" $data = (new Query($this->connection))\n" +" $query = (new Query($this->connection))\n" " ->select('*')\n" " ->from('{{%page}}')\n" -" ->where('slug = :slug', ['slug' => $slug])\n" -" ->one();\n" -"\n" -" if ($data === null) {\n" -" return null;\n" -" }\n" +" ->where('slug = :slug', ['slug' => $slug]);\n" "\n" -" return $this->createPage($data);\n" +" return $this->createPage($query->one());\n" " }\n" "\n" " /**\n" @@ -471,25 +474,44 @@ msgid "" " }\n" " }\n" "\n" -" private function createPage(array $data): Page\n" +" private function createPage(?array $data): ?Page\n" " {\n" +" if ($data === null) {\n" +" return null;\n" +" }\n" +"\n" " return Page::create(\n" " id: $data['id'],\n" " title: $data['title'],\n" " text: $data['text'],\n" " createdAt: new DateTimeImmutable($data['created_at']),\n" " updatedAt: new DateTimeImmutable($data['updated_at']),\n" -" deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,\n" " );\n" " }\n" "\n" " public function deleteBySlug(string $slug): void\n" " {\n" -" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" +" $this->connection->createCommand()->delete(\n" +" '{{%page}}',\n" +" ['slug' => $slug],\n" +" )->execute();\n" +" }\n" +"\n" +" public function exists(string $id): bool\n" +" {\n" +" return $this->connection->createQuery()\n" +" ->from('{{%page}}')\n" +" ->where(['id' => $id])\n" +" ->exists();\n" " }\n" "}\n" msgstr "" +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this repository there are both methods to get data and `save()` to do insert or update. DB returns raw data as arrays but our repository automatically creates entities from this raw data so later we operate typed data." +msgstr "" + #. type: Title ## #: ../src/guide/start/databases.md #, no-wrap @@ -498,7 +520,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need to be able to:" +msgid "We need some actions to:" msgstr "" #. type: Bullet: '1. ' @@ -523,7 +545,12 @@ msgstr "" #. type: Bullet: '5. ' #: ../src/guide/start/databases.md -msgid "Edit a page." +msgid "Update a page." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Then we need routing for all these." msgstr "" #. type: Plain text @@ -585,17 +612,21 @@ msgid "" " $pages */\n" +"/** @var UrlGeneratorInterface $urlGenerator */\n" "?>\n" "\n" "
    \n" " \n" "
  • \n" -" title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?>\n" +" title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>\n" "
  • \n" " \n" "
\n" +"\n" +"generate('page/edit', ['slug' => 'new'])) ?>\n" msgstr "" #. type: Title ### @@ -630,18 +661,15 @@ msgid "" " public function __construct(\n" " private ViewRenderer $viewRenderer,\n" " private PageRepository $pageRepository,\n" -" private ResponseFactoryInterface $responseFactory\n" -" )\n" -" {\n" -" }\n" +" private ResponseFactoryInterface $responseFactory,\n" +" ) {}\n" "\n" " public function __invoke(\n" " #[RouteArgument('slug')]\n" -" string $slug\n" -" ): ResponseInterface\n" -" {\n" +" string $slug,\n" +" ): ResponseInterface {\n" " $page = $this->pageRepository->findOneBySlug($slug);\n" -" if ($page === null || $page->isDeleted()) {\n" +" if ($page === null) {\n" " return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" " }\n" "\n" @@ -664,15 +692,36 @@ msgid "" "\n" "\n" -"

title) ?>

\n" +"

generate('page/list')) ?> → title) ?>

\n" "\n" "

\n" " text) ?>\n" "

\n" +"\n" +"generate('page/edit', ['slug' => $page->getSlug()])) ?> |\n" +"\n" +"\n" +"post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()]))\n" +" ->csrf($csrf);\n" +"?>\n" +"open() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this view we have a form that submits a request for page deletion. Handing it with `GET` is common as well, but it is very wrong. Since deletion changes data, it needs to be handled by one of the non-idempotent HTTP methods. We use POST and a form in our example, but it could be `DELETE` and async request made with JavaScript. The button could be later styled properly to look similar to the \"Edit\"." msgstr "" #. type: Title ### @@ -699,6 +748,7 @@ msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" "use Psr\\Http\\Message\\ResponseInterface;\n" "use Yiisoft\\Http\\Status;\n" +"use Yiisoft\\Router\\HydratorAttribute\\RouteArgument;\n" "use Yiisoft\\Router\\UrlGeneratorInterface;\n" "\n" "final readonly class DeleteAction\n" @@ -707,16 +757,17 @@ msgid "" " private PageRepository $pageRepository,\n" " private ResponseFactoryInterface $responseFactory,\n" " private UrlGeneratorInterface $urlGenerator,\n" -" )\n" -" {}\n" +" ) {}\n" "\n" -" public function __invoke(string $slug): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('slug')]\n" +" string $slug\n" +" ): ResponseInterface\n" " {\n" " $this->pageRepository->deleteBySlug($slug);\n" "\n" " return $this->responseFactory\n" -" ->createResponse()\n" -" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->createResponse(Status::SEE_OTHER)\n" " ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" " }\n" "}\n" @@ -725,5 +776,203 @@ msgstr "" #. type: Title ### #: ../src/guide/start/databases.md #, no-wrap -msgid "Create a page" +msgid "Create or update a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/EditAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"findOneBySlug($slug);\n" +" if ($page === null) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" $form->title = $page->title;\n" +" $form->text = $page->text;\n" +" }\n" +"\n" +" $this->formHydrator->populateFromPostAndValidate($form, $request);\n" +"\n" +" if ($form->isValid()) {\n" +" $id = $isNew ? Uuid::uuid7()->toString() : $page->id;\n" +"\n" +" $page = Page::create(\n" +" id: $id,\n" +" title: $form->title,\n" +" text: $form->text,\n" +" updatedAt: new DateTimeImmutable(),\n" +" );\n" +"\n" +" $pageRepository->save($page);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse(Status::SEE_OTHER)\n" +" ->withHeader(\n" +" 'Location',\n" +" $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]),\n" +" );\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/edit', [\n" +" 'form' => $form,\n" +" 'isNew' => $isNew,\n" +" 'slug' => $slug,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In the above we use a special slug in the URL for new pages so the URL looks like `http://localhost/pages/new`. If the page isn't new, we pre-fill the form with the data from the database. Similar to how we did in [Working with forms](forms.md), we handle the form submission. After successful save we redirect to the page view." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/edit.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"post($urlGenerator->generate('page/edit', ['slug' => $slug]))\n" +" ->csrf($csrf);\n" +"?>\n" +"\n" +"open() ?>\n" +" required() ?>\n" +" required() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, fuzzy, no-wrap +#| msgid "Routes" +msgid "Routing" +msgstr "Rute" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Adjust `config/common/routes.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"routes(\n" +" Route::get('/')\n" +" ->action(Web\\HomePage\\Action::class)\n" +" ->name('home'),\n" +" Route::methods([Method::GET, Method::POST], '/say')\n" +" ->action(Web\\Echo\\Action::class)\n" +" ->name('echo/say'),\n" +"\n" +" Group::create('/pages')->routes(\n" +" Route::get('')\n" +" ->action(Web\\Page\\ListAction::class)\n" +" ->name('page/list'),\n" +" Route::get('/{slug}')\n" +" ->action(Web\\Page\\ViewAction::class)\n" +" ->name('page/view'),\n" +" Route::methods([Method::GET, Method::POST], '/{slug}/edit')\n" +" ->action(Web\\Page\\EditAction::class)\n" +" ->name('page/edit'),\n" +" Route::post('/{slug}/delete')\n" +" ->action(Web\\Page\\DeleteAction::class)\n" +" ->name('page/delete'),\n" +" ),\n" +" ),\n" +"];\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Note that we've grouped all page-related routes with a group under `/pages` prefix. That is a convenient way to both not to repeat yourself and add some extra middleware, such as authentication, to the whole group." +msgstr "" + +#. type: Title ## +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Trying it out" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now try it out by opening `http://localhost/pages` in your browser." msgstr "" diff --git a/_translations/po/id/guide_start_forms.md.po b/_translations/po/id/guide_start_forms.md.po index d6e665a9..434dfd88 100644 --- a/_translations/po/id/guide_start_forms.md.po +++ b/_translations/po/id/guide_start_forms.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -91,6 +91,8 @@ msgstr "" msgid "" "=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md #, no-wrap msgid "Headers" msgstr "" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md +#: ../src/internals/006-git-commit-messages.md #, no-wrap msgid "Body" msgstr "" #. type: Title # -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Response" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "HTTP response has status code and message, a set of headers and a body:" msgstr "" #. type: Fenced code block -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "HTTP/1.1 200 OK\n" @@ -55,17 +56,17 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Yii uses [PSR-7 `Response`](https://www.php-fig.org/psr/psr-7/) in the web application to represent response." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "The object should be constructed and returned as a result of the execution of controller actions or other middleware. Usually, the middleware has a response factory injected into its constructor." msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" @@ -90,18 +91,18 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Status code" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set a status code like the following:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -110,55 +111,55 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Majority of status codes are available from `Status` class for convenience and readability." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set headers like this:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withHeader('Content-type', 'application/json');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "If there is a need to append a header value to the existing header:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withAddedHeader('Set-Cookie', 'qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "And, if needed, headers could be removed:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withoutHeader('Set-Cookie');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "Response body is an object implementing `Psr\\Http\\Message\\StreamInterface`." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can write to it via the interface itself:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "$body = $response->getBody();\n" @@ -166,19 +167,19 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Examples" msgstr "" #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Redirecting" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -188,16 +189,35 @@ msgid "" " ->withHeader('Location', 'https://www.example.com'); \n" msgstr "" +#. type: Plain text +#: ../src/guide/runtime/response.md +msgid "Note that there are different statuses used for redirection:" +msgstr "" + +#. type: Plain text +#: ../src/guide/runtime/response.md +#, no-wrap +msgid "" +"| Code | Usage | What is it for |\n" +"|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n" +"| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. |\n" +"| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. |\n" +"| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. |\n" +"| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. |\n" +"| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. |\n" +msgstr "" + #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Responding with JSON" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" +"use Yiisoft\\Http\\Status;\n" "use Yiisoft\\Json\\Json;\n" "\n" "$data = [\n" @@ -207,6 +227,6 @@ msgid "" "\n" "$response->getBody()->write(Json::encode($data));\n" "return $response\n" -" ->withStatus(200)\n" -" ->withHeader('Content-Type', 'application/json');\n" +" ->withStatus(Status::OK)\n" +" ->withHeader('Content-Type', 'application/json');\n" msgstr "" diff --git a/_translations/po/ru/guide_start_databases.md.po b/_translations/po/ru/guide_start_databases.md.po index 07bd2f73..3e12db28 100644 --- a/_translations/po/ru/guide_start_databases.md.po +++ b/_translations/po/ru/guide_start_databases.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -304,11 +304,10 @@ msgid "" " $b->createTable('page', [\n" " 'id' => $cb::uuidPrimaryKey(),\n" " 'title' => $cb::string()->notNull(),\n" -" 'slug' => $cb::string()->notNull(),\n" +" 'slug' => $cb::string()->notNull()->unique(),\n" " 'text' => $cb::text()->notNull(),\n" " 'created_at' => $cb::dateTime(),\n" " 'updated_at' => $cb::dateTime(),\n" -" 'deleted_at' => $cb::dateTime(),\n" " ]);\n" " }\n" "\n" @@ -321,12 +320,24 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Note that we use UUID as the primary key. While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. You generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgid "Note that we use UUID as the primary key. We are going to generate these IDs ourselves instead of relying on database so we'll need an extra compose package for that." msgstr "" +#. type: Fenced code block (shell) +#: ../src/guide/start/databases.md +#, fuzzy, no-wrap +#| msgid "composer install yiisoft/security\n" +msgid "make composer require ramsey/uuid\n" +msgstr "composer install yiisoft/security\n" + #. type: Plain text #: ../src/guide/start/databases.md -msgid "Apply it with `make yii migrate:up`." +msgid "While the storage space is a bit bigger than using int, the workflow with such IDs is beneficial. Since you generate the ID yourself so you can define a set of related data and save it in a single transaction. The entities that define this set of data in the code are often called an \"aggregate\"." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Apply the migration with `make yii migrate:up`." msgstr "" #. type: Title ## @@ -361,7 +372,6 @@ msgid "" " public string $text,\n" " public DateTimeImmutable $createdAt,\n" " public DateTimeImmutable $updatedAt,\n" -" public ?DateTimeImmutable $deletedAt = null,\n" " ) {}\n" "\n" " public static function create(\n" @@ -370,7 +380,6 @@ msgid "" " string $text,\n" " ?DateTimeImmutable $createdAt = null,\n" " ?DateTimeImmutable $updatedAt = null,\n" -" ?DateTimeImmutable $deletedAt = null,\n" " ): self {\n" " return new self(\n" " id: $id,\n" @@ -378,7 +387,6 @@ msgid "" " text: $text,\n" " createdAt: $createdAt ?? new DateTimeImmutable(),\n" " updatedAt: $updatedAt ?? new DateTimeImmutable(),\n" -" deletedAt: $deletedAt,\n" " );\n" " }\n" "\n" @@ -386,11 +394,6 @@ msgid "" " {\n" " return (new Inflector())->toSlug($this->title);\n" " }\n" -"\n" -" public function isDeleted(): bool\n" -" {\n" -" return $this->deletedAt !== null;\n" -" }\n" "}\n" msgstr "" @@ -433,30 +436,30 @@ msgid "" "\n" " public function save(Page $page): void\n" " {\n" -" $this->connection->createCommand()->upsert('{{%page}}', [\n" +" $data = [\n" " 'id' => $page->id,\n" " 'title' => $page->title,\n" " 'slug' => $page->getSlug(),\n" " 'text' => $page->text,\n" " 'created_at' => $page->createdAt,\n" " 'updated_at' => $page->updatedAt,\n" -" 'deleted_at' => $page->deletedAt,\n" -" ])->execute();\n" +" ];\n" +"\n" +" if ($this->exists($page->id)) {\n" +" $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute();\n" +" } else {\n" +" $this->connection->createCommand()->insert('{{%page}}', $data)->execute();\n" +" }\n" " }\n" "\n" " public function findOneBySlug(string $slug): ?Page\n" " {\n" -" $data = (new Query($this->connection))\n" +" $query = (new Query($this->connection))\n" " ->select('*')\n" " ->from('{{%page}}')\n" -" ->where('slug = :slug', ['slug' => $slug])\n" -" ->one();\n" -"\n" -" if ($data === null) {\n" -" return null;\n" -" }\n" +" ->where('slug = :slug', ['slug' => $slug]);\n" "\n" -" return $this->createPage($data);\n" +" return $this->createPage($query->one());\n" " }\n" "\n" " /**\n" @@ -474,25 +477,44 @@ msgid "" " }\n" " }\n" "\n" -" private function createPage(array $data): Page\n" +" private function createPage(?array $data): ?Page\n" " {\n" +" if ($data === null) {\n" +" return null;\n" +" }\n" +"\n" " return Page::create(\n" " id: $data['id'],\n" " title: $data['title'],\n" " text: $data['text'],\n" " createdAt: new DateTimeImmutable($data['created_at']),\n" " updatedAt: new DateTimeImmutable($data['updated_at']),\n" -" deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,\n" " );\n" " }\n" "\n" " public function deleteBySlug(string $slug): void\n" " {\n" -" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" +" $this->connection->createCommand()->delete(\n" +" '{{%page}}',\n" +" ['slug' => $slug],\n" +" )->execute();\n" +" }\n" +"\n" +" public function exists(string $id): bool\n" +" {\n" +" return $this->connection->createQuery()\n" +" ->from('{{%page}}')\n" +" ->where(['id' => $id])\n" +" ->exists();\n" " }\n" "}\n" msgstr "" +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this repository there are both methods to get data and `save()` to do insert or update. DB returns raw data as arrays but our repository automatically creates entities from this raw data so later we operate typed data." +msgstr "" + #. type: Title ## #: ../src/guide/start/databases.md #, no-wrap @@ -501,7 +523,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need to be able to:" +msgid "We need some actions to:" msgstr "" #. type: Bullet: '1. ' @@ -526,7 +548,12 @@ msgstr "" #. type: Bullet: '5. ' #: ../src/guide/start/databases.md -msgid "Edit a page." +msgid "Update a page." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Then we need routing for all these." msgstr "" #. type: Plain text @@ -589,17 +616,21 @@ msgid "" " $pages */\n" +"/** @var UrlGeneratorInterface $urlGenerator */\n" "?>\n" "\n" "
    \n" " \n" "
  • \n" -" title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?>\n" +" title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>\n" "
  • \n" " \n" "
\n" +"\n" +"generate('page/edit', ['slug' => 'new'])) ?>\n" msgstr "" #. type: Title ### @@ -634,18 +665,15 @@ msgid "" " public function __construct(\n" " private ViewRenderer $viewRenderer,\n" " private PageRepository $pageRepository,\n" -" private ResponseFactoryInterface $responseFactory\n" -" )\n" -" {\n" -" }\n" +" private ResponseFactoryInterface $responseFactory,\n" +" ) {}\n" "\n" " public function __invoke(\n" " #[RouteArgument('slug')]\n" -" string $slug\n" -" ): ResponseInterface\n" -" {\n" +" string $slug,\n" +" ): ResponseInterface {\n" " $page = $this->pageRepository->findOneBySlug($slug);\n" -" if ($page === null || $page->isDeleted()) {\n" +" if ($page === null) {\n" " return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" " }\n" "\n" @@ -668,15 +696,36 @@ msgid "" "\n" "\n" -"

title) ?>

\n" +"

generate('page/list')) ?> → title) ?>

\n" "\n" "

\n" " text) ?>\n" "

\n" +"\n" +"generate('page/edit', ['slug' => $page->getSlug()])) ?> |\n" +"\n" +"\n" +"post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()]))\n" +" ->csrf($csrf);\n" +"?>\n" +"open() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In this view we have a form that submits a request for page deletion. Handing it with `GET` is common as well, but it is very wrong. Since deletion changes data, it needs to be handled by one of the non-idempotent HTTP methods. We use POST and a form in our example, but it could be `DELETE` and async request made with JavaScript. The button could be later styled properly to look similar to the \"Edit\"." msgstr "" #. type: Title ### @@ -703,6 +752,7 @@ msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" "use Psr\\Http\\Message\\ResponseInterface;\n" "use Yiisoft\\Http\\Status;\n" +"use Yiisoft\\Router\\HydratorAttribute\\RouteArgument;\n" "use Yiisoft\\Router\\UrlGeneratorInterface;\n" "\n" "final readonly class DeleteAction\n" @@ -711,16 +761,17 @@ msgid "" " private PageRepository $pageRepository,\n" " private ResponseFactoryInterface $responseFactory,\n" " private UrlGeneratorInterface $urlGenerator,\n" -" )\n" -" {}\n" +" ) {}\n" "\n" -" public function __invoke(string $slug): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('slug')]\n" +" string $slug\n" +" ): ResponseInterface\n" " {\n" " $this->pageRepository->deleteBySlug($slug);\n" "\n" " return $this->responseFactory\n" -" ->createResponse()\n" -" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->createResponse(Status::SEE_OTHER)\n" " ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" " }\n" "}\n" @@ -729,5 +780,202 @@ msgstr "" #. type: Title ### #: ../src/guide/start/databases.md #, no-wrap -msgid "Create a page" +msgid "Create or update a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/EditAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"findOneBySlug($slug);\n" +" if ($page === null) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" $form->title = $page->title;\n" +" $form->text = $page->text;\n" +" }\n" +"\n" +" $this->formHydrator->populateFromPostAndValidate($form, $request);\n" +"\n" +" if ($form->isValid()) {\n" +" $id = $isNew ? Uuid::uuid7()->toString() : $page->id;\n" +"\n" +" $page = Page::create(\n" +" id: $id,\n" +" title: $form->title,\n" +" text: $form->text,\n" +" updatedAt: new DateTimeImmutable(),\n" +" );\n" +"\n" +" $pageRepository->save($page);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse(Status::SEE_OTHER)\n" +" ->withHeader(\n" +" 'Location',\n" +" $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]),\n" +" );\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/edit', [\n" +" 'form' => $form,\n" +" 'isNew' => $isNew,\n" +" 'slug' => $slug,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "In the above we use a special slug in the URL for new pages so the URL looks like `http://localhost/pages/new`. If the page isn't new, we pre-fill the form with the data from the database. Similar to how we did in [Working with forms](forms.md), we handle the form submission. After successful save we redirect to the page view." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/edit.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"post($urlGenerator->generate('page/edit', ['slug' => $slug]))\n" +" ->csrf($csrf);\n" +"?>\n" +"\n" +"open() ?>\n" +" required() ?>\n" +" required() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Routing" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Adjust `config/common/routes.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"routes(\n" +" Route::get('/')\n" +" ->action(Web\\HomePage\\Action::class)\n" +" ->name('home'),\n" +" Route::methods([Method::GET, Method::POST], '/say')\n" +" ->action(Web\\Echo\\Action::class)\n" +" ->name('echo/say'),\n" +"\n" +" Group::create('/pages')->routes(\n" +" Route::get('')\n" +" ->action(Web\\Page\\ListAction::class)\n" +" ->name('page/list'),\n" +" Route::get('/{slug}')\n" +" ->action(Web\\Page\\ViewAction::class)\n" +" ->name('page/view'),\n" +" Route::methods([Method::GET, Method::POST], '/{slug}/edit')\n" +" ->action(Web\\Page\\EditAction::class)\n" +" ->name('page/edit'),\n" +" Route::post('/{slug}/delete')\n" +" ->action(Web\\Page\\DeleteAction::class)\n" +" ->name('page/delete'),\n" +" ),\n" +" ),\n" +"];\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Note that we've grouped all page-related routes with a group under `/pages` prefix. That is a convenient way to both not to repeat yourself and add some extra middleware, such as authentication, to the whole group." +msgstr "" + +#. type: Title ## +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Trying it out" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now try it out by opening `http://localhost/pages` in your browser." msgstr "" diff --git a/_translations/po/ru/guide_start_forms.md.po b/_translations/po/ru/guide_start_forms.md.po index 0195d9a4..121abc35 100644 --- a/_translations/po/ru/guide_start_forms.md.po +++ b/_translations/po/ru/guide_start_forms.md.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: 2025-09-04 11:19+0500\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -94,6 +94,8 @@ msgstr "" msgid "" "\n" "Language-Team: LANGUAGE \n" @@ -17,30 +17,31 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md #, no-wrap msgid "Headers" msgstr "" #. type: Title ## -#: en/runtime/request.md en/runtime/response.md +#: ../src/guide/runtime/request.md ../src/guide/runtime/response.md +#: ../src/internals/006-git-commit-messages.md #, no-wrap msgid "Body" msgstr "" #. type: Title # -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Response" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "HTTP response has status code and message, a set of headers and a body:" msgstr "" #. type: Fenced code block -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "HTTP/1.1 200 OK\n" @@ -55,14 +56,14 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "" "Yii uses [PSR-7 `Response`](https://www.php-fig.org/psr/psr-7/) in the web " "application to represent response." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "" "The object should be constructed and returned as a result of the execution " "of controller actions or other middleware. Usually, the middleware has a " @@ -70,7 +71,7 @@ msgid "" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" @@ -95,18 +96,18 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Status code" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set a status code like the following:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -115,59 +116,59 @@ msgid "" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "" "Majority of status codes are available from `Status` class for convenience " "and readability." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can set headers like this:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withHeader('Content-type', 'application/json');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "If there is a need to append a header value to the existing header:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withAddedHeader('Set-Cookie', 'qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "And, if needed, headers could be removed:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "$response = $response->withoutHeader('Set-Cookie');\n" msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "" "Response body is an object implementing " "`Psr\\Http\\Message\\StreamInterface`." msgstr "" #. type: Plain text -#: en/runtime/response.md +#: ../src/guide/runtime/response.md msgid "You can write to it via the interface itself:" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "$body = $response->getBody();\n" @@ -175,19 +176,19 @@ msgid "" msgstr "" #. type: Title ## -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Examples" msgstr "" #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Redirecting" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" "use Yiisoft\\Http\\Status;\n" @@ -197,16 +198,35 @@ msgid "" " ->withHeader('Location', 'https://www.example.com'); \n" msgstr "" +#. type: Plain text +#: ../src/guide/runtime/response.md +msgid "Note that there are different statuses used for redirection:" +msgstr "" + +#. type: Plain text +#: ../src/guide/runtime/response.md +#, no-wrap +msgid "" +"| Code | Usage | What is it for |\n" +"|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n" +"| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. |\n" +"| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. |\n" +"| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. |\n" +"| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. |\n" +"| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. |\n" +msgstr "" + #. type: Title ### -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "Responding with JSON" msgstr "" #. type: Fenced code block (php) -#: en/runtime/response.md +#: ../src/guide/runtime/response.md #, no-wrap msgid "" +"use Yiisoft\\Http\\Status;\n" "use Yiisoft\\Json\\Json;\n" "\n" "$data = [\n" @@ -216,6 +236,6 @@ msgid "" "\n" "$response->getBody()->write(Json::encode($data));\n" "return $response\n" -" ->withStatus(200)\n" -" ->withHeader('Content-Type', 'application/json');\n" +" ->withStatus(Status::OK)\n" +" ->withHeader('Content-Type', 'application/json');\n" msgstr "" diff --git a/_translations/pot/guide_start_databases.md.pot b/_translations/pot/guide_start_databases.md.pot index 5ddb9e6c..48462d6c 100644 --- a/_translations/pot/guide_start_databases.md.pot +++ b/_translations/pot/guide_start_databases.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-25 21:55+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -331,11 +331,10 @@ msgid "" " $b->createTable('page', [\n" " 'id' => $cb::uuidPrimaryKey(),\n" " 'title' => $cb::string()->notNull(),\n" -" 'slug' => $cb::string()->notNull(),\n" +" 'slug' => $cb::string()->notNull()->unique(),\n" " 'text' => $cb::text()->notNull(),\n" " 'created_at' => $cb::dateTime(),\n" " 'updated_at' => $cb::dateTime(),\n" -" 'deleted_at' => $cb::dateTime(),\n" " ]);\n" " }\n" "\n" @@ -349,16 +348,29 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md msgid "" -"Note that we use UUID as the primary key. While the storage space is a bit " -"bigger than using int, the workflow with such IDs is beneficial. You " -"generate the ID yourself so you can define a set of related data and save it " -"in a single transaction. The entities that define this set of data in the " -"code are often called an \"aggregate\"." +"Note that we use UUID as the primary key. We are going to generate these IDs " +"ourselves instead of relying on database so we'll need an extra compose " +"package for that." +msgstr "" + +#. type: Fenced code block (shell) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "make composer require ramsey/uuid\n" msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "Apply it with `make yii migrate:up`." +msgid "" +"While the storage space is a bit bigger than using int, the workflow with " +"such IDs is beneficial. Since you generate the ID yourself so you can define " +"a set of related data and save it in a single transaction. The entities " +"that define this set of data in the code are often called an \"aggregate\"." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Apply the migration with `make yii migrate:up`." msgstr "" #. type: Title ## @@ -395,7 +407,6 @@ msgid "" " public string $text,\n" " public DateTimeImmutable $createdAt,\n" " public DateTimeImmutable $updatedAt,\n" -" public ?DateTimeImmutable $deletedAt = null,\n" " ) {}\n" "\n" " public static function create(\n" @@ -404,7 +415,6 @@ msgid "" " string $text,\n" " ?DateTimeImmutable $createdAt = null,\n" " ?DateTimeImmutable $updatedAt = null,\n" -" ?DateTimeImmutable $deletedAt = null,\n" " ): self {\n" " return new self(\n" " id: $id,\n" @@ -412,7 +422,6 @@ msgid "" " text: $text,\n" " createdAt: $createdAt ?? new DateTimeImmutable(),\n" " updatedAt: $updatedAt ?? new DateTimeImmutable(),\n" -" deletedAt: $deletedAt,\n" " );\n" " }\n" "\n" @@ -420,11 +429,6 @@ msgid "" " {\n" " return (new Inflector())->toSlug($this->title);\n" " }\n" -"\n" -" public function isDeleted(): bool\n" -" {\n" -" return $this->deletedAt !== null;\n" -" }\n" "}\n" msgstr "" @@ -469,30 +473,30 @@ msgid "" "\n" " public function save(Page $page): void\n" " {\n" -" $this->connection->createCommand()->upsert('{{%page}}', [\n" +" $data = [\n" " 'id' => $page->id,\n" " 'title' => $page->title,\n" " 'slug' => $page->getSlug(),\n" " 'text' => $page->text,\n" " 'created_at' => $page->createdAt,\n" " 'updated_at' => $page->updatedAt,\n" -" 'deleted_at' => $page->deletedAt,\n" -" ])->execute();\n" +" ];\n" +"\n" +" if ($this->exists($page->id)) {\n" +" $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute();\n" +" } else {\n" +" $this->connection->createCommand()->insert('{{%page}}', $data)->execute();\n" +" }\n" " }\n" "\n" " public function findOneBySlug(string $slug): ?Page\n" " {\n" -" $data = (new Query($this->connection))\n" +" $query = (new Query($this->connection))\n" " ->select('*')\n" " ->from('{{%page}}')\n" -" ->where('slug = :slug', ['slug' => $slug])\n" -" ->one();\n" +" ->where('slug = :slug', ['slug' => $slug]);\n" "\n" -" if ($data === null) {\n" -" return null;\n" -" }\n" -"\n" -" return $this->createPage($data);\n" +" return $this->createPage($query->one());\n" " }\n" "\n" " /**\n" @@ -510,25 +514,48 @@ msgid "" " }\n" " }\n" "\n" -" private function createPage(array $data): Page\n" +" private function createPage(?array $data): ?Page\n" " {\n" +" if ($data === null) {\n" +" return null;\n" +" }\n" +"\n" " return Page::create(\n" " id: $data['id'],\n" " title: $data['title'],\n" " text: $data['text'],\n" " createdAt: new DateTimeImmutable($data['created_at']),\n" " updatedAt: new DateTimeImmutable($data['updated_at']),\n" -" deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,\n" " );\n" " }\n" "\n" " public function deleteBySlug(string $slug): void\n" " {\n" -" $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute();\n" +" $this->connection->createCommand()->delete(\n" +" '{{%page}}',\n" +" ['slug' => $slug],\n" +" )->execute();\n" +" }\n" +"\n" +" public function exists(string $id): bool\n" +" {\n" +" return $this->connection->createQuery()\n" +" ->from('{{%page}}')\n" +" ->where(['id' => $id])\n" +" ->exists();\n" " }\n" "}\n" msgstr "" +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "" +"In this repository there are both methods to get data and `save()` to do " +"insert or update. DB returns raw data as arrays but our repository " +"automatically creates entities from this raw data so later we operate typed " +"data." +msgstr "" + #. type: Title ## #: ../src/guide/start/databases.md #, no-wrap @@ -537,7 +564,7 @@ msgstr "" #. type: Plain text #: ../src/guide/start/databases.md -msgid "You need to be able to:" +msgid "We need some actions to:" msgstr "" #. type: Bullet: '1. ' @@ -562,7 +589,12 @@ msgstr "" #. type: Bullet: '5. ' #: ../src/guide/start/databases.md -msgid "Edit a page." +msgid "Update a page." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Then we need routing for all these." msgstr "" #. type: Plain text @@ -624,17 +656,21 @@ msgid "" " $pages */\n" +"/** @var UrlGeneratorInterface $urlGenerator */\n" "?>\n" "\n" "
    \n" " \n" "
  • \n" -" title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?>\n" +" title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>\n" "
  • \n" " \n" "
\n" +"\n" +"generate('page/edit', ['slug' => 'new'])) ?>\n" msgstr "" #. type: Title ### @@ -669,18 +705,15 @@ msgid "" " public function __construct(\n" " private ViewRenderer $viewRenderer,\n" " private PageRepository $pageRepository,\n" -" private ResponseFactoryInterface $responseFactory\n" -" )\n" -" {\n" -" }\n" +" private ResponseFactoryInterface $responseFactory,\n" +" ) {}\n" "\n" " public function __invoke(\n" " #[RouteArgument('slug')]\n" -" string $slug\n" -" ): ResponseInterface\n" -" {\n" +" string $slug,\n" +" ): ResponseInterface {\n" " $page = $this->pageRepository->findOneBySlug($slug);\n" -" if ($page === null || $page->isDeleted()) {\n" +" if ($page === null) {\n" " return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" " }\n" "\n" @@ -703,15 +736,42 @@ msgid "" "\n" "\n" -"

title) ?>

\n" +"

generate('page/list')) ?> → title) ?>

\n" "\n" "

\n" " text) ?>\n" "

\n" +"\n" +"generate('page/edit', ['slug' => $page->getSlug()])) ?> |\n" +"\n" +"\n" +"post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()]))\n" +" ->csrf($csrf);\n" +"?>\n" +"open() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "" +"In this view we have a form that submits a request for page deletion. " +"Handing it with `GET` is common as well, but it is very wrong. Since " +"deletion changes data, it needs to be handled by one of the non-idempotent " +"HTTP methods. We use POST and a form in our example, but it could be " +"`DELETE` and async request made with JavaScript. The button could be later " +"styled properly to look similar to the \"Edit\"." msgstr "" #. type: Title ### @@ -738,6 +798,7 @@ msgid "" "use Psr\\Http\\Message\\ResponseFactoryInterface;\n" "use Psr\\Http\\Message\\ResponseInterface;\n" "use Yiisoft\\Http\\Status;\n" +"use Yiisoft\\Router\\HydratorAttribute\\RouteArgument;\n" "use Yiisoft\\Router\\UrlGeneratorInterface;\n" "\n" "final readonly class DeleteAction\n" @@ -746,16 +807,17 @@ msgid "" " private PageRepository $pageRepository,\n" " private ResponseFactoryInterface $responseFactory,\n" " private UrlGeneratorInterface $urlGenerator,\n" -" )\n" -" {}\n" +" ) {}\n" "\n" -" public function __invoke(string $slug): ResponseInterface\n" +" public function __invoke(\n" +" #[RouteArgument('slug')]\n" +" string $slug\n" +" ): ResponseInterface\n" " {\n" " $this->pageRepository->deleteBySlug($slug);\n" "\n" " return $this->responseFactory\n" -" ->createResponse()\n" -" ->withStatus(Status::PERMANENT_REDIRECT)\n" +" ->createResponse(Status::SEE_OTHER)\n" " ->withHeader('Location', $this->urlGenerator->generate('page/list'));\n" " }\n" "}\n" @@ -764,5 +826,210 @@ msgstr "" #. type: Title ### #: ../src/guide/start/databases.md #, no-wrap -msgid "Create a page" +msgid "Create or update a page" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Create `src/Web/Page/EditAction.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"findOneBySlug($slug);\n" +" if ($page === null) {\n" +" return $this->responseFactory->createResponse(Status::NOT_FOUND);\n" +" }\n" +"\n" +" $form->title = $page->title;\n" +" $form->text = $page->text;\n" +" }\n" +"\n" +" $this->formHydrator->populateFromPostAndValidate($form, $request);\n" +"\n" +" if ($form->isValid()) {\n" +" $id = $isNew ? Uuid::uuid7()->toString() : $page->id;\n" +"\n" +" $page = Page::create(\n" +" id: $id,\n" +" title: $form->title,\n" +" text: $form->text,\n" +" updatedAt: new DateTimeImmutable(),\n" +" );\n" +"\n" +" $pageRepository->save($page);\n" +"\n" +" return $this->responseFactory\n" +" ->createResponse(Status::SEE_OTHER)\n" +" ->withHeader(\n" +" 'Location',\n" +" $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]),\n" +" );\n" +" }\n" +"\n" +" return $this->viewRenderer->render(__DIR__ . '/edit', [\n" +" 'form' => $form,\n" +" 'isNew' => $isNew,\n" +" 'slug' => $slug,\n" +" ]);\n" +" }\n" +"}\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "" +"In the above we use a special slug in the URL for new pages so the URL looks " +"like `http://localhost/pages/new`. If the page isn't new, we pre-fill the " +"form with the data from the database. Similar to how we did in [Working with " +"forms](forms.md), we handle the form submission. After successful save we " +"redirect to the page view." +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now, a template in `src/Web/Page/edit.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"post($urlGenerator->generate('page/edit', ['slug' => $slug]))\n" +" ->csrf($csrf);\n" +"?>\n" +"\n" +"open() ?>\n" +" required() ?>\n" +" required() ?>\n" +" \n" +"close() ?>\n" +msgstr "" + +#. type: Title ### +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Routing" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Adjust `config/common/routes.php`:" +msgstr "" + +#. type: Fenced code block (php) +#: ../src/guide/start/databases.md +#, no-wrap +msgid "" +"routes(\n" +" Route::get('/')\n" +" ->action(Web\\HomePage\\Action::class)\n" +" ->name('home'),\n" +" Route::methods([Method::GET, Method::POST], '/say')\n" +" ->action(Web\\Echo\\Action::class)\n" +" ->name('echo/say'),\n" +"\n" +" Group::create('/pages')->routes(\n" +" Route::get('')\n" +" ->action(Web\\Page\\ListAction::class)\n" +" ->name('page/list'),\n" +" Route::get('/{slug}')\n" +" ->action(Web\\Page\\ViewAction::class)\n" +" ->name('page/view'),\n" +" Route::methods([Method::GET, Method::POST], '/{slug}/edit')\n" +" ->action(Web\\Page\\EditAction::class)\n" +" ->name('page/edit'),\n" +" Route::post('/{slug}/delete')\n" +" ->action(Web\\Page\\DeleteAction::class)\n" +" ->name('page/delete'),\n" +" ),\n" +" ),\n" +"];\n" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "" +"Note that we've grouped all page-related routes with a group under `/pages` " +"prefix. That is a convenient way to both not to repeat yourself and add some " +"extra middleware, such as authentication, to the whole group." +msgstr "" + +#. type: Title ## +#: ../src/guide/start/databases.md +#, no-wrap +msgid "Trying it out" +msgstr "" + +#. type: Plain text +#: ../src/guide/start/databases.md +msgid "Now try it out by opening `http://localhost/pages` in your browser." msgstr "" diff --git a/_translations/pot/guide_start_forms.md.pot b/_translations/pot/guide_start_forms.md.pot index b8c6a848..0a8be2ea 100644 --- a/_translations/pot/guide_start_forms.md.pot +++ b/_translations/pot/guide_start_forms.md.pot @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-12-24 13:00+0000\n" +"POT-Creation-Date: 2025-12-26 22:37+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -99,6 +99,8 @@ msgstr "" msgid "" "withHeader('Location', 'https://www.example.com'); ``` +Note that there are different statuses used for redirection: + +| Code | Usage | What is it for | +|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. | +| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. | +| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. | +| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. | +| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. | + ### Responding with JSON ```php +use Yiisoft\Http\Status; use Yiisoft\Json\Json; $data = [ @@ -112,6 +123,6 @@ $data = [ $response->getBody()->write(Json::encode($data)); return $response - ->withStatus(200) - ->withHeader('Content-Type', 'application/json'); + ->withStatus(Status::OK) + ->withHeader('Content-Type', 'application/json'); ``` diff --git a/src/es/guide/start/databases.md b/src/es/guide/start/databases.md index 22f7657a..a92204d3 100644 --- a/src/es/guide/start/databases.md +++ b/src/es/guide/start/databases.md @@ -184,11 +184,10 @@ final class M251102141707Page implements RevertibleMigrationInterface $b->createTable('page', [ 'id' => $cb::uuidPrimaryKey(), 'title' => $cb::string()->notNull(), - 'slug' => $cb::string()->notNull(), + 'slug' => $cb::string()->notNull()->unique(), 'text' => $cb::text()->notNull(), 'created_at' => $cb::dateTime(), 'updated_at' => $cb::dateTime(), - 'deleted_at' => $cb::dateTime(), ]); } @@ -199,13 +198,20 @@ final class M251102141707Page implements RevertibleMigrationInterface } ``` -Note that we use UUID as the primary key. While the storage space is a bit -bigger than using int, the workflow with such IDs is beneficial. You -generate the ID yourself so you can define a set of related data and save it -in a single transaction. The entities that define this set of data in the -code are often called an "aggregate". +Note that we use UUID as the primary key. We are going to generate these IDs +ourselves instead of relying on database so we'll need an extra compose +package for that. -Apply it with `make yii migrate:up`. +```shell +make composer require ramsey/uuid +``` + +While the storage space is a bit bigger than using int, the workflow with +such IDs is beneficial. Since you generate the ID yourself so you can define +a set of related data and save it in a single transaction. The entities +that define this set of data in the code are often called an "aggregate". + +Apply the migration with `make yii migrate:up`. ## An entity @@ -230,7 +236,6 @@ final readonly class Page public string $text, public DateTimeImmutable $createdAt, public DateTimeImmutable $updatedAt, - public ?DateTimeImmutable $deletedAt = null, ) {} public static function create( @@ -239,7 +244,6 @@ final readonly class Page string $text, ?DateTimeImmutable $createdAt = null, ?DateTimeImmutable $updatedAt = null, - ?DateTimeImmutable $deletedAt = null, ): self { return new self( id: $id, @@ -247,7 +251,6 @@ final readonly class Page text: $text, createdAt: $createdAt ?? new DateTimeImmutable(), updatedAt: $updatedAt ?? new DateTimeImmutable(), - deletedAt: $deletedAt, ); } @@ -255,11 +258,6 @@ final readonly class Page { return (new Inflector())->toSlug($this->title); } - - public function isDeleted(): bool - { - return $this->deletedAt !== null; - } } ``` @@ -289,30 +287,30 @@ final readonly class PageRepository public function save(Page $page): void { - $this->connection->createCommand()->upsert('{{%page}}', [ + $data = [ 'id' => $page->id, 'title' => $page->title, 'slug' => $page->getSlug(), 'text' => $page->text, 'created_at' => $page->createdAt, 'updated_at' => $page->updatedAt, - 'deleted_at' => $page->deletedAt, - ])->execute(); + ]; + + if ($this->exists($page->id)) { + $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute(); + } else { + $this->connection->createCommand()->insert('{{%page}}', $data)->execute(); + } } public function findOneBySlug(string $slug): ?Page { - $data = (new Query($this->connection)) + $query = (new Query($this->connection)) ->select('*') ->from('{{%page}}') - ->where('slug = :slug', ['slug' => $slug]) - ->one(); + ->where('slug = :slug', ['slug' => $slug]); - if ($data === null) { - return null; - } - - return $this->createPage($data); + return $this->createPage($query->one()); } /** @@ -330,37 +328,57 @@ final readonly class PageRepository } } - private function createPage(array $data): Page + private function createPage(?array $data): ?Page { + if ($data === null) { + return null; + } + return Page::create( id: $data['id'], title: $data['title'], text: $data['text'], createdAt: new DateTimeImmutable($data['created_at']), updatedAt: new DateTimeImmutable($data['updated_at']), - deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null, ); } public function deleteBySlug(string $slug): void { - $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); + $this->connection->createCommand()->delete( + '{{%page}}', + ['slug' => $slug], + )->execute(); + } + + public function exists(string $id): bool + { + return $this->connection->createQuery() + ->from('{{%page}}') + ->where(['id' => $id]) + ->exists(); } } ``` +In this repository there are both methods to get data and `save()` to do +insert or update. DB returns raw data as arrays but our repository +automatically creates entities from this raw data so later we operate typed +data. + ## Actions and routes -You need to be able to: +We need some actions to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. -5. Edit a page. +5. Update a page. -Let's tackle these one by one. +Then we need routing for all these. +Let's tackle these one by one. ### List all pages @@ -400,17 +418,21 @@ Define list view in `src/Web/Page/list.php`: $pages */ +/** @var UrlGeneratorInterface $urlGenerator */ ?>
  • - title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> + title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>
+ +generate('page/edit', ['slug' => 'new'])) ?> ``` ### View a page @@ -435,18 +457,15 @@ final readonly class ViewAction public function __construct( private ViewRenderer $viewRenderer, private PageRepository $pageRepository, - private ResponseFactoryInterface $responseFactory - ) - { - } + private ResponseFactoryInterface $responseFactory, + ) {} public function __invoke( #[RouteArgument('slug')] - string $slug - ): ResponseInterface - { + string $slug, + ): ResponseInterface { $page = $this->pageRepository->findOneBySlug($slug); - if ($page === null || $page->isDeleted()) { + if ($page === null) { return $this->responseFactory->createResponse(Status::NOT_FOUND); } @@ -463,17 +482,40 @@ Now, a template in `src/Web/Page/view.php`: -

title) ?>

+

generate('page/list')) ?> → title) ?>

text) ?>

+ +generate('page/edit', ['slug' => $page->getSlug()])) ?> | + + +post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()])) + ->csrf($csrf); +?> +open() ?> + +close() ?> ``` +In this view we have a form that submits a request for page +deletion. Handing it with `GET` is common as well, but it is very +wrong. Since deletion changes data, it needs to be handled by one of the +non-idempotent HTTP methods. We use POST and a form in our example, but it +could be `DELETE` and async request made with JavaScript. The button could +be later styled properly to look similar to the "Edit". + ### Delete a page Create `src/Web/Page/DeleteAction.php`: @@ -488,6 +530,7 @@ namespace App\Web\Page; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Yiisoft\Http\Status; +use Yiisoft\Router\HydratorAttribute\RouteArgument; use Yiisoft\Router\UrlGeneratorInterface; final readonly class DeleteAction @@ -496,21 +539,189 @@ final readonly class DeleteAction private PageRepository $pageRepository, private ResponseFactoryInterface $responseFactory, private UrlGeneratorInterface $urlGenerator, - ) - {} + ) {} - public function __invoke(string $slug): ResponseInterface + public function __invoke( + #[RouteArgument('slug')] + string $slug + ): ResponseInterface { $this->pageRepository->deleteBySlug($slug); return $this->responseFactory - ->createResponse() - ->withStatus(Status::PERMANENT_REDIRECT) + ->createResponse(Status::SEE_OTHER) ->withHeader('Location', $this->urlGenerator->generate('page/list')); } } ``` -### Create a page +### Create or update a page + +Create `src/Web/Page/EditAction.php`: + +```php +findOneBySlug($slug); + if ($page === null) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + $form->title = $page->title; + $form->text = $page->text; + } + + $this->formHydrator->populateFromPostAndValidate($form, $request); + + if ($form->isValid()) { + $id = $isNew ? Uuid::uuid7()->toString() : $page->id; + + $page = Page::create( + id: $id, + title: $form->title, + text: $form->text, + updatedAt: new DateTimeImmutable(), + ); + + $pageRepository->save($page); + + return $this->responseFactory + ->createResponse(Status::SEE_OTHER) + ->withHeader( + 'Location', + $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]), + ); + } + + return $this->viewRenderer->render(__DIR__ . '/edit', [ + 'form' => $form, + 'isNew' => $isNew, + 'slug' => $slug, + ]); + } +} +``` + +In the above we use a special slug in the URL for new pages so the URL looks +like `http://localhost/pages/new`. If the page isn't new, we pre-fill the +form with the data from the database. Similar to how we did in [Working with +forms](forms.md), we handle the form submission. After successful save we +redirect to the page view. + +Now, a template in `src/Web/Page/edit.php`: + +```php +post($urlGenerator->generate('page/edit', ['slug' => $slug])) + ->csrf($csrf); +?> + +open() ?> + required() ?> + required() ?> + +close() ?> +``` + +### Routing + +Adjust `config/common/routes.php`: + +```php +routes( + Route::get('/') + ->action(Web\HomePage\Action::class) + ->name('home'), + Route::methods([Method::GET, Method::POST], '/say') + ->action(Web\Echo\Action::class) + ->name('echo/say'), + + Group::create('/pages')->routes( + Route::get('') + ->action(Web\Page\ListAction::class) + ->name('page/list'), + Route::get('/{slug}') + ->action(Web\Page\ViewAction::class) + ->name('page/view'), + Route::methods([Method::GET, Method::POST], '/{slug}/edit') + ->action(Web\Page\EditAction::class) + ->name('page/edit'), + Route::post('/{slug}/delete') + ->action(Web\Page\DeleteAction::class) + ->name('page/delete'), + ), + ), +]; +``` + +Note that we've grouped all page-related routes with a group under `/pages` +prefix. That is a convenient way to both not to repeat yourself and add some +extra middleware, such as authentication, to the whole group. + +## Trying it out +Now try it out by opening `http://localhost/pages` in your browser. diff --git a/src/es/guide/start/forms.md b/src/es/guide/start/forms.md index 64cb0f95..1e8e6499 100644 --- a/src/es/guide/start/forms.md +++ b/src/es/guide/start/forms.md @@ -33,6 +33,8 @@ as shown below and saved in the file `/src/App/Web/Echo/Form.php`: ```php withHeader('Location', 'https://www.example.com'); ``` +Note that there are different statuses used for redirection: + +| Code | Usage | What is it for | +|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. | +| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. | +| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. | +| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. | +| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. | + ### Responding with JSON ```php +use Yiisoft\Http\Status; use Yiisoft\Json\Json; $data = [ @@ -112,6 +123,6 @@ $data = [ $response->getBody()->write(Json::encode($data)); return $response - ->withStatus(200) - ->withHeader('Content-Type', 'application/json'); + ->withStatus(Status::OK) + ->withHeader('Content-Type', 'application/json'); ``` diff --git a/src/id/guide/start/databases.md b/src/id/guide/start/databases.md index 22f7657a..a92204d3 100644 --- a/src/id/guide/start/databases.md +++ b/src/id/guide/start/databases.md @@ -184,11 +184,10 @@ final class M251102141707Page implements RevertibleMigrationInterface $b->createTable('page', [ 'id' => $cb::uuidPrimaryKey(), 'title' => $cb::string()->notNull(), - 'slug' => $cb::string()->notNull(), + 'slug' => $cb::string()->notNull()->unique(), 'text' => $cb::text()->notNull(), 'created_at' => $cb::dateTime(), 'updated_at' => $cb::dateTime(), - 'deleted_at' => $cb::dateTime(), ]); } @@ -199,13 +198,20 @@ final class M251102141707Page implements RevertibleMigrationInterface } ``` -Note that we use UUID as the primary key. While the storage space is a bit -bigger than using int, the workflow with such IDs is beneficial. You -generate the ID yourself so you can define a set of related data and save it -in a single transaction. The entities that define this set of data in the -code are often called an "aggregate". +Note that we use UUID as the primary key. We are going to generate these IDs +ourselves instead of relying on database so we'll need an extra compose +package for that. -Apply it with `make yii migrate:up`. +```shell +make composer require ramsey/uuid +``` + +While the storage space is a bit bigger than using int, the workflow with +such IDs is beneficial. Since you generate the ID yourself so you can define +a set of related data and save it in a single transaction. The entities +that define this set of data in the code are often called an "aggregate". + +Apply the migration with `make yii migrate:up`. ## An entity @@ -230,7 +236,6 @@ final readonly class Page public string $text, public DateTimeImmutable $createdAt, public DateTimeImmutable $updatedAt, - public ?DateTimeImmutable $deletedAt = null, ) {} public static function create( @@ -239,7 +244,6 @@ final readonly class Page string $text, ?DateTimeImmutable $createdAt = null, ?DateTimeImmutable $updatedAt = null, - ?DateTimeImmutable $deletedAt = null, ): self { return new self( id: $id, @@ -247,7 +251,6 @@ final readonly class Page text: $text, createdAt: $createdAt ?? new DateTimeImmutable(), updatedAt: $updatedAt ?? new DateTimeImmutable(), - deletedAt: $deletedAt, ); } @@ -255,11 +258,6 @@ final readonly class Page { return (new Inflector())->toSlug($this->title); } - - public function isDeleted(): bool - { - return $this->deletedAt !== null; - } } ``` @@ -289,30 +287,30 @@ final readonly class PageRepository public function save(Page $page): void { - $this->connection->createCommand()->upsert('{{%page}}', [ + $data = [ 'id' => $page->id, 'title' => $page->title, 'slug' => $page->getSlug(), 'text' => $page->text, 'created_at' => $page->createdAt, 'updated_at' => $page->updatedAt, - 'deleted_at' => $page->deletedAt, - ])->execute(); + ]; + + if ($this->exists($page->id)) { + $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute(); + } else { + $this->connection->createCommand()->insert('{{%page}}', $data)->execute(); + } } public function findOneBySlug(string $slug): ?Page { - $data = (new Query($this->connection)) + $query = (new Query($this->connection)) ->select('*') ->from('{{%page}}') - ->where('slug = :slug', ['slug' => $slug]) - ->one(); + ->where('slug = :slug', ['slug' => $slug]); - if ($data === null) { - return null; - } - - return $this->createPage($data); + return $this->createPage($query->one()); } /** @@ -330,37 +328,57 @@ final readonly class PageRepository } } - private function createPage(array $data): Page + private function createPage(?array $data): ?Page { + if ($data === null) { + return null; + } + return Page::create( id: $data['id'], title: $data['title'], text: $data['text'], createdAt: new DateTimeImmutable($data['created_at']), updatedAt: new DateTimeImmutable($data['updated_at']), - deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null, ); } public function deleteBySlug(string $slug): void { - $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); + $this->connection->createCommand()->delete( + '{{%page}}', + ['slug' => $slug], + )->execute(); + } + + public function exists(string $id): bool + { + return $this->connection->createQuery() + ->from('{{%page}}') + ->where(['id' => $id]) + ->exists(); } } ``` +In this repository there are both methods to get data and `save()` to do +insert or update. DB returns raw data as arrays but our repository +automatically creates entities from this raw data so later we operate typed +data. + ## Actions and routes -You need to be able to: +We need some actions to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. -5. Edit a page. +5. Update a page. -Let's tackle these one by one. +Then we need routing for all these. +Let's tackle these one by one. ### List all pages @@ -400,17 +418,21 @@ Define list view in `src/Web/Page/list.php`: $pages */ +/** @var UrlGeneratorInterface $urlGenerator */ ?>
  • - title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> + title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>
+ +generate('page/edit', ['slug' => 'new'])) ?> ``` ### View a page @@ -435,18 +457,15 @@ final readonly class ViewAction public function __construct( private ViewRenderer $viewRenderer, private PageRepository $pageRepository, - private ResponseFactoryInterface $responseFactory - ) - { - } + private ResponseFactoryInterface $responseFactory, + ) {} public function __invoke( #[RouteArgument('slug')] - string $slug - ): ResponseInterface - { + string $slug, + ): ResponseInterface { $page = $this->pageRepository->findOneBySlug($slug); - if ($page === null || $page->isDeleted()) { + if ($page === null) { return $this->responseFactory->createResponse(Status::NOT_FOUND); } @@ -463,17 +482,40 @@ Now, a template in `src/Web/Page/view.php`: -

title) ?>

+

generate('page/list')) ?> → title) ?>

text) ?>

+ +generate('page/edit', ['slug' => $page->getSlug()])) ?> | + + +post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()])) + ->csrf($csrf); +?> +open() ?> + +close() ?> ``` +In this view we have a form that submits a request for page +deletion. Handing it with `GET` is common as well, but it is very +wrong. Since deletion changes data, it needs to be handled by one of the +non-idempotent HTTP methods. We use POST and a form in our example, but it +could be `DELETE` and async request made with JavaScript. The button could +be later styled properly to look similar to the "Edit". + ### Delete a page Create `src/Web/Page/DeleteAction.php`: @@ -488,6 +530,7 @@ namespace App\Web\Page; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Yiisoft\Http\Status; +use Yiisoft\Router\HydratorAttribute\RouteArgument; use Yiisoft\Router\UrlGeneratorInterface; final readonly class DeleteAction @@ -496,21 +539,189 @@ final readonly class DeleteAction private PageRepository $pageRepository, private ResponseFactoryInterface $responseFactory, private UrlGeneratorInterface $urlGenerator, - ) - {} + ) {} - public function __invoke(string $slug): ResponseInterface + public function __invoke( + #[RouteArgument('slug')] + string $slug + ): ResponseInterface { $this->pageRepository->deleteBySlug($slug); return $this->responseFactory - ->createResponse() - ->withStatus(Status::PERMANENT_REDIRECT) + ->createResponse(Status::SEE_OTHER) ->withHeader('Location', $this->urlGenerator->generate('page/list')); } } ``` -### Create a page +### Create or update a page + +Create `src/Web/Page/EditAction.php`: + +```php +findOneBySlug($slug); + if ($page === null) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + $form->title = $page->title; + $form->text = $page->text; + } + + $this->formHydrator->populateFromPostAndValidate($form, $request); + + if ($form->isValid()) { + $id = $isNew ? Uuid::uuid7()->toString() : $page->id; + + $page = Page::create( + id: $id, + title: $form->title, + text: $form->text, + updatedAt: new DateTimeImmutable(), + ); + + $pageRepository->save($page); + + return $this->responseFactory + ->createResponse(Status::SEE_OTHER) + ->withHeader( + 'Location', + $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]), + ); + } + + return $this->viewRenderer->render(__DIR__ . '/edit', [ + 'form' => $form, + 'isNew' => $isNew, + 'slug' => $slug, + ]); + } +} +``` + +In the above we use a special slug in the URL for new pages so the URL looks +like `http://localhost/pages/new`. If the page isn't new, we pre-fill the +form with the data from the database. Similar to how we did in [Working with +forms](forms.md), we handle the form submission. After successful save we +redirect to the page view. + +Now, a template in `src/Web/Page/edit.php`: + +```php +post($urlGenerator->generate('page/edit', ['slug' => $slug])) + ->csrf($csrf); +?> + +open() ?> + required() ?> + required() ?> + +close() ?> +``` + +### Routing + +Adjust `config/common/routes.php`: + +```php +routes( + Route::get('/') + ->action(Web\HomePage\Action::class) + ->name('home'), + Route::methods([Method::GET, Method::POST], '/say') + ->action(Web\Echo\Action::class) + ->name('echo/say'), + + Group::create('/pages')->routes( + Route::get('') + ->action(Web\Page\ListAction::class) + ->name('page/list'), + Route::get('/{slug}') + ->action(Web\Page\ViewAction::class) + ->name('page/view'), + Route::methods([Method::GET, Method::POST], '/{slug}/edit') + ->action(Web\Page\EditAction::class) + ->name('page/edit'), + Route::post('/{slug}/delete') + ->action(Web\Page\DeleteAction::class) + ->name('page/delete'), + ), + ), +]; +``` + +Note that we've grouped all page-related routes with a group under `/pages` +prefix. That is a convenient way to both not to repeat yourself and add some +extra middleware, such as authentication, to the whole group. + +## Trying it out +Now try it out by opening `http://localhost/pages` in your browser. diff --git a/src/id/guide/start/forms.md b/src/id/guide/start/forms.md index 64cb0f95..1e8e6499 100644 --- a/src/id/guide/start/forms.md +++ b/src/id/guide/start/forms.md @@ -33,6 +33,8 @@ as shown below and saved in the file `/src/App/Web/Echo/Form.php`: ```php withHeader('Location', 'https://www.example.com'); ``` +Note that there are different statuses used for redirection: + +| Code | Usage | What is it for | +|------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 301 | `Status::MOVED_PERMANENTLY` | Permanently changed a URL structure. Search engines update their indexes, and browsers cache it. | +| 308 | `Status::PERMANENT_REDIRECT` | Like 301, but guarantees the HTTP method won't change. | +| 302 | `Status::FOUND` | Temporary changes like maintenance pages. Original URL should still be used for future requests. Search engines typically don't update their indexes. | +| 307 | `Status::TEMPORARY_REDIRECT` | Like 302, but guarantees the HTTP method won't change. | +| 303 | `Status::SEE_OTHER` | After form submissions to prevent duplicate submissions if the user refreshes. Explicitly tells to use `GET` for the redirect, even if the original request was `POST`. | + ### Responding with JSON ```php +use Yiisoft\Http\Status; use Yiisoft\Json\Json; $data = [ @@ -112,6 +123,6 @@ $data = [ $response->getBody()->write(Json::encode($data)); return $response - ->withStatus(200) - ->withHeader('Content-Type', 'application/json'); + ->withStatus(Status::OK) + ->withHeader('Content-Type', 'application/json'); ``` diff --git a/src/ru/guide/start/databases.md b/src/ru/guide/start/databases.md index 22f7657a..a92204d3 100644 --- a/src/ru/guide/start/databases.md +++ b/src/ru/guide/start/databases.md @@ -184,11 +184,10 @@ final class M251102141707Page implements RevertibleMigrationInterface $b->createTable('page', [ 'id' => $cb::uuidPrimaryKey(), 'title' => $cb::string()->notNull(), - 'slug' => $cb::string()->notNull(), + 'slug' => $cb::string()->notNull()->unique(), 'text' => $cb::text()->notNull(), 'created_at' => $cb::dateTime(), 'updated_at' => $cb::dateTime(), - 'deleted_at' => $cb::dateTime(), ]); } @@ -199,13 +198,20 @@ final class M251102141707Page implements RevertibleMigrationInterface } ``` -Note that we use UUID as the primary key. While the storage space is a bit -bigger than using int, the workflow with such IDs is beneficial. You -generate the ID yourself so you can define a set of related data and save it -in a single transaction. The entities that define this set of data in the -code are often called an "aggregate". +Note that we use UUID as the primary key. We are going to generate these IDs +ourselves instead of relying on database so we'll need an extra compose +package for that. -Apply it with `make yii migrate:up`. +```shell +make composer require ramsey/uuid +``` + +While the storage space is a bit bigger than using int, the workflow with +such IDs is beneficial. Since you generate the ID yourself so you can define +a set of related data and save it in a single transaction. The entities +that define this set of data in the code are often called an "aggregate". + +Apply the migration with `make yii migrate:up`. ## An entity @@ -230,7 +236,6 @@ final readonly class Page public string $text, public DateTimeImmutable $createdAt, public DateTimeImmutable $updatedAt, - public ?DateTimeImmutable $deletedAt = null, ) {} public static function create( @@ -239,7 +244,6 @@ final readonly class Page string $text, ?DateTimeImmutable $createdAt = null, ?DateTimeImmutable $updatedAt = null, - ?DateTimeImmutable $deletedAt = null, ): self { return new self( id: $id, @@ -247,7 +251,6 @@ final readonly class Page text: $text, createdAt: $createdAt ?? new DateTimeImmutable(), updatedAt: $updatedAt ?? new DateTimeImmutable(), - deletedAt: $deletedAt, ); } @@ -255,11 +258,6 @@ final readonly class Page { return (new Inflector())->toSlug($this->title); } - - public function isDeleted(): bool - { - return $this->deletedAt !== null; - } } ``` @@ -289,30 +287,30 @@ final readonly class PageRepository public function save(Page $page): void { - $this->connection->createCommand()->upsert('{{%page}}', [ + $data = [ 'id' => $page->id, 'title' => $page->title, 'slug' => $page->getSlug(), 'text' => $page->text, 'created_at' => $page->createdAt, 'updated_at' => $page->updatedAt, - 'deleted_at' => $page->deletedAt, - ])->execute(); + ]; + + if ($this->exists($page->id)) { + $this->connection->createCommand()->update('{{%page}}', $data, ['id' => $page->id])->execute(); + } else { + $this->connection->createCommand()->insert('{{%page}}', $data)->execute(); + } } public function findOneBySlug(string $slug): ?Page { - $data = (new Query($this->connection)) + $query = (new Query($this->connection)) ->select('*') ->from('{{%page}}') - ->where('slug = :slug', ['slug' => $slug]) - ->one(); + ->where('slug = :slug', ['slug' => $slug]); - if ($data === null) { - return null; - } - - return $this->createPage($data); + return $this->createPage($query->one()); } /** @@ -330,37 +328,57 @@ final readonly class PageRepository } } - private function createPage(array $data): Page + private function createPage(?array $data): ?Page { + if ($data === null) { + return null; + } + return Page::create( id: $data['id'], title: $data['title'], text: $data['text'], createdAt: new DateTimeImmutable($data['created_at']), updatedAt: new DateTimeImmutable($data['updated_at']), - deletedAt: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null, ); } public function deleteBySlug(string $slug): void { - $this->connection->createCommand()->delete('{{%page}}', ['slug' => $slug])->execute(); + $this->connection->createCommand()->delete( + '{{%page}}', + ['slug' => $slug], + )->execute(); + } + + public function exists(string $id): bool + { + return $this->connection->createQuery() + ->from('{{%page}}') + ->where(['id' => $id]) + ->exists(); } } ``` +In this repository there are both methods to get data and `save()` to do +insert or update. DB returns raw data as arrays but our repository +automatically creates entities from this raw data so later we operate typed +data. + ## Actions and routes -You need to be able to: +We need some actions to: 1. List all pages. 2. View a page. 3. Delete a page. 4. Create a page. -5. Edit a page. +5. Update a page. -Let's tackle these one by one. +Then we need routing for all these. +Let's tackle these one by one. ### List all pages @@ -400,17 +418,21 @@ Define list view in `src/Web/Page/list.php`: $pages */ +/** @var UrlGeneratorInterface $urlGenerator */ ?>
  • - title, $this->urlGenerator->generate('page/view', ['slug' => $page->slug])) ?> + title, $urlGenerator->generate('page/view', ['slug' => $page->getSlug()])) ?>
+ +generate('page/edit', ['slug' => 'new'])) ?> ``` ### View a page @@ -435,18 +457,15 @@ final readonly class ViewAction public function __construct( private ViewRenderer $viewRenderer, private PageRepository $pageRepository, - private ResponseFactoryInterface $responseFactory - ) - { - } + private ResponseFactoryInterface $responseFactory, + ) {} public function __invoke( #[RouteArgument('slug')] - string $slug - ): ResponseInterface - { + string $slug, + ): ResponseInterface { $page = $this->pageRepository->findOneBySlug($slug); - if ($page === null || $page->isDeleted()) { + if ($page === null) { return $this->responseFactory->createResponse(Status::NOT_FOUND); } @@ -463,17 +482,40 @@ Now, a template in `src/Web/Page/view.php`: -

title) ?>

+

generate('page/list')) ?> → title) ?>

text) ?>

+ +generate('page/edit', ['slug' => $page->getSlug()])) ?> | + + +post($urlGenerator->generate('page/delete', ['slug' => $page->getSlug()])) + ->csrf($csrf); +?> +open() ?> + +close() ?> ``` +In this view we have a form that submits a request for page +deletion. Handing it with `GET` is common as well, but it is very +wrong. Since deletion changes data, it needs to be handled by one of the +non-idempotent HTTP methods. We use POST and a form in our example, but it +could be `DELETE` and async request made with JavaScript. The button could +be later styled properly to look similar to the "Edit". + ### Delete a page Create `src/Web/Page/DeleteAction.php`: @@ -488,6 +530,7 @@ namespace App\Web\Page; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Yiisoft\Http\Status; +use Yiisoft\Router\HydratorAttribute\RouteArgument; use Yiisoft\Router\UrlGeneratorInterface; final readonly class DeleteAction @@ -496,21 +539,189 @@ final readonly class DeleteAction private PageRepository $pageRepository, private ResponseFactoryInterface $responseFactory, private UrlGeneratorInterface $urlGenerator, - ) - {} + ) {} - public function __invoke(string $slug): ResponseInterface + public function __invoke( + #[RouteArgument('slug')] + string $slug + ): ResponseInterface { $this->pageRepository->deleteBySlug($slug); return $this->responseFactory - ->createResponse() - ->withStatus(Status::PERMANENT_REDIRECT) + ->createResponse(Status::SEE_OTHER) ->withHeader('Location', $this->urlGenerator->generate('page/list')); } } ``` -### Create a page +### Create or update a page + +Create `src/Web/Page/EditAction.php`: + +```php +findOneBySlug($slug); + if ($page === null) { + return $this->responseFactory->createResponse(Status::NOT_FOUND); + } + + $form->title = $page->title; + $form->text = $page->text; + } + + $this->formHydrator->populateFromPostAndValidate($form, $request); + + if ($form->isValid()) { + $id = $isNew ? Uuid::uuid7()->toString() : $page->id; + + $page = Page::create( + id: $id, + title: $form->title, + text: $form->text, + updatedAt: new DateTimeImmutable(), + ); + + $pageRepository->save($page); + + return $this->responseFactory + ->createResponse(Status::SEE_OTHER) + ->withHeader( + 'Location', + $this->urlGenerator->generate('page/view', ['slug' => $page->getSlug()]), + ); + } + + return $this->viewRenderer->render(__DIR__ . '/edit', [ + 'form' => $form, + 'isNew' => $isNew, + 'slug' => $slug, + ]); + } +} +``` + +In the above we use a special slug in the URL for new pages so the URL looks +like `http://localhost/pages/new`. If the page isn't new, we pre-fill the +form with the data from the database. Similar to how we did in [Working with +forms](forms.md), we handle the form submission. After successful save we +redirect to the page view. + +Now, a template in `src/Web/Page/edit.php`: + +```php +post($urlGenerator->generate('page/edit', ['slug' => $slug])) + ->csrf($csrf); +?> + +open() ?> + required() ?> + required() ?> + +close() ?> +``` + +### Routing + +Adjust `config/common/routes.php`: + +```php +routes( + Route::get('/') + ->action(Web\HomePage\Action::class) + ->name('home'), + Route::methods([Method::GET, Method::POST], '/say') + ->action(Web\Echo\Action::class) + ->name('echo/say'), + + Group::create('/pages')->routes( + Route::get('') + ->action(Web\Page\ListAction::class) + ->name('page/list'), + Route::get('/{slug}') + ->action(Web\Page\ViewAction::class) + ->name('page/view'), + Route::methods([Method::GET, Method::POST], '/{slug}/edit') + ->action(Web\Page\EditAction::class) + ->name('page/edit'), + Route::post('/{slug}/delete') + ->action(Web\Page\DeleteAction::class) + ->name('page/delete'), + ), + ), +]; +``` + +Note that we've grouped all page-related routes with a group under `/pages` +prefix. That is a convenient way to both not to repeat yourself and add some +extra middleware, such as authentication, to the whole group. + +## Trying it out +Now try it out by opening `http://localhost/pages` in your browser. diff --git a/src/ru/guide/start/forms.md b/src/ru/guide/start/forms.md index 64cb0f95..1e8e6499 100644 --- a/src/ru/guide/start/forms.md +++ b/src/ru/guide/start/forms.md @@ -33,6 +33,8 @@ as shown below and saved in the file `/src/App/Web/Echo/Form.php`: ```php