diff --git a/.env b/.env new file mode 100644 index 0000000..19cd959 --- /dev/null +++ b/.env @@ -0,0 +1,29 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=4777a99cd6c61ce84969bd1338737c38 +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7&charset=utf8mb4" +DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fdcc07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> symfony/webpack-encore-bundle ### +/node_modules/ +/public/build/ +npm-debug.log +yarn-error.log +###< symfony/webpack-encore-bundle ### diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0b2b84 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Tutorials, Friendship & Symfony6 + +Well hi there! This repository holds the code and script +for the [Symfony6 Tutorials](https://symfonycasts.com/tracks/symfony) on SymfonyCasts. + +## Setup + +If you've just downloaded the code, congratulations!! + +To get it working, follow these steps: + +**Download Composer dependencies** + +Make sure you have [Composer installed](https://getcomposer.org/download/) +and then run: + +``` +composer install +``` + +You may alternatively need to run `php composer.phar install`, depending +on how you installed Composer. + +**Start the Symfony web server** + +You can use Nginx or Apache, but Symfony's local web server +works even better. + +To install the Symfony local web server, follow +"Downloading the Symfony client" instructions found +here: https://symfony.com/download - you only need to do this +once on your system. + +Then, to start the web server, open a terminal, move into the +project, and run: + +``` +symfony serve +``` + +(If this is your first time using this command, you may see an +error that you need to run `symfony server:ca:install` first). + +Now check out the site at `https://localhost:8000` + +Have fun! + +**OPTIONAL: Webpack Encore Assets** + +This app uses Webpack Encore for the CSS, JS and image files. +But, the built files are already included... so you don't need +to download or build anything if you don't want to! + +But if you *do* want to play with the CSS/JS and build the +final files, no problem. Make sure you have [yarn](https://yarnpkg.com/lang/en/) +or `npm` installed (`npm` comes with Node) and then run: + +``` +yarn install +yarn encore dev --watch + +# or +npm install +npm run watch +``` + +## Have Ideas, Feedback or an Issue? + +If you have suggestions or questions, please feel free to +open an issue on this repository or comment on the course +itself. We're watching both :). + +## SymfonyCasts Team Mix Tapes + +Want to listen to *our* mixtapes? + +* Diego: https://tidal.com/browse/playlist/4b0dd1f6-c3d4-4886-8e31-c59455596528 +* Victor: https://tidal.com/browse/playlist/e2473e0d-e4df-407e-b12c-f40765d1dd4b +* Danielle: https://tidal.com/browse/playlist/acd3fae7-10ea-43a4-be89-56a3f6f3040c +* Ryan: https://tidal.com/browse/playlist/96d88543-e1bb-4457-b38a-3afd7d71d1da +* Matias: https://tidal.com/browse/playlist/9f914493-5844-4fbc-8029-a828ee4d090d +* Leanna: https://tidal.com/browse/playlist/fb01a86a-802e-4861-9a20-a0d37244b696 +* Vladimir: https://tidal.com/browse/playlist/0e78bb9e-a6ce-4548-9521-3e6779875978 + +... or for countries where Tidal isn't available: +https://open.spotify.com/user/31smny3k6o3kbqmmyzwebplayhda?si=8kq-n_8aRNOieXAOQJoleQ + +## Rock 'N' Roll Band + +If we were a rock 'n' roll band, +We'd travel all over the land. +We'd play and we'd sing and wear spangly things. +If we were a rock 'n' roll band. +If we were a rock 'n' roll band, +And we were up there on the stand, +The people would here us and love us and cheer us. +Hurray for that rock 'n' roll band. +If we were a rock 'n' roll band, +Then we'd have a million fans. +We'd giggle and laugh and sign autographs, +If we were a rock 'n' roll band. +If we were a rock 'n' roll band. +The people would all kiss our hands. +We'd be millionaires and have extra long hair, +If we were a rock 'n' roll band. +But we ain't no rock 'n' roll band, +We're just seven kids in the sand. +With homemade guitars and pails and jars +And drums of potato chip cans. +Just seven kids in the sand. +Talk'n and waven' our hands. +And dreamin' and thinkin' oh wouldn't it be grand, +If we were a rock 'n' roll band. + +Shel Silverstein + +## Thanks! + +And as always, thanks so much for your support and letting +us do what we love! + +<3 Your friends at SymfonyCasts diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..ba24537 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,14 @@ +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// any CSS you import will output into a single css file (app.css in this case) +import './styles/app.css'; + +// start the Stimulus application +import './bootstrap'; + +console.log('Hi! My name is app.js!'); diff --git a/assets/bootstrap.js b/assets/bootstrap.js new file mode 100644 index 0000000..4ab2df6 --- /dev/null +++ b/assets/bootstrap.js @@ -0,0 +1,11 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge'; + +// Registers Stimulus controllers from controllers.json and in the controllers/ directory +export const app = startStimulusApp(require.context( + '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', + true, + /\.[jt]sx?$/ +)); + +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 0000000..7ba2551 --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,11 @@ +{ + "controllers": { + "@symfony/ux-turbo": { + "turbo-core": { + "enabled": true, + "fetch": "eager" + } + } + }, + "entrypoints": [] +} diff --git a/assets/controllers/song-controls_controller.js b/assets/controllers/song-controls_controller.js new file mode 100644 index 0000000..b7c97f3 --- /dev/null +++ b/assets/controllers/song-controls_controller.js @@ -0,0 +1,28 @@ +import { Controller } from '@hotwired/stimulus'; + +/* + * This is an example Stimulus controller! + * + * Any element with a data-controller="hello" attribute will cause + * this controller to be executed. The name "hello" comes from the filename: + * hello_controller.js -> "hello" + * + * Delete this file or adapt it for your use! + */ +import axios from 'axios'; + +export default class extends Controller { + static values = { + infoUrl: String + } + + play(event) { + event.preventDefault(); + + axios.get(this.infoUrlValue) + .then((response) => { + const audio = new Audio(response.data.url); + audio.play(); + }); + } +} diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..1ea81c5 --- /dev/null +++ b/assets/styles/app.css @@ -0,0 +1,39 @@ +@import '~bootstrap'; +@import '~@fortawesome/fontawesome-free/css/all.css'; +@import '~@fontsource/roboto-condensed'; + +body { + background-color: #17242C; + color: #fff; + font-family: 'Roboto Condensed', sans-serif; +} + +.song-list i { + margin-top: 7px; +} + +.song-list a { + color: #fff; +} + +.song-details { + background-color: #222F36; + border-radius: 5px; + padding: 5px; +} + +.genre-list li { + margin-right: 5px; +} + +.mixed-vinyl-container { + background: rgb(23,36,44); + background: linear-gradient(180deg, rgba(23,36,44,1) 1%, rgba(34,47,54,1) 97%); + border-radius: 5px; + display: block; + color: #fff; + text-decoration: none; +} +.mixed-vinyl-container:hover { + color: #fff; +} diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..c933dc5 --- /dev/null +++ b/bin/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +=8.1", + "ext-ctype": "*", + "ext-iconv": "*", + "babdev/pagerfanta-bundle": "^3.7", + "doctrine/doctrine-bundle": "^2.7", + "doctrine/doctrine-migrations-bundle": "^3.2", + "doctrine/orm": "^2.12", + "knplabs/knp-time-bundle": "^1.18", + "pagerfanta/doctrine-orm-adapter": "^3.6", + "pagerfanta/twig": "^3.6", + "sensio/framework-extra-bundle": "^6.2", + "stof/doctrine-extensions-bundle": "^1.7", + "symfony/asset": "6.1.*", + "symfony/console": "6.1.*", + "symfony/dotenv": "6.1.*", + "symfony/flex": "^2", + "symfony/framework-bundle": "6.1.*", + "symfony/http-client": "6.1.*", + "symfony/monolog-bundle": "^3.0", + "symfony/proxy-manager-bridge": "6.1.*", + "symfony/runtime": "6.1.*", + "symfony/twig-bundle": "6.1.*", + "symfony/ux-turbo": "^2.0", + "symfony/webpack-encore-bundle": "^1.13", + "symfony/yaml": "6.1.*", + "twig/extra-bundle": "^2.12|^3.0", + "twig/twig": "^2.12|^3.0" + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "platform": { + "php": "8.1.0" + } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": true, + "require": "6.1.*", + "docker": true + } + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.4", + "symfony/debug-bundle": "6.1.*", + "symfony/maker-bundle": "^1.41", + "symfony/stopwatch": "6.1.*", + "symfony/web-profiler-bundle": "6.1.*", + "zenstruck/foundry": "^1.21" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..29d3994 --- /dev/null +++ b/composer.lock @@ -0,0 +1,6944 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9a5fe75b51c7fbf653e97a4fdb7bdc3e", + "packages": [ + { + "name": "babdev/pagerfanta-bundle", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/BabDev/PagerfantaBundle.git", + "reference": "d2760ebc3d22db10f219544323bea432dedc9eb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/BabDev/PagerfantaBundle/zipball/d2760ebc3d22db10f219544323bea432dedc9eb0", + "reference": "d2760ebc3d22db10f219544323bea432dedc9eb0", + "shasum": "" + }, + "require": { + "pagerfanta/core": "^3.1", + "php": "^7.4 || ^8.0", + "symfony/config": "^4.4 || ^5.4 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0", + "symfony/polyfill-php80": "^1.15", + "symfony/property-access": "^4.4 || ^5.4 || ^6.0", + "symfony/routing": "^4.4 || ^5.4 || ^6.0" + }, + "conflict": { + "pagerfanta/twig": "<3.1", + "twig/twig": "<1.35 || >=2.0,<2.5", + "white-october/pagerfanta-bundle": "*" + }, + "require-dev": { + "doctrine/annotations": "^1.8", + "jms/serializer": "^3.0", + "jms/serializer-bundle": "^3.0 || ^4.0", + "matthiasnoback/symfony-dependency-injection-test": "^4.3", + "pagerfanta/twig": "^3.1", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.7.2", + "phpstan/phpstan-phpunit": "1.1.1", + "phpstan/phpstan-symfony": "1.2.0", + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^5.4 || ^6.0", + "symfony/serializer": "^4.4 || ^5.4 || ^6.0", + "symfony/translation": "^4.4 || ^5.4 || ^6.0", + "symfony/twig-bridge": "^4.4 || ^5.4 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0", + "twig/twig": "^1.35 || ^2.5 || ^3.0" + }, + "suggest": { + "jms/serializer-bundle": "To use the Pagerfanta class with the JMS Serializer", + "symfony/serializer": "To use the Pagerfanta class with the Symfony Serializer", + "symfony/translation": "To use the Twig templates with translation support", + "twig/twig": "To integrate Pagerfanta with Twig through extensions" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "BabDev\\PagerfantaBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Bundle integrating Pagerfanta with Symfony", + "keywords": [ + "pagerfanta", + "pagination", + "symfony" + ], + "support": { + "issues": "https://github.com/BabDev/PagerfantaBundle/issues", + "source": "https://github.com/BabDev/PagerfantaBundle/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://github.com/mbabker", + "type": "github" + } + ], + "time": "2022-05-27T20:58:26+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0", + "phpunit/phpunit": "^8.5.25 || ^9.5.19" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Transliterator\\": "src/Behat/Transliterator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "support": { + "issues": "https://github.com/Behat/Transliterator/issues", + "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" + }, + "time": "2022-03-30T09:27:43+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.3" + }, + "time": "2022-07-02T10:48:51+00:00" + }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "1.6.8", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", + "shasum": "" + }, + "require": { + "php": "^7.1.3 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", + "vimeo/psalm": "^4.2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/1.6.8" + }, + "time": "2021-08-10T18:51:53+00:00" + }, + { + "name": "doctrine/common", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "c824e95d4c83b7102d8bc60595445a6f7d540f96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/c824e95d4c83b7102d8bc60595445a6f7d540f96", + "reference": "c824e95d4c83b7102d8bc60595445a6f7d540f96", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^4.0.5", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2022-02-05T18:28:51+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.3.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f79d4650430b582f4598fe0954ef4d52fbc0a8a", + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1.0", + "php": "^7.3 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "9.0.0", + "jetbrains/phpstorm-stubs": "2022.1", + "phpstan/phpstan": "1.7.13", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "9.5.20", + "psalm/plugin-phpunit": "0.16.1", + "squizlabs/php_codesniffer": "3.7.0", + "symfony/cache": "^5.2|^6.0", + "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", + "vimeo/psalm": "4.23.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.3.7" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2022-06-13T21:43:03+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "d2088fc50494e4e7441fecca54732245a613eeb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/d2088fc50494e4e7441fecca54732245a613eeb6", + "reference": "d2088fc50494e4e7441fecca54732245a613eeb6", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^2.13.1|^3.3.2", + "doctrine/persistence": "^2.2|^3", + "doctrine/sql-formatter": "^1.0.1", + "php": "^7.1 || ^8.0", + "symfony/cache": "^4.3.3|^5.0|^6.0", + "symfony/config": "^4.4.3|^5.0|^6.0", + "symfony/console": "^3.4.30|^4.3.3|^5.0|^6.0", + "symfony/dependency-injection": "^4.4.18|^5.0|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/doctrine-bridge": "^4.4.22|^5.2.7|^6.0", + "symfony/framework-bundle": "^3.4.30|^4.3.3|^5.0|^6.0", + "symfony/service-contracts": "^1.1.1|^2.0|^3" + }, + "conflict": { + "doctrine/orm": "<2.10|>=3.0", + "twig/twig": "<1.34|>=2.0,<2.4" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "doctrine/orm": "^2.11 || ^3.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.3 || ^10.0", + "psalm/plugin-phpunit": "^0.16.1", + "psalm/plugin-symfony": "^3", + "psr/log": "^1.1.4|^2.0|^3.0", + "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/property-info": "^4.3.3|^5.0|^6.0", + "symfony/proxy-manager-bridge": "^3.4|^4.3.3|^5.0|^6.0", + "symfony/security-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bridge": "^3.4.30|^4.3.3|^5.0|^6.0", + "symfony/validator": "^3.4.30|^4.3.3|^5.0|^6.0", + "symfony/web-profiler-bundle": "^3.4.30|^4.3.3|^5.0|^6.0", + "symfony/yaml": "^3.4.30|^4.3.3|^5.0|^6.0", + "twig/twig": "^1.34|^2.12|^3.0", + "vimeo/psalm": "^4.7" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.7.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2022-06-10T10:55:26+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "3393f411ba25ade21969c33f2053220044854d01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/3393f411ba25ade21969c33f2053220044854d01", + "reference": "3393f411ba25ade21969c33f2053220044854d01", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "~1.0|~2.0", + "doctrine/migrations": "^3.2", + "php": "^7.2|^8.0", + "symfony/framework-bundle": "~3.4|~4.0|~5.0|~6.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "doctrine/orm": "^2.6", + "doctrine/persistence": "^1.3||^2.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^8.0|^9.0", + "vimeo/psalm": "^4.11" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2022-02-01T18:08:07+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/1.1.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2020-05-29T18:28:51+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:16:43+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "c0a01ddead0ccaf0282f3f4fcaa026d11918a481" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/c0a01ddead0ccaf0282f3f4fcaa026d11918a481", + "reference": "c0a01ddead0ccaf0282f3f4fcaa026d11918a481", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.3", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "php": "^7.4 || ^8.0", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^4.4.16 || ^5.4 || ^6.0", + "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0" + }, + "conflict": { + "doctrine/orm": "<2.12" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "doctrine/orm": "^2.12", + "doctrine/persistence": "^2 || ^3", + "doctrine/sql-formatter": "^1.0", + "ergebnis/composer-normalize": "^2.9", + "ext-pdo_sqlite": "*", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan-symfony": "^1.1", + "phpunit/phpunit": "^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "symfony/process": "^4.4 || ^5.4 || ^6.0", + "symfony/yaml": "^4.4 || ^5.4 || ^6.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 4, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.5.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2022-05-09T20:24:38+00:00" + }, + { + "name": "doctrine/orm", + "version": "2.12.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "c05e1709e9ffb9abe8d37260a78975cc816ee385" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/c05e1709e9ffb9abe8d37260a78975cc816ee385", + "reference": "c05e1709e9ffb9abe8d37260a78975cc816ee385", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.12.1 || ^2.1.1", + "doctrine/collections": "^1.5", + "doctrine/common": "^3.0.3", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.1", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3", + "doctrine/lexer": "^1.2.3", + "doctrine/persistence": "^2.4 || ^3", + "ext-ctype": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "symfony/polyfill-php72": "^1.23", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "doctrine/annotations": "<1.13 || >= 2.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13", + "doctrine/coding-standard": "^9.0", + "phpbench/phpbench": "^0.16.10 || ^1.0", + "phpstan/phpstan": "~1.4.10 || 1.7.13", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.0", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", + "vimeo/psalm": "4.23.0" + }, + "suggest": { + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "lib/Doctrine/ORM" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/2.12.3" + }, + "time": "2022-06-16T13:42:23+00:00" + }, + { + "name": "doctrine/persistence", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "25ec98a8cbd1f850e60fdb62c0ef77c162da8704" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/25ec98a8cbd1f850e60fdb62c0ef77c162da8704", + "reference": "25ec98a8cbd1f850e60fdb62c0ef77c162da8704", + "shasum": "" + }, + "require": { + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/annotations": "<1.7 || >=2.0", + "doctrine/common": "<2.10" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^1.7", + "doctrine/coding-standard": "^9.0", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.5.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "vimeo/psalm": "4.22.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.0.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2022-05-06T06:10:05+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/25a06c7bf4c6b8218f47928654252863ffc890a5", + "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.1.3" + }, + "time": "2022-05-23T21:33:49+00:00" + }, + { + "name": "friendsofphp/proxy-manager-lts", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", + "reference": "8419f0158715b30d4b99a5bd37c6a39671994ad7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/8419f0158715b30d4b99a5bd37c6a39671994ad7", + "reference": "8419f0158715b30d4b99a5bd37c6a39671994ad7", + "shasum": "" + }, + "require": { + "laminas/laminas-code": "~3.4.1|^4.0", + "php": ">=7.1", + "symfony/filesystem": "^4.4.17|^5.0|^6.0" + }, + "conflict": { + "laminas/laminas-stdlib": "<3.2.1", + "zendframework/zend-stdlib": "<3.2.1" + }, + "replace": { + "ocramius/proxy-manager": "^2.1" + }, + "require-dev": { + "ext-phar": "*", + "symfony/phpunit-bridge": "^5.4|^6.0" + }, + "type": "library", + "extra": { + "thanks": { + "name": "ocramius/proxy-manager", + "url": "https://github.com/Ocramius/ProxyManager" + } + }, + "autoload": { + "psr-4": { + "ProxyManager\\": "src/ProxyManager" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + } + ], + "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager", + "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "support": { + "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues", + "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.12" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", + "type": "tidelift" + } + ], + "time": "2022-05-05T09:31:05+00:00" + }, + { + "name": "gedmo/doctrine-extensions", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", + "reference": "4ba5377b1a1d9a988205c4e37f5d5287eb8ebd28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/4ba5377b1a1d9a988205c4e37f5d5287eb8ebd28", + "reference": "4ba5377b1a1d9a988205c4e37f5d5287eb8ebd28", + "shasum": "" + }, + "require": { + "behat/transliterator": "~1.2", + "doctrine/annotations": "^1.13", + "doctrine/collections": "^1.0", + "doctrine/common": "^2.13 || ^3.0", + "doctrine/event-manager": "^1.0", + "doctrine/persistence": "^2.2 || ^3.0", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/cache": "^4.4 || ^5.3 || ^6.0" + }, + "conflict": { + "doctrine/cache": "<1.11", + "doctrine/dbal": "<2.13.1 || ^3.0 <3.2", + "doctrine/mongodb-odm": "<2.3", + "doctrine/orm": "<2.10.2", + "sebastian/comparator": "<2.0" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/doctrine-bundle": "^2.3", + "doctrine/mongodb-odm": "^2.3", + "doctrine/orm": "^2.10.2", + "friendsofphp/php-cs-fixer": "^3.4.0", + "nesbot/carbon": "^2.55", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^4.4 || ^5.3 || ^6.0", + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^4.4 || ^5.3 || ^6.0" + }, + "suggest": { + "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", + "doctrine/orm": "to use the extensions with the ORM", + "symfony/cache": "to cache parsed annotations" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.8-dev" + } + }, + "autoload": { + "psr-4": { + "Gedmo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gediminas Morkevicius", + "email": "gediminas.morkevicius@gmail.com" + }, + { + "name": "Gustavo Falco", + "email": "comfortablynumb84@gmail.com" + }, + { + "name": "David Buchmann", + "email": "david@liip.ch" + } + ], + "description": "Doctrine2 behavioral extensions", + "homepage": "http://gediminasm.org/", + "keywords": [ + "Blameable", + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree", + "uploadable" + ], + "support": { + "email": "gediminas.morkevicius@gmail.com", + "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.8.0", + "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" + }, + "time": "2022-07-17T12:04:16+00:00" + }, + { + "name": "knplabs/knp-time-bundle", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpTimeBundle.git", + "reference": "56e4d154bcd14c1b67c0d1defa1e7d00d27430e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpTimeBundle/zipball/56e4d154bcd14c1b67c0d1defa1e7d00d27430e3", + "reference": "56e4d154bcd14c1b67c0d1defa1e7d00d27430e3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/config": "^4.3|^5.0|^6.0", + "symfony/dependency-injection": "^4.3|^5.0|^6.0", + "symfony/translation": "^4.3|^5.0|^6.0" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "symfony/framework-bundle": "^4.3|^5.0|^6.0", + "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/templating": "^4.3|^5.0|^6.0", + "symfony/twig-bundle": "^4.3|^5.0|^6.0" + }, + "suggest": { + "symfony/twig-bundle": "to use the Twig `time_diff()` function or `|ago` filter" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\TimeBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "http://github.com/KnpLabs/KnpTimeBundle/contributors" + } + ], + "description": "Making your dates look sensible and descriptive", + "homepage": "http://github.com/KnpLabs/KnpTimeBundle", + "keywords": [ + "bundle", + "date", + "descriptive time", + "knp", + "knplabs", + "time" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpTimeBundle/issues", + "source": "https://github.com/KnpLabs/KnpTimeBundle/tree/v1.19.0" + }, + "time": "2022-05-10T00:14:22+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "4.5.2", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "da01fb74c08f37e20e7ae49f1e3ee09aa401ebad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/da01fb74c08f37e20e7ae49f1e3ee09aa401ebad", + "reference": "da01fb74c08f37e20e7ae49f1e3ee09aa401ebad", + "shasum": "" + }, + "require": { + "php": ">=7.4, <8.2" + }, + "require-dev": { + "doctrine/annotations": "^1.13.2", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^2.3.0", + "laminas/laminas-stdlib": "^3.6.1", + "phpunit/phpunit": "^9.5.10", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.13.1" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" + }, + "type": "library", + "autoload": { + "files": [ + "polyfill/ReflectionEnumPolyfill.php" + ], + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-06-06T11:26:02+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "0c375495d40df0207e5833dca333f963b171ff43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/0c375495d40df0207e5833dca333f963b171ff43", + "reference": "0c375495d40df0207e5833dca333f963b171ff43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5.16", + "predis/predis": "^1.1", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-06-09T09:09:00+00:00" + }, + { + "name": "pagerfanta/core", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/Pagerfanta/core.git", + "reference": "ed751ad06edea1d88451683405e4a207f20c26ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Pagerfanta/core/zipball/ed751ad06edea1d88451683405e4a207f20c26ac", + "reference": "ed751ad06edea1d88451683405e4a207f20c26ac", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^5.4 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pagerfanta\\": "./" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Core Pagerfanta API", + "keywords": [ + "pagerfanta" + ], + "support": { + "source": "https://github.com/Pagerfanta/core/tree/v3.6.1" + }, + "time": "2022-03-15T23:30:46+00:00" + }, + { + "name": "pagerfanta/doctrine-orm-adapter", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/Pagerfanta/doctrine-orm-adapter.git", + "reference": "592926e29e280f30a8912030247960f396faece7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Pagerfanta/doctrine-orm-adapter/zipball/592926e29e280f30a8912030247960f396faece7", + "reference": "592926e29e280f30a8912030247960f396faece7", + "shasum": "" + }, + "require": { + "doctrine/orm": "^2.8", + "pagerfanta/core": "^3.0", + "php": "^7.4 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.11.1", + "doctrine/cache": "^1.11 || ^2.0", + "phpunit/phpunit": "^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "symfony/phpunit-bridge": "^5.4 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pagerfanta\\Doctrine\\ORM\\": "./" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pagerfanta adapter for Doctrine ORM", + "keywords": [ + "doctrine", + "orm", + "pagerfanta" + ], + "support": { + "source": "https://github.com/Pagerfanta/doctrine-orm-adapter/tree/v3.6.1" + }, + "time": "2022-03-09T00:03:03+00:00" + }, + { + "name": "pagerfanta/twig", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/Pagerfanta/twig.git", + "reference": "0d7af77b027ffd613212abb434a40f4520f4e9d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Pagerfanta/twig/zipball/0d7af77b027ffd613212abb434a40f4520f4e9d5", + "reference": "0d7af77b027ffd613212abb434a40f4520f4e9d5", + "shasum": "" + }, + "require": { + "pagerfanta/core": "^3.0", + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.15", + "twig/twig": "^1.35 || ^2.5 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^5.4 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pagerfanta\\Twig\\": "./" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Twig integration for Pagerfanta", + "keywords": [ + "pagerfanta" + ], + "support": { + "source": "https://github.com/Pagerfanta/twig/tree/v3.6.1" + }, + "time": "2022-01-18T04:22:14+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sensio/framework-extra-bundle", + "version": "v6.2.6", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", + "reference": "6bd976c99ef3f78e31c9490a10ba6dd8901076eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/6bd976c99ef3f78e31c9490a10ba6dd8901076eb", + "reference": "6bd976c99ef3f78e31c9490a10ba6dd8901076eb", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "php": ">=7.2.5", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/doctrine-cache-bundle": "<1.3.1", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/dbal": "^2.10|^3.0", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.5", + "symfony/browser-kit": "^4.4|^5.0|^6.0", + "symfony/doctrine-bridge": "^4.4|^5.0|^6.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/monolog-bridge": "^4.0|^5.0|^6.0", + "symfony/monolog-bundle": "^3.2", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/security-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\FrameworkExtraBundle\\": "src/" + }, + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "This bundle provides a way to configure your controllers with annotations", + "keywords": [ + "annotations", + "controllers" + ], + "support": { + "issues": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/issues", + "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v6.2.6" + }, + "time": "2022-01-14T11:51:13+00:00" + }, + { + "name": "stof/doctrine-extensions-bundle", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git", + "reference": "a2bffca41974d1c968557b343e269a60a8d5ffa4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/a2bffca41974d1c968557b343e269a60a8d5ffa4", + "reference": "a2bffca41974d1c968557b343e269a60a8d5ffa4", + "shasum": "" + }, + "require": { + "gedmo/doctrine-extensions": "^2.3.4 || ^3.0.0", + "php": "^7.1.3 || ^8.0", + "symfony/config": "^4.4 || ^5.2 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.2 || ^6.0", + "symfony/event-dispatcher": "^4.4 || ^5.2 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.2 || ^6.0" + }, + "require-dev": { + "symfony/mime": "^4.4 || ^5.2 || ^6.0", + "symfony/phpunit-bridge": "^v5.2.4 || ^6.0", + "symfony/security-core": "^4.4 || ^5.2 || ^6.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "to use the ORM extensions", + "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions", + "symfony/mime": "To use the Mime component integration for Uploadable" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stof\\DoctrineExtensionsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integration of the gedmo/doctrine-extensions with Symfony", + "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle", + "keywords": [ + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree" + ], + "support": { + "issues": "https://github.com/stof/StofDoctrineExtensionsBundle/issues", + "source": "https://github.com/stof/StofDoctrineExtensionsBundle/tree/v1.7.0" + }, + "time": "2021-11-22T15:17:44+00:00" + }, + { + "name": "symfony/asset", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "dc6f572f142627d42ba88a42ea34f79d3776ee6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/dc6f572f142627d42ba88a42ea34f79d3776ee6c", + "reference": "dc6f572f142627d42ba88a42ea34f79d3776ee6c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-14T08:23:11+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "7d8415956df68c8dcbc9468e119945e39bacead1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/7d8415956df68c8dcbc9468e119945e39bacead1", + "reference": "7d8415956df68c8dcbc9468e119945e39bacead1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2|^3", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^5.4|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an extended PSR-6, PSR-16 (and tags) implementation", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-19T13:21:48+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "2eab7fa459af6d75c6463e63e633b667a9b761d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/2eab7fa459af6d75c6463e63e633b667a9b761d3", + "reference": "2eab7fa459af6d75c6463e63e633b667a9b761d3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/config", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "ed8d12417bcacd2d969750feb1fe1aab1c11e613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/ed8d12417bcacd2d969750feb1fe1aab1c11e613", + "reference": "ed8d12417bcacd2d969750feb1fe1aab1c11e613", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-17T12:56:32+00:00" + }, + { + "name": "symfony/console", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T13:01:30+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "5635ff016a814d7984b1c4644ad28e7df546077b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5635ff016a814d7984b1c4644ad28e7df546077b", + "reference": "5635ff016a814d7984b1c4644ad28e7df546077b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/service-contracts": "^1.1.6|^2.0|^3.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1", + "symfony/expression-language": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T13:01:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "3f5e41d66dc82b92f31a1c6cc6bb3443cb8844f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/3f5e41d66dc82b92f31a1c6cc6bb3443cb8844f4", + "reference": "3f5e41d66dc82b92f31a1c6cc6bb3443cb8844f4", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "~1.0", + "doctrine/persistence": "^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.7.4", + "phpunit/phpunit": "<5.4.3", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/form": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-bundle": "<5.4", + "symfony/security-core": "<6.0", + "symfony/validator": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "doctrine/collections": "~1.0", + "doctrine/data-fixtures": "^1.1", + "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/orm": "^2.7.4", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/doctrine-messenger": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4.9|^6.0.9", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/proxy-manager-bridge": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "doctrine/data-fixtures": "", + "doctrine/dbal": "", + "doctrine/orm": "", + "symfony/form": "", + "symfony/property-info": "", + "symfony/validator": "" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-21T07:16:29+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "568c11bcedf419e7e61f663912c3547b54de51df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/568c11bcedf419e7e61f663912c3547b54de51df", + "reference": "568c11bcedf419e7e61f663912c3547b54de51df", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-01T07:15:35+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "d02c662651e5de760bb7d5e94437113309e8f8a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d02c662651e5de760bb7d5e94437113309e8f8a0", + "reference": "d02c662651e5de760bb7d5e94437113309e8f8a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-23T10:32:57+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a0449a7ad7daa0f7c0acd508259f80544ab5a347", + "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^5.4|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-05T16:51:07+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "02ff5eea2f453731cfbc6bc215e456b781480448" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/02ff5eea2f453731cfbc6bc215e456b781480448", + "reference": "02ff5eea2f453731cfbc6bc215e456b781480448", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3132d2f43ca799c2aa099f9738d98228c56baa5d", + "reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-21T13:34:40+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/45b8beb69d6eb3b05a65689ebfd4222326773f8f", + "reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-15T08:08:08+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "78510b1be591433513c8087deec24e9fd90d110d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/78510b1be591433513c8087deec24e9fd90d110d", + "reference": "78510b1be591433513c8087deec24e9fd90d110d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-15T06:51:57+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v6.1.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "58a8a7c1ea054d45f5ee6435ebbfe2606c7e869a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/58a8a7c1ea054d45f5ee6435ebbfe2606c7e869a", + "reference": "58a8a7c1ea054d45f5ee6435ebbfe2606c7e869a", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^5.4|^6.0" + }, + "conflict": { + "doctrine/annotations": "<1.13.1", + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "phpunit/phpunit": "<5.4.3", + "symfony/asset": "<5.4", + "symfony/console": "<5.4", + "symfony/dom-crawler": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/mime": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-core": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/serializer": "<6.1", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<5.4", + "symfony/web-profiler-bundle": "<5.4", + "symfony/workflow": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13.1|^2", + "doctrine/persistence": "^1.3|^2|^3", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4.9|^6.0.9", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/semaphore": "^5.4|^6.0", + "symfony/serializer": "^6.1", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.10|^3.0" + }, + "suggest": { + "ext-apcu": "For best performance of the system caches", + "symfony/console": "For using the console commands", + "symfony/form": "For using forms", + "symfony/property-info": "For using the property_info service", + "symfony/serializer": "For using the serializer service", + "symfony/validator": "For using validation", + "symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering", + "symfony/yaml": "For using the debug:config and lint:yaml commands" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v6.1.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-11T11:51:43+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "ea1af6c8cc479147d67a3fead457dd7b06261041" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ea1af6c8cc479147d67a3fead457dd7b06261041", + "reference": "ea1af6c8cc479147d67a3fead457dd7b06261041", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "^3", + "symfony/service-contracts": "^1.0|^2|^3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-19T13:02:30+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "fd038f08c623ab5d22b26e9ba35afe8c79071800" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/fd038f08c623ab5d22b26e9ba35afe8c79071800", + "reference": "fd038f08c623ab5d22b26e9ba35afe8c79071800", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-22T07:30:54+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "86119d294e51afe4d8e07da96b63332bd1f3f52c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/86119d294e51afe4d8e07da96b63332bd1f3f52c", + "reference": "86119d294e51afe4d8e07da96b63332bd1f3f52c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-19T13:21:48+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "8aaede489900dda61aee208557f63bfa1bca0877" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/8aaede489900dda61aee208557f63bfa1bca0877", + "reference": "8aaede489900dda61aee208557f63bfa1bca0877", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.1", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<5.4", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dependency-injection": "^6.1", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/process": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T17:06:14+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "1efd6a42b1dc80e03bd74d4a2fdc85e728dc2627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1efd6a42b1dc80e03bd74d4a2fdc85e728dc2627", + "reference": "1efd6a42b1dc80e03bd74d4a2fdc85e728dc2627", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<6.0" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", + "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-21T08:28:57+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.22 || ^2.0 || ^3.0", + "php": ">=7.1.3", + "symfony/config": "~4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "~4.4 || ^5.0 || ^6.0", + "symfony/monolog-bridge": "~4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "symfony/console": "~4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/yaml": "~4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.8.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-10T14:24:36+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "433d05519ce6990bf3530fba6957499d327395c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/property-access", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "71a4cab59753349d25dfb1e34a342a6bd6a818cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/71a4cab59753349d25dfb1e34a342a6bd6a818cc", + "reference": "71a4cab59753349d25dfb1e34a342a6bd6a818cc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/property-info": "^5.4|^6.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:22:53+00:00" + }, + { + "name": "symfony/property-info", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "4989684b39c929fcf3ba432afdd932879fbc1334" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/4989684b39c929fcf3ba432afdd932879fbc1334", + "reference": "4989684b39c929fcf3ba432afdd932879fbc1334", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-10T06:48:13+00:00" + }, + { + "name": "symfony/proxy-manager-bridge", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/proxy-manager-bridge.git", + "reference": "bcb3e7a28ee373bd64ecd2a2289a64c5860716af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/proxy-manager-bridge/zipball/bcb3e7a28ee373bd64ecd2a2289a64c5860716af", + "reference": "bcb3e7a28ee373bd64ecd2a2289a64c5860716af", + "shasum": "" + }, + "require": { + "friendsofphp/proxy-manager-lts": "^1.0.2", + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0" + }, + "require-dev": { + "symfony/config": "^5.4|^6.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\ProxyManager\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for ProxyManager with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/proxy-manager-bridge/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-02T13:21:45+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "8f068b792e515b25e26855ac8dc7fe800399f3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/8f068b792e515b25e26855ac8dc7fe800399f3e5", + "reference": "8f068b792e515b25e26855ac8dc7fe800399f3e5", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-08T12:21:15+00:00" + }, + { + "name": "symfony/runtime", + "version": "v6.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "7be7f6e7546b2a680fd4a604ea27985eef3b0ef9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/7be7f6e7546b2a680fd4a604ea27985eef3b0ef9", + "reference": "7be7f6e7546b2a680fd4a604ea27985eef3b0ef9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.1" + }, + "conflict": { + "symfony/dotenv": "<5.4" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "symfony/console": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/runtime/tree/v6.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-02T11:43:19+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-30T19:18:58+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "77dedae82ce2a26e2e9b481855473fc3b3e4e54d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/77dedae82ce2a26e2e9b481855473fc3b3e4e54d", + "reference": "77dedae82ce2a26e2e9b481855473fc3b3e4e54d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/string", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "1903f2879875280c5af944625e8246d81c2f0604" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604", + "reference": "1903f2879875280c5af944625e8246d81c2f0604", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-26T16:35:04+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "b254416631615bc6fe49b0a67f18658827288147" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/b254416631615bc6fe49b0a67f18658827288147", + "reference": "b254416631615bc6fe49b0a67f18658827288147", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.3|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client-contracts": "^1.1|^2.0|^3.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0", + "symfony/service-contracts": "^1.1.2|^2|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-11T12:12:29+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "606be0f48e05116baef052f7f3abdb345c8e02cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/606be0f48e05116baef052f7f3abdb345c8e02cc", + "reference": "606be0f48e05116baef052f7f3abdb345c8e02cc", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-27T17:24:16+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "320b9ec7db38b78ce2ea8173b925be55e697f8e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/320b9ec7db38b78ce2ea8173b925be55e697f8e5", + "reference": "320b9ec7db38b78ce2ea8173b925be55e697f8e5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/translation-contracts": "^1.1|^2|^3", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<5.4", + "symfony/form": "<6.1", + "symfony/http-foundation": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4", + "symfony/workflow": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12", + "egulias/email-validator": "^2.1.10|^3", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/form": "^6.1", + "symfony/html-sanitizer": "^6.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "suggest": { + "symfony/asset": "For using the AssetExtension", + "symfony/expression-language": "For using the ExpressionExtension", + "symfony/finder": "", + "symfony/form": "For using the FormExtension", + "symfony/html-sanitizer": "For using the HtmlSanitizerExtension", + "symfony/http-kernel": "For using the HttpKernelExtension", + "symfony/routing": "For using the RoutingExtension", + "symfony/security-core": "For using the SecurityExtension", + "symfony/security-csrf": "For using the CsrfExtension", + "symfony/security-http": "For using the LogoutUrlExtension", + "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/translation": "For using the TranslationExtension", + "symfony/var-dumper": "For using the DumpExtension", + "symfony/web-link": "For using the WebLinkExtension", + "symfony/yaml": "For using the YamlExtension" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-19T13:21:48+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v6.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "a2abab10068525a7f5a879e40e411d369d688545" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/a2abab10068525a7f5a879e40e411d369d688545", + "reference": "a2abab10068525a7f5a879e40e411d369d688545", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.1", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/twig-bridge": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/framework-bundle": "<5.4", + "symfony/translation": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "symfony/asset": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v6.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-27T16:55:36+00:00" + }, + { + "name": "symfony/ux-turbo", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-turbo.git", + "reference": "0ac0bbc70b86aa4883c7eb8ac17bc6e3a774dc17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/0ac0bbc70b86aa4883c7eb8ac17bc6e3a774dc17", + "reference": "0ac0bbc70b86aa4883c7eb8ac17bc6e3a774dc17", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/webpack-encore-bundle": "^1.11" + }, + "conflict": { + "symfony/flex": "<1.13" + }, + "require-dev": { + "doctrine/annotations": "^1.12", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.8 | 3.0", + "phpstan/phpstan": "^0.12", + "symfony/debug-bundle": "^5.2|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.2|^6.0", + "symfony/framework-bundle": "^5.2|^6.0", + "symfony/mercure-bundle": "^0.3", + "symfony/messenger": "^5.2|^6.0", + "symfony/panther": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.2.1|^6.0", + "symfony/property-access": "^5.2|^6.0", + "symfony/security-core": "^5.2|^6.0", + "symfony/stopwatch": "^5.2|^6.0", + "symfony/twig-bundle": "^5.2|^6.0", + "symfony/web-profiler-bundle": "^5.2|^6.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Turbo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Hotwire Turbo integration for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "hotwire", + "javascript", + "mercure", + "symfony-ux", + "turbo", + "turbo-stream" + ], + "support": { + "source": "https://github.com/symfony/ux-turbo/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-08T14:57:18+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "98587d939cb783aa04e828e8fa857edaca24c212" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/98587d939cb783aa04e828e8fa857edaca24c212", + "reference": "98587d939cb783aa04e828e8fa857edaca24c212", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-21T13:34:40+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "ce1452317b1210ddfe18d143fa8a09c18f034b89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ce1452317b1210ddfe18d143fa8a09c18f034b89", + "reference": "ce1452317b1210ddfe18d143fa8a09c18f034b89", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-27T12:58:07+00:00" + }, + { + "name": "symfony/webpack-encore-bundle", + "version": "v1.15.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/webpack-encore-bundle.git", + "reference": "718673b1e758533614190ae74d07305a72bc66a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/718673b1e758533614190ae74d07305a72bc66a9", + "reference": "718673b1e758533614190ae74d07305a72bc66a9", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/asset": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25.0", + "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.3 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/web-link": "^4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/webpack-encore", + "url": "https://github.com/symfony/webpack-encore" + } + }, + "autoload": { + "psr-4": { + "Symfony\\WebpackEncoreBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Webpack Encore!", + "support": { + "issues": "https://github.com/symfony/webpack-encore-bundle/issues", + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v1.15.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-13T17:07:35+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "b01c4e7dc6a51cbf114567af04a19789fd1011fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b01c4e7dc6a51cbf114567af04a19789fd1011fe", + "reference": "b01c4e7dc6a51cbf114567af04a19789fd1011fe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-20T12:01:07+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "2e58256b0e9fe52f30149347c0547e4633304765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/2e58256b0e9fe52f30149347c0547e4633304765", + "reference": "2e58256b0e9fe52f30149347c0547e4633304765", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^2.12|^3.0", + "twig/html-extra": "^2.12|^3.0", + "twig/inky-extra": "^2.12|^3.0", + "twig/intl-extra": "^2.12|^3.0", + "twig/markdown-extra": "^2.12|^3.0", + "twig/string-extra": "^2.12|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2022-01-04T13:58:53+00:00" + }, + { + "name": "twig/twig", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/e939eae92386b69b49cfa4599dd9bead6bf4a342", + "reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2022-05-17T05:48:52+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "ba37bfb776de763c5bf04a36d074cd5f5a083c42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/ba37bfb776de763c5bf04a36d074cd5f5a083c42", + "reference": "ba37bfb776de763c5bf04a36d074cd5f5a083c42", + "shasum": "" + }, + "require": { + "doctrine/common": "^2.13|^3.0", + "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<2.13", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "doctrine/dbal": "^2.13 || ^3.0", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.7.0", + "ext-sqlite3": "*", + "jangregor/phpstan-prophecy": "^1", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/cache": "^5.0 || ^6.0", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/1.5.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2022-04-19T10:01:44+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "3.4.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/601988c5b46dbd20a0f886f967210aba378a6fd5", + "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.6.0", + "doctrine/persistence": "^1.3.7|^2.0|^3.0", + "php": "^7.1 || ^8.0", + "symfony/config": "^3.4|^4.3|^5.0|^6.0", + "symfony/console": "^3.4|^4.3|^5.0|^6.0", + "symfony/dependency-injection": "^3.4.47|^4.3|^5.0|^6.0", + "symfony/doctrine-bridge": "^3.4|^4.1|^5.0|^6.0", + "symfony/http-kernel": "^3.4|^4.3|^5.0|^6.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "^1.4.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "symfony/phpunit-bridge": "^6.0.8", + "vimeo/psalm": "^4.22" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2022-04-28T17:58:29+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/d7f08a622b3346766325488aa32ddc93ccdecc75", + "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "symfony/phpunit-bridge": "^4.4 || ^5.2" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "v1.19-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.19.0" + }, + "time": "2022-02-02T17:38:57+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.14.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + }, + "time": "2022-05-31T20:59:12+00:00" + }, + { + "name": "symfony/debug-bundle", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug-bundle.git", + "reference": "46c6fea401634b4bcdf9bd46a29065e7ea83a62c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/46c6fea401634b4bcdf9bd46a29065e7ea83a62c", + "reference": "46c6fea401634b4bcdf9bd46a29065e7ea83a62c", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/dependency-injection": "<5.4" + }, + "require-dev": { + "symfony/config": "^5.4|^6.0", + "symfony/web-profiler-bundle": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "For service container configuration", + "symfony/dependency-injection": "For using as a service from the container" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\DebugBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug-bundle/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-15T14:25:02+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.44.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "fa535d792e9261ae7a78481f643c00991e1d1588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/fa535d792e9261ae7a78481f643c00991e1d1588", + "reference": "fa535d792e9261ae7a78481f643c00991e1d1588", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.11", + "php": ">=8.0", + "symfony/config": "^5.4.7|^6.0", + "symfony/console": "^5.4.7|^6.0", + "symfony/dependency-injection": "^5.4.7|^6.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^5.4.7|^6.0", + "symfony/finder": "^5.4.3|^6.0", + "symfony/framework-bundle": "^5.4.7|^6.0", + "symfony/http-kernel": "^5.4.7|^6.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.4", + "doctrine/orm": "<2.10", + "symfony/doctrine-bridge": "<5.4" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.4", + "doctrine/orm": "^2.10.0", + "symfony/http-client": "^5.4.7|^6.0", + "symfony/phpunit-bridge": "^5.4.7|^6.0", + "symfony/polyfill-php80": "^1.16.0", + "symfony/process": "^5.4.7|^6.0", + "symfony/security-core": "^5.4.7|^6.0", + "symfony/yaml": "^5.4.3|^6.0", + "twig/twig": "^2.0|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.44.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-13T17:37:04+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "6589c2ee4b94d7df2f8ca160ec41265fee3f33eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/6589c2ee4b94d7df2f8ca160ec41265fee3f33eb", + "reference": "6589c2ee4b94d7df2f8ca160ec41265fee3f33eb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/http-kernel": "^6.1", + "symfony/routing": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/form": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-12T09:53:37+00:00" + }, + { + "name": "zenstruck/assert", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/assert.git", + "reference": "a1c6501c975339ded3bcf8092a17277f6e21ac82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/assert/zipball/a1c6501c975339ded3bcf8092a17277f6e21ac82", + "reference": "a1c6501c975339ded3bcf8092a17277f6e21ac82", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/polyfill-php81": "^1.23" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^5.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Standalone, lightweight, framework agnostic, test assertion library.", + "homepage": "https://github.com/zenstruck/assert", + "keywords": [ + "assertion", + "phpunit", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/assert/issues", + "source": "https://github.com/zenstruck/assert/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2022-07-12T12:30:27+00:00" + }, + { + "name": "zenstruck/callback", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/callback.git", + "reference": "b65a8d67eb77ec1b9139cf22fded85d0fe5d1c03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/callback/zipball/b65a8d67eb77ec1b9139cf22fded85d0fe5d1c03", + "reference": "b65a8d67eb77ec1b9139cf22fded85d0fe5d1c03", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.14" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Callable wrapper to validate and inject arguments.", + "homepage": "https://github.com/zenstruck/callback", + "keywords": [ + "callable", + "callback", + "utility" + ], + "support": { + "issues": "https://github.com/zenstruck/callback/issues", + "source": "https://github.com/zenstruck/callback/tree/v1.4.2" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2022-03-26T21:35:54+00:00" + }, + { + "name": "zenstruck/foundry", + "version": "v1.21.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/foundry.git", + "reference": "a535f83ef65680a63f615d64b04219cdeee35f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/a535f83ef65680a63f615d64b04219cdeee35f1b", + "reference": "a535f83ef65680a63f615d64b04219cdeee35f1b", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "fakerphp/faker": "^1.5", + "php": ">=7.2.12", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/property-access": "^3.4|^4.4|^5.0|^6.0", + "zenstruck/assert": "^1.0", + "zenstruck/callback": "^1.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "dama/doctrine-test-bundle": "^6.0|^7.0", + "doctrine/doctrine-bundle": "^2.0", + "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", + "doctrine/mongodb-odm-bundle": "^3.1|^4.2", + "doctrine/orm": "^2.7", + "matthiasnoback/symfony-dependency-injection-test": "^4.1", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/maker-bundle": "^1.30", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + }, + "bamarni-bin": { + "target-directory": "bin/tools" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Zenstruck\\Foundry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", + "homepage": "https://github.com/zenstruck/foundry", + "keywords": [ + "Fixture", + "doctrine", + "factory", + "faker", + "symfony", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/foundry/issues", + "source": "https://github.com/zenstruck/foundry/tree/v1.21.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2022-06-27T18:52:09+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.1.0" + }, + "plugin-api-version": "2.3.0" +} diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000..e1a3602 --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,21 @@ + ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Symfony\UX\Turbo\TurboBundle::class => ['all' => true], + Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true], + BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], +]; diff --git a/config/packages/babdev_pagerfanta.yaml b/config/packages/babdev_pagerfanta.yaml new file mode 100644 index 0000000..7449986 --- /dev/null +++ b/config/packages/babdev_pagerfanta.yaml @@ -0,0 +1,3 @@ +babdev_pagerfanta: + default_view: twig + default_twig_template: '@BabDevPagerfanta/twitter_bootstrap5.html.twig' diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000..2e83e04 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,20 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + app: '%cache_adapter%' + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null + diff --git a/config/packages/debug.yaml b/config/packages/debug.yaml new file mode 100644 index 0000000..ad874af --- /dev/null +++ b/config/packages/debug.yaml @@ -0,0 +1,5 @@ +when@dev: + debug: + # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. + # See the "server:dump" command to start a new server. + dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..9a18675 --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,42 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + server_version: '13' + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..a0a17a0 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: '%kernel.debug%' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000..51181fd --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,31 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + http_method_override: false + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: null + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native + + #esi: true + #fragments: true + php_errors: + log: true + + http_client: + scoped_clients: + githubContentClient: + base_uri: https://raw.githubusercontent.com + headers: + Authorization: 'Token %env(GITHUB_TOKEN)%' + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..8c9efa9 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,61 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000..4b766ce --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,12 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/config/packages/sensio_framework_extra.yaml b/config/packages/sensio_framework_extra.yaml new file mode 100644 index 0000000..1821ccc --- /dev/null +++ b/config/packages/sensio_framework_extra.yaml @@ -0,0 +1,3 @@ +sensio_framework_extra: + router: + annotations: false diff --git a/config/packages/stof_doctrine_extensions.yaml b/config/packages/stof_doctrine_extensions.yaml new file mode 100644 index 0000000..5082189 --- /dev/null +++ b/config/packages/stof_doctrine_extensions.yaml @@ -0,0 +1,8 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html +# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc +stof_doctrine_extensions: + default_locale: en_US + orm: + default: + timestampable: true + sluggable: true diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..abb76aa --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,13 @@ +framework: + default_locale: en + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - en +# providers: +# crowdin: +# dsn: '%env(CROWDIN_DSN)%' +# loco: +# dsn: '%env(LOCO_DSN)%' +# lokalise: +# dsn: '%env(LOKALISE_DSN)%' diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..f9f4cc5 --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + default_path: '%kernel.project_dir%/templates' + +when@test: + twig: + strict_variables: true diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000..b946111 --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,17 @@ +when@dev: + web_profiler: + toolbar: true + intercept_redirects: false + + framework: + profiler: + only_exceptions: false + collect_serializer_data: true + +when@test: + web_profiler: + toolbar: false + intercept_redirects: false + + framework: + profiler: { collect: false } diff --git a/config/packages/webpack_encore.yaml b/config/packages/webpack_encore.yaml new file mode 100644 index 0000000..46834f0 --- /dev/null +++ b/config/packages/webpack_encore.yaml @@ -0,0 +1,49 @@ +webpack_encore: + # The path where Encore is building the assets - i.e. Encore.setOutputPath() + output_path: '%kernel.project_dir%/public/build' + # If multiple builds are defined (as shown below), you can disable the default build: + # output_path: false + + # Set attributes that will be rendered on all script and link tags + script_attributes: + defer: true + # Uncomment (also under link_attributes) if using Turbo Drive + # https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change + # 'data-turbo-track': reload + # link_attributes: + # Uncomment if using Turbo Drive + # 'data-turbo-track': reload + + # If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') + # crossorigin: 'anonymous' + + # Preload all rendered script and link tags automatically via the HTTP/2 Link header + # preload: true + + # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data + # strict_mode: false + + # If you have multiple builds: + # builds: + # pass "frontend" as the 3rg arg to the Twig functions + # {{ encore_entry_script_tags('entry1', null, 'frontend') }} + + # frontend: '%kernel.project_dir%/public/frontend/build' + + # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) + # Put in config/packages/prod/webpack_encore.yaml + # cache: true + +framework: + assets: + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' + +#when@prod: +# webpack_encore: +# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) +# # Available in version 1.2 +# cache: true + +#when@test: +# webpack_encore: +# strict_mode: false diff --git a/config/packages/zenstruck_foundry.yaml b/config/packages/zenstruck_foundry.yaml new file mode 100644 index 0000000..0657d2c --- /dev/null +++ b/config/packages/zenstruck_foundry.yaml @@ -0,0 +1,7 @@ +when@dev: &dev + # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration + zenstruck_foundry: + # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh) + auto_refresh_proxies: true + +when@test: *dev diff --git a/config/preload.php b/config/preload.php new file mode 100644 index 0000000..5ebcdb2 --- /dev/null +++ b/config/preload.php @@ -0,0 +1,5 @@ + null, +]; diff --git a/config/secrets/prod/prod.GITHUB_TOKEN.28bd2f.php b/config/secrets/prod/prod.GITHUB_TOKEN.28bd2f.php new file mode 100644 index 0000000..9b238b7 --- /dev/null +++ b/config/secrets/prod/prod.GITHUB_TOKEN.28bd2f.php @@ -0,0 +1,3 @@ + null, +]; diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..c6d06ad --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,31 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration +parameters: + cache_adapter: 'cache.adapter.filesystem' + +when@dev: + parameters: + cache_adapter: 'cache.adapter.array' + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + bind: + 'bool $isDebug': '%kernel.debug%' + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/' + exclude: + - '../src/DependencyInjection/' + - '../src/Entity/' + - '../src/Kernel.php' + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..f2247d5 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,8 @@ +version: '3' + +services: +###> doctrine/doctrine-bundle ### + database: + ports: + - "5432" +###< doctrine/doctrine-bundle ### diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a80a8cf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' + +services: +###> doctrine/doctrine-bundle ### + database: + image: postgres:${POSTGRES_VERSION:-13}-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-app} + # You should definitely change the password in production + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ChangeMe} + POSTGRES_USER: ${POSTGRES_USER:-symfony} + volumes: + - db-data:/var/lib/postgresql/data:rw + # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! + # - ./docker/db/data:/var/lib/postgresql/data:rw +###< doctrine/doctrine-bundle ### + +volumes: +###> doctrine/doctrine-bundle ### + db-data: +###< doctrine/doctrine-bundle ### diff --git a/migrations/.gitignore b/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/migrations/Version20220718170654.php b/migrations/Version20220718170654.php new file mode 100644 index 0000000..4135368 --- /dev/null +++ b/migrations/Version20220718170654.php @@ -0,0 +1,35 @@ +addSql('CREATE SEQUENCE vinyl_mix_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE vinyl_mix (id INT NOT NULL, title VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, track_count INT NOT NULL, genre VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('COMMENT ON COLUMN vinyl_mix.created_at IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE vinyl_mix_id_seq CASCADE'); + $this->addSql('DROP TABLE vinyl_mix'); + } +} diff --git a/migrations/Version20220718170741.php b/migrations/Version20220718170741.php new file mode 100644 index 0000000..68af151 --- /dev/null +++ b/migrations/Version20220718170741.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE vinyl_mix ADD votes INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE vinyl_mix DROP votes'); + } +} diff --git a/migrations/Version20220718170826.php b/migrations/Version20220718170826.php new file mode 100644 index 0000000..68f3d0f --- /dev/null +++ b/migrations/Version20220718170826.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE vinyl_mix ADD updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('ALTER TABLE vinyl_mix ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE vinyl_mix ALTER created_at DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN vinyl_mix.created_at IS NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE vinyl_mix DROP updated_at'); + $this->addSql('ALTER TABLE vinyl_mix ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE vinyl_mix ALTER created_at DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN vinyl_mix.created_at IS \'(DC2Type:datetime_immutable)\''); + } +} diff --git a/migrations/Version20220718171433.php b/migrations/Version20220718171433.php new file mode 100644 index 0000000..ee9bb58 --- /dev/null +++ b/migrations/Version20220718171433.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE vinyl_mix ADD slug VARCHAR(100) NOT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_38F28AFB989D9B62 ON vinyl_mix (slug)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX UNIQ_38F28AFB989D9B62'); + $this->addSql('ALTER TABLE vinyl_mix DROP slug'); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..84d80cf --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "devDependencies": { + "@fontsource/roboto-condensed": "^4.5.2", + "@fortawesome/fontawesome-free": "^6.0.0", + "@hotwired/stimulus": "^3.0.0", + "@hotwired/turbo": "^7.0.1", + "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", + "@symfony/webpack-encore": "^3.0.0", + "axios": "^0.27.0", + "bootstrap": "^5.1.3", + "core-js": "^3.23.0", + "regenerator-runtime": "^0.13.9", + "webpack-notifier": "^1.15.0" + }, + "license": "UNLICENSED", + "private": true, + "scripts": { + "dev-server": "encore dev-server", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress" + } +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100755 index 0000000..cc9ea31 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100755 index 0000000..a477481 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100755 index 0000000..a199313 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..9982c21 --- /dev/null +++ b/public/index.php @@ -0,0 +1,9 @@ + Unknown function: did you forget to run `composer require symfony/asset`. + +I keep saying that Symfony starts small... and then you install things *as* you need +them. Apparently, this `asset()` function comes from a part of Symfony that we don't +have yet! But getting it is easy. Copy this composer require command, spin over +to your terminal and run it: + +```terminal-silent +composer require symfony/asset +``` + +This is a pretty simple install: it downloads just this one package... and there +are no recipes. + +But when we try the page now... it works! Check out the HTML source. Interesting: +the `link` tag's `href` is still, literally, `/styles/app.css`. That's *exactly* +what we had before! So what the heck is this `asset()` function doing? + +The answer is... not much. But it's still a good idea to use. The `asset()` function +gives you two features. First, imagine you deploy to a sub-directory of a domain. +Like, the homepage lives at https://example.com/mixed-vinyl. + +If that were the case, then in order for our CSS to work, the `href` would need +to be `/mixed-vinyl/styles/app.css`. In this situation, the `asset()` +function would detect the sub-directory automatically and add that prefix for you. + +The second - and more important thing that the `asset()` function does - is allow +you to easily switch to a CDN later. Because this path is now going through the +`asset()` function, we could, via a configuration file, say: + +> Hey Symfony! When you output this path, please prefix it with the URL +> to my CDN. + +This means that, when we load the the page, instead of `href="/styles/app.css`, it +would be something like `https://mycdn.com/styles/app.css`. + +So the `asset()` function might not be doing anything you need *today*, but anytime +you reference a static file - whether it's a CSS file, JavaScript file, image, +whatever, use this function. + +In fact, up here, I'm referencing three images. Let's use `asset`: `{{ asset()`... +and then it auto-completes the path! Thanks Symfony plugin! Repeat this for the +second image... and the third. + +[[[ code('e67894d6cf') ]]] + +We know this won't make any difference today... we can refresh the HTML source +to see the same paths... but we're ready for a CDN in the future. + +## Homepage And Browse Page HTML + +So the layout now looks great! But the content for our homepage is... just kind of +hanging out... looking weird... like me in middle school. Back in the `tutorial/` +directory, copy the homepage template... and overwrite our original file. + +Open that up. This still extends `base.html.twig`... and it still overrides the +`body` block. And then, it has a bunch of completely hard coded HTML. Let's go see +what it looks like. Refresh and... it looks awesome! + +Except that... it's 100% hard coded. Let's fix that. All the way on top, +here's the name of our record, print the `title` variable. + +And then, below for the songs.. we have a long list of hardcoded HTML. +Let's turn this into a loop. Add `{% for track in tracks %}` like we had before. +And... at the bottom, `endfor`. + +For the song details, use `track.song`... and `track.artist`. And now we can +remove *all* the hardcoded songs. + +[[[ code('a04b980086') ]]] + +Sweet! Let's try that. Hey! It's coming to life people! + +One more page to go! The `/browse` page. You know the drill: copy `browse.html.twig`, +and paste into our directory. This looks a lot like the homepage: it extends +`base.html.twig` and overrides block `body`. + +Over in `VinylController`, we weren't rendering a template before... so let's do +that now: `return $this->render('vinyl/browse.html.twig')` and let's pass in the +genre. Add a variable for that: `$genre =` and if we have a slug... use our fancy +title-case code, else set this to null. Then delete the `$title` stuff... and pass +`genre` into Twig. + +[[[ code('91a16480e7') ]]] + +Back in the template, use this in the `h1`. In Twig, we can *also* use fancy +syntax. So *if* we have a `genre`, print `genre`, else print `All Genres`. + +[[[ code('49da07784d') ]]] + +Testing time. Head over to `/browse`: "Browse all genres"! And then +`/browse/death-metal`: Browse Death Metal. Friends, this is starting to feel like +a real site! + +Except that these links up in the nav... go nowhere! Let's fix that next by +learning how to generate URLs. We're also going to meet the mega-powerful +`bin/console` command line tool. diff --git a/sfcasts/ep1/directories.md b/sfcasts/ep1/directories.md new file mode 100644 index 0000000..73a3e78 --- /dev/null +++ b/sfcasts/ep1/directories.md @@ -0,0 +1,89 @@ +# Meet our Tiny App + +Let's get to know our new project because my *ultimate* goal is for you to *really* +understand how things work. As I mentioned, there isn't a lot here yet... about +15 files. And there are really only three directories that we ever need to think +or worry about. + +## The public/ Directory + +The first is `public/`... and this is simple: it's the document root. In other +words, if you need a file to be publicly accessible - like an image file or a CSS +file - it needs to live inside `public/`. + +Right now, this holds exactly one file: `index.php`, which is called the "front +controller". + +[[[code('f3303a0b1f')]]] + +Ooo. That's a fancy word that means that - no matter what URL the +user goes to - *this* is the script that's always executed first. Its job is to +boot up Symfony and run our app. And now that we've looked at it, we'll probably +never need to think about or open it ever again. + +## config/ & src/ + +And, really, other than putting CSS or image files into `public/`, this is *not* +a directory you will deal with on a day-to-day basis. Which means... I kinda lied! +There are really only *two* directories that we need to think about: `config/` +and `src/`. + +The `config/` directory holds... kittens! Oh, I wish. Nah, it holds config files. +And `src/` holds 100% of your PHP classes. We will spend 95% of our time inside the +`src/` directory. + +## composer.json & vendor/ + +Okay... so where is "Symfony"? Our project started with a `composer.json` file. This +lists all of the third party libraries that our app needs. The "symfony new" command +that we ran secretly used "Composer" - that's PHP's package manager - to install +these libraries... which is really just a way of saying that Composer downloaded +these libraries into the `vendor/` directory. + +***SEEALSO +If you're not familiar with Composer package manager - check out our separate course +called [Wonderful World of Composer](https://symfonycasts.com/screencast/composer). +*** + +Symfony itself is actually a collection of a bunch of small libraries that each +solve a specific problem. In the `vendor/symfony/` directory, it looks like we +already have about 25 of these. *Technically* our app only requires these +six packages, but some of *those* packages require *other* packages... and Composer +is smart enough to download everything we need. + +*Anyways*, "Symfony", or really, a set of Symfony libraries, lives in the `vendor/` +directory and our new app leverages that code to do its job. We're going to talk +more about Composer and installing third party packages later. But for the most +part, `vendor/` is yet another directory that... we don't need to worry about! + +## bin/ and var/ + +So what's left? Well, `bin/` holds exactly one file... and will always hold just +this one file. We'll talk about what `bin/console` does a bit later. And the `var/` +directory holds cache and log files. Those files are important... but *we* will +never need to look at or think about that stuff. + +Yup, we're going to live pretty much entirely inside of the `config/` and `src/` +directories. + +## PhpStorm Setup + +Ok, one last piece of homework before we start coding. Feel free to use whatever +code editor you want: PhpStorm, VS Code, code carrier pigeon, whatever. But I *highly* +recommend PhpStorm. It makes developing with Symfony a dream... and they're not even +paying me to say that! Though, if they *do* want to start paying me, I accept +payment in stroopwafels. + +Part of what makes PhpStorm *so* great is a plugin that's designed specifically for +Symfony. I'll go to my PhpStorm preferences and, inside, find Plugins, Marketplace +then search for Symfony. Here it is. This plugin is amazing.... which you can see +because it's been downloaded 5.4 million times! It adds tons of crazy +auto-completion features that are specific to Symfony. + +If you don't have it already, get it installed. Once it *is* installed, go *back* +to Settings and search up here for "Symfony" to find a new Symfony area. The one +trick about this plugin is that you need to enable it for each project. So click +that check box. Also, it's not too important, but change the web directory to `public/`. + +Hit Ok and... we are ready! Let's bring our app to life by creating our very +first page next. diff --git a/sfcasts/ep1/es/assets.md b/sfcasts/ep1/es/assets.md new file mode 100644 index 0000000..a1bb918 --- /dev/null +++ b/sfcasts/ep1/es/assets.md @@ -0,0 +1,104 @@ +# Activos, CSS, imágenes, etc + +Si descargas el código del curso desde la página en la que estás viendo este vídeo, después de descomprimirlo, encontrarás un directorio `start/` que contiene la misma aplicación nueva de Symfony 6 que hemos creado antes. En realidad no necesitas ese código, pero contiene un directorio extra llamado `tutorial/`, como el que tengo aquí. Este contiene algunos archivos que vamos a utilizar. + +Así que hablemos de nuestro siguiente objetivo: hacer que este sitio parezca un sitio real... en lugar de parecer algo que he diseñado yo mismo. Y eso significa que necesitamos un verdadero diseño HTML que incluya algo de CSS. + +## Añadir un diseño y archivos CSS + +Sabemos que nuestro archivo de diseño es `base.html.twig`... y también hay un archivo`base.html.twig` en el nuevo directorio `tutorial/`. Copia eso... pégalo en las plantillas, y anula el original. + +Antes de ver eso, copia también los tres archivos `.png` y ponlos en el directorio `public/`... para que nuestros usuarios puedan acceder a ellos. + +Muy bien. Abre el nuevo archivo `base.html.twig`. Aquí no hay nada especial. Traemos algunos archivos CSS externos de algunos CDN para Bootstrap y FontAwesome. Al final de este tutorial, refactorizaremos esto para que sea una forma más elegante de manejar el CSS... pero por ahora, esto funcionará bien. + +Por lo demás, todo sigue estando codificado. Tenemos una navegación codificada, el mismo bloque `body`... y un pie de página codificado. Vamos a ver cómo queda. ¡Refresca y woo! Bueno, no es perfecto, pero es mejor + + Añadir un archivo CSS personalizado + +El directorio `tutorial/` también contiene un archivo `app.css` con CSS personalizado. Para que esté disponible públicamente, de modo que el navegador de nuestro usuario pueda descargarlo, tiene que estar en algún lugar del directorio `public/`. Pero no importa dónde o cómo organices las cosas dentro. + +Creemos un directorio `styles/`... y luego copiamos `app.css`... y lo pegamos allí. + +De vuelta en `base.html.twig`, dirígete a la parte superior. Después de todos los archivos CSS externos, vamos a añadir una etiqueta de enlace para nuestro `app.css`. Así que ` Función desconocida: ¿te has olvidado de ejecutar `composer require symfony/asset`. + +Sigo diciendo que Symfony empieza con algo pequeño... y luego vas instalando cosas a medida que las necesitas. ¡Aparentemente, esta función `asset()` viene de una parte de Symfony que aún no tenemos! Pero conseguirla es fácil. Copia este comando composer require, pásalo a tu terminal y ejecútalo: + +```terminal-silent +composer require symfony/asset +``` + +Se trata de una instalación bastante sencilla: sólo descarga este paquete... y no hay recetas. + +Pero cuando probamos la página ahora... ¡funciona! Comprueba el código fuente HTML. Interesante: la etiqueta `link` `href` sigue siendo, literalmente, `/styles/app.css`. ¡Es exactamente lo que teníamos antes! Entonces, ¿qué diablos hace esta función `asset()`? + +La respuesta es... no mucho. Pero sigue siendo una buena idea utilizarla. La función `asset()` te ofrece dos características. En primer lugar, imagina que te despliegas en un subdirectorio de un dominio. Por ejemplo, la página de inicio vive en https://example.com/mixed-vinyl. + +Si ese fuera el caso, para que nuestro CSS funcione, el `href` tendría que ser `/mixed-vinyl/styles/app.css`. En esta situación, la función `asset()`detectaría el subdirectorio automáticamente y añadiría ese prefijo por ti. + +Lo segundo -y más importante- que hace la función `asset()` es permitirte cambiar fácilmente a una CDN más adelante. Como esta ruta pasa ahora por la función`asset()`, podríamos, a través de un archivo de configuración, decir: + +> ¡Hey Symfony! Cuando emitas esta ruta, por favor ponle el prefijo de la URL +> a mi CDN. + +Esto significa que, cuando carguemos la página, en lugar de `href="/styles/app.css`, sería algo como `https://mycdn.com/styles/app.css`. + +Así que la función `asset()` puede que no haga nada que necesites hoy, pero siempre que hagas referencia a un archivo estático, ya sea un archivo CSS, un archivo JavaScript, una imagen, lo que sea, utiliza esta función. + +De hecho, aquí arriba, estoy haciendo referencia a tres imágenes. Usemos `asset`: `{{ asset()`... ¡y entonces se autocompleta la ruta! ¡Gracias plugin Symfony! Repite esto para la segunda imagen... y la tercera. + +[[[ code('e67894d6cf') ]]] + +Sabemos que esto no supondrá ninguna diferencia hoy... podemos refrescar el código fuente HTML para ver las mismas rutas... pero estamos preparados para una CDN en el futuro. + +## HTML de la página de inicio y de navegación + +¡Así que el diseño ahora se ve muy bien! Pero el contenido de nuestra página de inicio está... como colgando... con un aspecto raro... como yo en la escuela secundaria. De vuelta al directorio `tutorial/`, copia la plantilla de la página de inicio... y sobrescribe nuestro archivo original. + +Ábrelo. Esto sigue extendiendo `base.html.twig`... y sigue anulando el bloque`body`. Y además, tiene un montón de HTML completamente codificado. Vamos a ver qué aspecto tiene. Actualiza y... ¡se ve genial! + +Excepto que... está 100% codificado. Vamos a arreglarlo. En la parte superior, aquí está el nombre de nuestro disco, imprime la variable `title`. + +Y luego, abajo para las canciones... tenemos una larga lista de HTML codificado. Convirtamos esto en un bucle. Añade `{% for track in tracks %}` como teníamos antes. Y... al final, `endfor`. + +Para los detalles de la canción, utiliza `track.song`... y `track.artist`. Y ahora podemos eliminar todas las canciones codificadas. + +[[[ code('a04b980086') ]]] + +¡Genial! Vamos a probarlo. ¡Hey! ¡Está cobrando vida gente! + +¡Falta una página más! La página `/browse`. Ya sabes lo que hay que hacer: copiar `browse.html.twig`, y pegar en nuestro directorio. Esto se parece mucho a la página de inicio: extiende`base.html.twig` y anula el bloque `body`. + +En `VinylController`, no hemos renderizado antes una plantilla... así que hagámoslo ahora: `return $this->render('vinyl/browse.html.twig')` y pasemos el género. Añade una variable para ello: `$genre =` y si tenemos un slug... utiliza nuestro elegante código de mayúsculas y minúsculas, si no, ponlo en null. Luego borra lo de `$title`... y pasa`genre` a Twig. + +[[[ code('91a16480e7') ]]] + +De vuelta a la plantilla, utiliza esto en el `h1`. En Twig, también podemos utilizar una sintaxis de fantasía. Así que si tenemos un `genre`, imprime `genre`, si no imprime `All Genres`. + +[[[ code('49da07784d') ]]] + +Es hora de probar. Dirígete a `/browse`: "Navega por todos los géneros" Y luego`/browse/death-metal`: Navega por el Death Metal. Amigos, ¡esto empieza a parecerse a un sitio real! + +Excepto que estos enlaces en el navegador... ¡no van a ninguna parte! Vamos a arreglar eso aprendiendo a generar URLs. También vamos a conocer la mega-poderosa herramienta de línea de comandos`bin/console`. diff --git a/sfcasts/ep1/es/assets.vtt b/sfcasts/ep1/es/assets.vtt new file mode 100644 index 0000000..07b37cb --- /dev/null +++ b/sfcasts/ep1/es/assets.vtt @@ -0,0 +1,540 @@ +WEBVTT + +00:00:00.036 --> 00:00:04.486 align:middle +Si descargas el código del curso desde la +página en la que estás viendo este vídeo, + +00:00:04.796 --> 00:00:07.436 align:middle +después de descomprimirlo, +encontrarás un directorio start/ + +00:00:07.546 --> 00:00:12.136 align:middle +que contiene la misma aplicación nueva +de Symfony 6 que hemos creado antes. + +00:00:12.876 --> 00:00:19.446 align:middle +En realidad no necesitas ese código, pero contiene +un directorio extra llamado tutorial/, como el que + +00:00:19.896 --> 00:00:20.936 align:middle +tengo aquí. + +00:00:21.866 --> 00:00:23.916 align:middle +Este contiene algunos +archivos que vamos a utilizar. + +00:00:24.746 --> 00:00:30.076 align:middle +Así que hablemos de nuestro siguiente objetivo: +hacer que este sitio parezca un sitio real... + +00:00:30.496 --> 00:00:33.446 align:middle +en lugar de parecer algo +que he diseñado yo mismo. + +00:00:33.816 --> 00:00:39.406 align:middle +Y eso significa que necesitamos una verdadera +maquetación HTML que incluya algo de CSS. + +00:00:40.506 --> 00:00:43.956 align:middle +Sabemos que nuestro archivo de +diseño es base.html.twig... + +00:00:44.446 --> 00:00:49.636 align:middle +y también hay un archivo base.html.twig +en el nuevo directorio tutorial/. + +00:00:50.516 --> 00:00:51.166 align:middle +Copia eso... + +00:00:51.676 --> 00:00:54.746 align:middle +pégalo en las plantillas, y anula el original. + +00:00:56.336 --> 00:01:00.746 align:middle +Antes de ver eso, copia +también los tres archivos .png + +00:01:01.026 --> 00:01:03.246 align:middle +y ponlos en el directorio public/... + +00:01:03.536 --> 00:01:06.236 align:middle +para que nuestros usuarios +puedan acceder a ellos. + +00:01:07.476 --> 00:01:08.866 align:middle +Muy bien. + +00:01:08.866 --> 00:01:12.076 align:middle +Abre el nuevo archivo base.html.twig. + +00:01:12.806 --> 00:01:14.636 align:middle +Aquí no hay nada especial. + +00:01:15.226 --> 00:01:21.056 align:middle +Traemos algunos archivos CSS externos de +algunos CDN para Bootstrap y FontAwesome. + +00:01:21.986 --> 00:01:27.866 align:middle +Al final de este tutorial, refactorizaremos esto +en una forma más elegante de manejar el CSS... + +00:01:28.106 --> 00:01:31.226 align:middle +pero por ahora, esto funcionará muy bien. + +00:01:32.046 --> 00:01:34.876 align:middle +Por lo demás, todo sigue estando codificado. + +00:01:35.156 --> 00:01:39.206 align:middle +Tenemos una navegación +codificada, el mismo bloque body... + +00:01:39.646 --> 00:01:41.146 align:middle +y un pie de página codificado. + +00:01:41.146 --> 00:01:43.756 align:middle +Vamos a ver qué aspecto tiene. + +00:01:44.186 --> 00:01:45.326 align:middle +Refresca y ¡vaya! + +00:01:45.786 --> 00:01:49.096 align:middle +Bueno, no es perfecto, pero sí mejor + +00:01:50.146 --> 00:01:55.186 align:middle +El directorio tutorial/ también contiene un +archivo app.css con CSS personalizado. Para que + +00:01:56.466 --> 00:02:01.186 align:middle +esté disponible públicamente, de modo que el +navegador de nuestro usuario pueda descargarlo, + +00:02:01.456 --> 00:02:04.866 align:middle +tiene que estar en algún +lugar del directorio public/. + +00:02:05.386 --> 00:02:09.116 align:middle +Pero no importa dónde o cómo +organices las cosas dentro. + +00:02:09.816 --> 00:02:11.676 align:middle +Vamos a crear un directorio styles/... + +00:02:13.116 --> 00:02:15.306 align:middle +y luego copiemos app.css... + +00:02:15.716 --> 00:02:16.756 align:middle +y pégalo allí. + +00:02:18.776 --> 00:02:22.406 align:middle +De vuelta en base.html.twig, +dirígete a la parte superior. + +00:02:24.876 --> 00:02:30.846 align:middle +Después de todos los archivos CSS externos, vamos a +añadir una etiqueta de enlace para nuestro app.css. + +00:02:31.406 --> 00:02:35.206 align:middle +Así que 00:02:42.486 align:middle +Como el directorio public/ es la raíz de nuestro documento, +para referirse a un archivo CSS o de imagen allí, + +00:02:42.486 --> 00:02:46.466 align:middle +la ruta debe ser con respecto a ese directorio. + +00:02:47.126 --> 00:02:52.076 align:middle +Así que esto será /styles/app.css. + +00:02:52.116 --> 00:02:52.846 align:middle +Vamos a comprobarlo. + +00:02:53.206 --> 00:02:54.856 align:middle +Actualiza ahora y... + +00:02:55.186 --> 00:02:56.336 align:middle +¡aún mejor! + +00:02:57.206 --> 00:02:58.636 align:middle +Quiero que te des cuenta de algo. + +00:02:59.116 --> 00:03:07.466 align:middle +Hasta ahora, Symfony no interviene para nada en cómo +organizamos o utilizamos las imágenes o los archivos CSS. + +00:03:07.846 --> 00:03:13.046 align:middle +No. Nuestra configuración es muy sencilla: +ponemos las cosas en el directorio public/... + +00:03:13.446 --> 00:03:15.736 align:middle +y luego nos referimos a ellas con sus rutas. + +00:03:16.236 --> 00:03:21.366 align:middle +Pero, ¿tiene Symfony alguna función interesante +para ayudar a trabajar con CSS y JavaScript? + +00:03:21.816 --> 00:03:22.706 align:middle +Por supuesto. + +00:03:23.146 --> 00:03:26.246 align:middle +Se llaman Webpack Encore y Stimulus. + +00:03:26.526 --> 00:03:29.946 align:middle +Y hablaremos de ambas hacia +el final del tutorial. + +00:03:30.486 --> 00:03:35.516 align:middle +Pero incluso en esta sencilla configuración -en la que +sólo ponemos archivos en public/ y apuntamos a ellos- + +00:03:36.036 --> 00:03:40.356 align:middle +Symfony tiene una característica +menor: la función asset(). + +00:03:41.206 --> 00:03:49.266 align:middle +Funciona así: en lugar de usar +/styles/app.css, decimos {{ asset() }} y luego, + +00:03:49.446 --> 00:03:52.726 align:middle +entre comillas, movemos nuestra ruta allí... + +00:03:53.246 --> 00:03:55.076 align:middle +pero sin la apertura "/". + +00:03:55.076 --> 00:03:59.226 align:middle +Así, la ruta sigue siendo +relativa al directorio public/... + +00:03:59.276 --> 00:04:02.006 align:middle +sólo que no necesitas incluir la primera "/". + +00:04:02.006 --> 00:04:05.276 align:middle +Antes de hablar de lo que hace esto... + +00:04:05.746 --> 00:04:07.196 align:middle +vamos a ver si funciona. + +00:04:07.676 --> 00:04:08.936 align:middle +Refresca y... + +00:04:09.786 --> 00:04:10.886 align:middle +¡no funciona! + +00:04:11.286 --> 00:04:17.606 align:middle +Error: Función desconocida: ¿te has olvidado +de ejecutar composer require symfony/asset. + +00:04:18.406 --> 00:04:20.626 align:middle +Sigo diciendo que Symfony empieza con poco... + +00:04:21.066 --> 00:04:24.286 align:middle +y luego va instalando cosas +a medida que las necesita. + +00:04:24.746 --> 00:04:30.736 align:middle +¡Aparentemente, esta función asset() viene +de una parte de Symfony que aún no tenemos! + +00:04:31.336 --> 00:04:33.186 align:middle +Pero conseguirla es fácil. + +00:04:33.646 --> 00:04:38.846 align:middle +Copia este comando composer require, +pásalo a tu terminal y ejecútalo: + +00:04:40.236 --> 00:04:44.966 align:middle +Es una instalación bastante sencilla: +sólo descarga este paquete... + +00:04:45.476 --> 00:04:47.706 align:middle +y no hay recetas. + +00:04:49.106 --> 00:04:50.646 align:middle +Pero cuando probamos la página ahora... + +00:04:51.776 --> 00:04:55.416 align:middle +¡funciona! Comprueba el código fuente HTML. + +00:04:57.166 --> 00:05:04.666 align:middle +Es interesante: la etiqueta link href sigue +siendo, literalmente, /styles/app.css. + +00:05:05.606 --> 00:05:07.996 align:middle +¡Es exactamente lo que teníamos antes! + +00:05:08.536 --> 00:05:12.296 align:middle +Entonces, ¿qué diablos +hace esta función asset()? + +00:05:12.786 --> 00:05:14.026 align:middle +La respuesta es... + +00:05:14.266 --> 00:05:17.996 align:middle +no mucho. Pero sigue siendo +una buena idea utilizarla. + +00:05:18.576 --> 00:05:21.096 align:middle +La función asset() te +ofrece dos características. + +00:05:21.206 --> 00:05:26.346 align:middle +En primer lugar, imagina que te despliegas +en un subdirectorio de un dominio. + +00:05:26.826 --> 00:05:31.206 align:middle +Por ejemplo, la página de inicio vive +en https://example.com/mixed-vinyl. + +00:05:32.186 --> 00:05:37.296 align:middle +Si ese fuera el caso, para que +nuestro CSS funcione, el href tendría + +00:05:37.296 --> 00:05:42.916 align:middle +que ser /mixed-vinyl/styles/app.css. + +00:05:42.996 --> 00:05:48.696 align:middle +En esta situación, la función asset() +detectaría el subdirectorio automáticamente + +00:05:49.076 --> 00:05:50.926 align:middle +y añadiría ese prefijo por ti. + +00:05:52.066 --> 00:05:55.916 align:middle +Lo segundo -y más importante- +que hace la función asset() + +00:05:56.246 --> 00:06:00.116 align:middle +es permitirte cambiar fácilmente +a una CDN más adelante. + +00:06:00.786 --> 00:06:05.196 align:middle +Como esta ruta pasa ahora por +la función asset(), podríamos, + +00:06:05.376 --> 00:06:08.326 align:middle +a través de un archivo de +configuración, decir: ¡Eh, Symfony! + +00:06:08.586 --> 00:06:13.366 align:middle +Cuando saques esta ruta, ponle +como prefijo la URL de mi CDN. + +00:06:14.536 --> 00:06:21.336 align:middle +Esto significa que, cuando carguemos la +página, en lugar de href="/styles/app.css, + +00:06:21.666 --> 00:06:28.976 align:middle +sería algo como +https://mycdn.com/styles/app.css. + +00:06:30.016 --> 00:06:34.036 align:middle +Así que la función asset() puede +que no haga nada que necesites hoy, + +00:06:34.566 --> 00:06:39.776 align:middle +pero siempre que hagas referencia a un archivo +estático, ya sea un archivo CSS, un archivo JavaScript, + +00:06:39.876 --> 00:06:42.646 align:middle +una imagen, lo que sea, utiliza esta función. + +00:06:43.916 --> 00:06:47.836 align:middle +De hecho, aquí arriba, estoy +haciendo referencia a tres imágenes. + +00:06:48.616 --> 00:06:51.436 align:middle +Vamos a utilizar asset: {{ asset()... + +00:06:52.256 --> 00:06:54.916 align:middle +¡y entonces se autocompleta la ruta! + +00:06:55.316 --> 00:06:56.666 align:middle +¡Gracias plugin Symfony! + +00:06:57.446 --> 00:06:59.836 align:middle +Repite esto para la segunda imagen... + +00:07:00.576 --> 00:07:02.126 align:middle +y la tercera. + +00:07:03.876 --> 00:07:06.186 align:middle +Sabemos que esto no hará +ninguna diferencia hoy... + +00:07:06.726 --> 00:07:10.596 align:middle +podemos refrescar la fuente HTML +para ver las mismas rutas... + +00:07:10.986 --> 00:07:13.396 align:middle +pero estamos preparados +para un CDN en el futuro. + +00:07:14.296 --> 00:07:16.546 align:middle +¡Así que el diseño ahora se ve muy bien! + +00:07:17.256 --> 00:07:19.556 align:middle +Pero el contenido de nuestra +página de inicio está... + +00:07:19.556 --> 00:07:21.346 align:middle +simplemente colgando... + +00:07:21.346 --> 00:07:22.346 align:middle +con un aspecto extraño... + +00:07:22.496 --> 00:07:23.846 align:middle +como yo en la escuela secundaria. + +00:07:24.686 --> 00:07:27.906 align:middle +De vuelta al directorio tutorial/, copia +la plantilla de la pá gina de inicio... + +00:07:28.096 --> 00:07:30.096 align:middle +y sobrescribe nuestro archivo original. + +00:07:34.456 --> 00:07:35.166 align:middle +Ábrelo. + +00:07:36.386 --> 00:07:39.166 align:middle +Esto sigue ampliando base.html.twig... + +00:07:39.576 --> 00:07:41.916 align:middle +y sigue sobreescribiendo el bloque body. + +00:07:42.736 --> 00:07:47.236 align:middle +Y además, tiene un montón de +HTML completamente codificado. + +00:07:47.276 --> 00:07:49.986 align:middle +Vamos a ver qué aspecto tiene. + +00:07:50.616 --> 00:07:51.706 align:middle +Actualiza y... + +00:07:52.126 --> 00:07:54.016 align:middle +¡se ve increíble! + +00:07:54.406 --> 00:07:55.406 align:middle +Excepto que... + +00:07:55.406 --> 00:07:57.686 align:middle +está 100% codificado. + +00:07:58.546 --> 00:07:59.436 align:middle +Vamos a arreglar eso. + +00:08:00.876 --> 00:08:05.826 align:middle +En la parte superior, aquí está el nombre +de nuestro disco, imprime la variable title. + +00:08:05.826 --> 00:08:09.036 align:middle +Y luego, abajo para las canciones.. + +00:08:09.826 --> 00:08:12.126 align:middle +tenemos una larga lista de HTML codificado. + +00:08:12.826 --> 00:08:14.046 align:middle +Vamos a convertir esto en un bucle. + +00:08:14.046 --> 00:08:18.906 align:middle +Añade {% for track in tracks +%} como teníamos antes. + +00:08:19.576 --> 00:08:21.676 align:middle +Y... al final, endfor. + +00:08:24.886 --> 00:08:27.986 align:middle +Para los detalles de las +canciones, utiliza track.song... + +00:08:29.336 --> 00:08:31.176 align:middle +y track.artist. + +00:08:32.676 --> 00:08:35.656 align:middle +Y ahora podemos eliminar todas +las canciones codificadas. + +00:08:40.976 --> 00:08:44.346 align:middle +¡Genial! Vamos a probarlo. + +00:08:45.936 --> 00:08:48.356 align:middle +¡Hey! ¡Está cobrando vida, gente! + +00:08:49.336 --> 00:08:50.446 align:middle +¡Falta una página más! + +00:08:50.666 --> 00:08:52.256 align:middle +La página /browse. + +00:08:53.146 --> 00:08:59.236 align:middle +Ya sabes lo que hay que hacer: copiar +browse.html.twig, y pegar en nuestro directorio. + +00:09:02.436 --> 00:09:08.966 align:middle +Esto se parece mucho a la página de inicio: +extiende base.html.twig y anula el bloque body. + +00:09:10.246 --> 00:09:14.446 align:middle +En VinylController, no hemos +renderizado antes una plantilla... + +00:09:14.676 --> 00:09:15.956 align:middle +así que hagámoslo ahora: + +00:09:16.836 --> 00:09:24.866 align:middle +return $this->render('vinyl/browse.html.twig') +y pasemos el género. + +00:09:25.646 --> 00:09:30.756 align:middle +Añade una variable para eso: +$genre = y si tenemos un slug... + +00:09:31.046 --> 00:09:37.546 align:middle +utiliza nuestro elegante código de mayúsculas +y minúsculas, si no, ponlo en null. + +00:09:38.206 --> 00:09:40.716 align:middle +Luego borra lo de $title... + +00:09:40.716 --> 00:09:42.826 align:middle +y pasa genre a Twig. + +00:09:46.006 --> 00:09:49.036 align:middle +De vuelta en la plantilla, +utiliza esto en el h1. + +00:09:50.056 --> 00:09:53.476 align:middle +En Twig, también podemos utilizar +una sintaxis extravagante. + +00:09:54.136 --> 00:10:01.246 align:middle +Así que si tenemos un genre, imprime +genre, si no imprime All Genres. + +00:10:03.306 --> 00:10:04.376 align:middle +Es hora de probar. + +00:10:04.756 --> 00:10:09.986 align:middle +Dirígete a /browse: +"Examinar todos los géneros" + +00:10:10.446 --> 00:10:15.216 align:middle +Y luego /browse/death-metal: +"Examinar el Death Metal ". + +00:10:15.216 --> 00:10:19.316 align:middle +Amigos, ¡esto empieza a +parecerse a un sitio real! + +00:10:19.806 --> 00:10:22.686 align:middle +Excepto que estos enlaces en el navegador... + +00:10:22.866 --> 00:10:23.926 align:middle +¡no van a ninguna parte! + +00:10:24.486 --> 00:10:28.856 align:middle +Vamos a arreglar eso +aprendiendo a generar URLs. + +00:10:29.646 --> 00:10:35.066 align:middle +También vamos a conocer la mega-poderosa +herramienta de línea de comandos bin/console diff --git a/sfcasts/ep1/es/directories.md b/sfcasts/ep1/es/directories.md new file mode 100644 index 0000000..35d2aa6 --- /dev/null +++ b/sfcasts/ep1/es/directories.md @@ -0,0 +1,43 @@ +# Conoce nuestra Diminuta App + +Vamos a conocer nuestro nuevo proyecto porque mi objetivo final es que entiendas realmente cómo funcionan las cosas. Como he mencionado, no hay mucho aquí todavía... unos 15 archivos. Y realmente sólo hay tres directorios en los que tengamos que pensar o preocuparnos. + +## El directorio public/ + +El primero es `public/`... y esto es sencillo: es la raíz del documento. En otras palabras, si necesitas que un archivo sea accesible públicamente -como un archivo de imagen o un archivo CSS- tiene que vivir dentro de `public/`. + +Ahora mismo, esto contiene exactamente un archivo: `index.php`, que se llama "controlador frontal" + +[[[ code('f3303a0b1f') ]]] + +Ooo. Es una palabra elegante que significa que, independientemente de la URL a la que vaya el usuario, éste es el script que siempre se ejecuta primero. Su trabajo es arrancar Symfony y ejecutar nuestra aplicación. Y ahora que lo hemos visto, probablemente no tengamos que pensar ni abrirlo nunca más. + +## config/ & src/ + +Y, realmente, aparte de poner archivos CSS o de imagen en `public/`, este no es un directorio con el que vayas a tratar en el día a día. Lo que significa... Que en realidad sólo hay dos directorios en los que tenemos que pensar: `config/` +y `src/`. + +El directorio `config/` contiene... ¡gatos! Ya me gustaría. No, contiene archivos de configuración. Y `src/` contiene el 100% de tus clases PHP. Pasaremos el 95% de nuestro tiempo dentro del directorio`src/`. + +## composer.json & vendor/ + +Bien... ¿dónde está "Symfony"? Nuestro proyecto comenzó con un archivo `composer.json`. En él se enumeran todas las librerías de terceros que necesita nuestra aplicación. El comando "symfony new" que ejecutamos en secreto utilizó "composer" -es decir, el gestor de paquetes de PHP- para instalar estas librerías... que en realidad es sólo una forma de decir que Composer descargó estas librerías en el directorio `vendor/`. + +El propio Symfony es en realidad una colección de un montón de pequeñas bibliotecas que resuelven cada una un problema específico. En el directorio `vendor/symfony/`, parece que ya tenemos unas 25 de ellas. Técnicamente, nuestra aplicación sólo requiere estos seis paquetes, pero algunos de ellos requieren otros paquetes... y Composer es lo suficientemente inteligente como para descargar todo lo que necesitamos. + +De todos modos, "Symfony", o en realidad, un conjunto de bibliotecas de Symfony, vive en el directorio `vendor/`y nuestra nueva aplicación aprovecha ese código para hacer su trabajo. Más adelante hablaremos de Composer y de la instalación de paquetes de terceros. Pero en su mayor parte, `vendor/` es otro directorio del que... ¡no tenemos que preocuparnos! +# bin/ y var/ + +Entonces, ¿qué queda? Bueno, `bin/` contiene exactamente un archivo... y siempre contendrá sólo este archivo. Hablaremos de lo que hace `bin/console` un poco más tarde. Y el directorio `var/`contiene archivos de caché y de registro. Esos archivos son importantes... pero nunca necesitaremos mirar o pensar en esas cosas. + +Sí, vamos a vivir casi exclusivamente dentro de los directorios `config/` y `src/`. + +## Configuración de PhpStorm + +Bien, una última tarea antes de empezar a codificar. Siéntete libre de utilizar el editor de código que quieras: PhpStorm, VS Code, code carrier pigeon, lo que sea. Pero recomiendo encarecidamente PhpStorm. Hace que desarrollar con Symfony sea un sueño... ¡y ni siquiera me pagan por decir eso! Aunque, si quieren empezar a pagarme, acepto el pago en stroopwafels. + +Parte de lo que hace que PhpStorm sea tan bueno es un plugin diseñado específicamente para Symfony. Voy a mis preferencias de PhpStorm y, dentro, busco Plugins, Marketplace y luego busco Symfony. Aquí está. ¡Este plugin es increíble.... lo que puedes ver porque ha sido descargado 5,4 millones de veces! Añade toneladas de locas funciones de autocompletado que son específicas de Symfony. + +Si aún no lo tienes, instálalo. Una vez instalado, vuelve a Configuración y busca aquí arriba "Symfony" para encontrar una nueva área de Symfony. El único truco de este plugin es que tienes que activarlo para cada proyecto. Así que haz clic en esa casilla. Además, no es demasiado importante, pero cambia el directorio web a `public/`. + +Pulsa Ok y... ¡estamos listos! Vamos a dar vida a nuestra aplicación creando nuestra primera página a continuación. \ No newline at end of file diff --git a/sfcasts/ep1/es/directories.vtt b/sfcasts/ep1/es/directories.vtt new file mode 100644 index 0000000..2be9deb --- /dev/null +++ b/sfcasts/ep1/es/directories.vtt @@ -0,0 +1,268 @@ +WEBVTT + +00:00:01.146 --> 00:00:06.496 align:middle +Vamos a conocer nuestro nuevo proyecto +porque mi objetivo final es que + +00:00:06.496 --> 00:00:09.076 align:middle +entiendas realmente cómo funcionan las cosas. + +00:00:09.786 --> 00:00:13.136 align:middle +Como he mencionado, todavía +no hay mucho aquí... + +00:00:13.216 --> 00:00:14.906 align:middle +unos 15 archivos. + +00:00:15.116 --> 00:00:20.756 align:middle +Y realmente sólo hay tres directorios en +los que tenemos que pensar o preocuparnos. + +00:00:20.756 --> 00:00:23.396 align:middle +El primero es public/... + +00:00:23.546 --> 00:00:27.186 align:middle +y es sencillo: es la raíz del documento. En + +00:00:27.826 --> 00:00:33.146 align:middle +otras palabras, si necesitas que un archivo sea +accesible públicamente -como un archivo de imagen + +00:00:33.146 --> 00:00:37.016 align:middle +o un archivo CSS- tiene que +vivir dentro de public/. + +00:00:37.936 --> 00:00:42.026 align:middle +Ahora mismo, esto contiene +exactamente un archivo: index.php, + +00:00:42.576 --> 00:00:45.016 align:middle +que se llama "controlador frontal". + +00:00:45.186 --> 00:00:51.726 align:middle +Ooo. Es una palabra elegante que significa que, +independientemente de la URL a la que vaya el usuario, + +00:00:52.106 --> 00:00:55.536 align:middle +éste es el script que +siempre se ejecuta primero. + +00:00:56.216 --> 00:00:59.736 align:middle +Su trabajo es arrancar Symfony +y ejecutar nuestra aplicación. + +00:01:00.216 --> 00:01:06.536 align:middle +Y ahora que lo hemos visto, probablemente no +tengamos que pensar ni abrirlo nunca más. + +00:01:07.096 --> 00:01:11.516 align:middle +Y, realmente, aparte de poner +archivos CSS o de imagen en public/, + +00:01:11.746 --> 00:01:16.326 align:middle +no es un directorio con el que +vayas a tratar en el día a día. + +00:01:16.516 --> 00:01:17.216 align:middle +Lo que significa... + +00:01:17.416 --> 00:01:18.956 align:middle +¡que he mentido un poco! + +00:01:19.476 --> 00:01:25.156 align:middle +En realidad, sólo hay dos directorios en +los que debemos pensar: config/ y src/. + +00:01:25.236 --> 00:01:27.926 align:middle +El directorio config/ contiene... + +00:01:28.156 --> 00:01:30.296 align:middle +¡gatos! Ya me gustaría. + +00:01:30.706 --> 00:01:32.696 align:middle +No, contiene archivos de configuración. + +00:01:32.926 --> 00:01:37.706 align:middle +Y src/ contiene el 100% de tus clases PHP. + +00:01:38.136 --> 00:01:42.336 align:middle +Pasaremos el 95% de nuestro +tiempo dentro del directorio src/. + +00:01:43.206 --> 00:01:45.876 align:middle +Bien... ¿dónde está "Symfony"? + +00:01:45.876 --> 00:01:49.806 align:middle +Nuestro proyecto comenzó +con un archivo composer.json. + +00:01:50.406 --> 00:01:54.956 align:middle +En él se enumeran todas las librerías de +terceros que necesita nuestra aplicación. + +00:01:55.546 --> 00:02:00.056 align:middle +El comando "symfony new" que ejecutamos +en secreto utilizó "composer" - que es + +00:02:00.366 --> 00:02:04.426 align:middle +el gestor de paquetes de PHP - +para instalar estas bibli otecas... + +00:02:05.106 --> 00:02:07.606 align:middle +que en realidad es sólo una forma de decir + +00:02:07.606 --> 00:02:12.256 align:middle +que Composer descargó estas +bibliotecas en el directorio vendor/. + +00:02:12.366 --> 00:02:18.416 align:middle +El propio Symfony es en realidad una colección +de un montón de pequeñas bibliotecas + +00:02:18.576 --> 00:02:20.636 align:middle +que resuelven cada una un problema específico. + +00:02:21.746 --> 00:02:27.766 align:middle +En el directorio vendor/symfony/, +parece que ya tenemos unas 25 de ellas. + +00:02:28.376 --> 00:02:32.536 align:middle +Técnicamente, nuestra aplicación +sólo requiere estos seis paquetes, + +00:02:32.706 --> 00:02:36.286 align:middle +pero algunos de ellos +requieren otros paquetes... + +00:02:36.456 --> 00:02:40.066 align:middle +y Composer es lo suficientemente inteligente +como para descargar todo lo que necesitamos. De + +00:02:41.156 --> 00:02:48.326 align:middle +todos modos, "Symfony", o en realidad, un conjunto de +bibliotecas de Symfony, vive en el directorio vendor/ + +00:02:48.576 --> 00:02:52.556 align:middle +y nuestra nueva aplicación aprovecha +ese código para hacer su trabajo. + +00:02:52.556 --> 00:02:58.016 align:middle +Más adelante hablaremos de Composer y de +la instalación de paquetes de terceros. + +00:02:58.546 --> 00:03:03.026 align:middle +Pero en su mayor parte, vendor/ +es otro directorio del que... + +00:03:03.366 --> 00:03:05.266 align:middle +no tenemos que preocuparnos + +00:03:06.016 --> 00:03:07.566 align:middle +Entonces, ¿qué queda? + +00:03:08.216 --> 00:03:11.156 align:middle +Bueno, bin/ contiene exactamente un ar chivo... + +00:03:11.646 --> 00:03:14.156 align:middle +y siempre contendrá sólo este archivo. + +00:03:14.946 --> 00:03:17.716 align:middle +Hablaremos de lo que hace +bin/console un poco más tarde. + +00:03:18.186 --> 00:03:21.746 align:middle +Y el directorio var/ contiene +archivos de caché y de registro. + +00:03:22.306 --> 00:03:24.096 align:middle +Esos archivos son importantes... + +00:03:24.286 --> 00:03:28.876 align:middle +pero nunca necesitaremos +mirar o pensar en esas cosas. + +00:03:28.916 --> 00:03:35.556 align:middle +Sí, vamos a vivir casi exclusivamente +dentro de los directorios config/ y src/. + +00:03:36.536 --> 00:03:39.946 align:middle +Bien, una última tarea +antes de empezar a codificar. + +00:03:40.066 --> 00:03:45.076 align:middle +Siéntete libre de utilizar el editor +de código que quieras: PhpStorm, + +00:03:45.226 --> 00:03:48.786 align:middle +VS Code, code carrier pigeon, lo que sea. + +00:03:49.046 --> 00:03:51.586 align:middle +Pero recomiendo encarecidamente PhpStorm. + +00:03:52.226 --> 00:03:54.726 align:middle +Hace que desarrollar con +Symfony sea un sueño... + +00:03:54.986 --> 00:03:57.646 align:middle +¡y ni siquiera me pagan por decir eso! + +00:03:57.816 --> 00:04:02.436 align:middle +Aunque, si quieren empezar a pagarme, +acepto el pago en stroopwafels. + +00:04:03.206 --> 00:04:09.006 align:middle +Parte de lo que hace que PhpStorm sea tan bueno es un +plugin diseñado específicamente para Symfony. Voy a + +00:04:09.886 --> 00:04:15.556 align:middle +mis preferencias de PhpStorm +y, dentro, busco Plugins, + +00:04:16.286 --> 00:04:18.886 align:middle +Marketplace y luego busco Symfony. + +00:04:18.886 --> 00:04:22.066 align:middle +Aquí está. + +00:04:22.696 --> 00:04:24.676 align:middle +Este plugin es increíble.... + +00:04:24.876 --> 00:04:30.416 align:middle +¡que puedes ver porque ha sido +descargado 5,4 millones de veces! + +00:04:30.786 --> 00:04:35.656 align:middle +Añade toneladas de locas funciones de +autocompletado que son específicas de Symfony. + +00:04:35.656 --> 00:04:39.096 align:middle +Si aún no lo tienes, instálalo. + +00:04:39.836 --> 00:04:44.406 align:middle +Una vez instalado, vuelve a +Configuración y busca aquí arriba + +00:04:44.406 --> 00:04:47.426 align:middle +"Symfony" para encontrar +una nueva área de Symfony. + +00:04:48.576 --> 00:04:53.386 align:middle +El único truco de este plugin es que +tienes que activarlo para cada proyecto. + +00:04:54.076 --> 00:04:55.916 align:middle +Así que haz clic en esa casilla. + +00:04:56.786 --> 00:05:01.276 align:middle +Además, no es demasiado importante, +pero cambia el directorio web a public/. + +00:05:03.736 --> 00:05:05.176 align:middle +Pulsa Ok y... + +00:05:05.426 --> 00:05:07.036 align:middle +¡estamos listos! + +00:05:07.606 --> 00:05:12.746 align:middle +Vamos a dar vida a nuestra aplicación creando +nuestra primera página a continuación diff --git a/sfcasts/ep1/es/flex-recipes.md b/sfcasts/ep1/es/flex-recipes.md new file mode 100644 index 0000000..003e383 --- /dev/null +++ b/sfcasts/ep1/es/flex-recipes.md @@ -0,0 +1,115 @@ +# Recetas Flex + +Acabamos de instalar un nuevo paquete ejecutando `composer require templates`. Normalmente, al hacerlo, Composer actualizará los archivos `composer.json` y `composer.lock`, pero nada más. + +Pero cuando ejecutamos + +```terminal +git status +``` + +Hay otros cambios. Esto es gracias al sistema de recetas de Flex. Cada vez que instalamos un nuevo paquete, Flex comprueba en un repositorio central si ese paquete tiene una receta. Y si la tiene, la instala. + +## ¿Dónde viven las recetas? + +¿Dónde viven estas recetas? En la nube... o más concretamente en GitHub. Compruébalo. Ejecutar: + +```terminal +composer recipes +``` + +Este es un comando añadido a Composer por Flex. Enumera todas las recetas que se han instalado. Y si quieres más información sobre una, ejecútala: + +```terminal +composer recipes symfony/twig-bundle +``` + +Esta es una de las recetas que se acaba de ejecutar. Y... ¡guay! ¡Nos muestra un par de cosas bonitas! La primera es un árbol de los archivos que ha añadido a nuestro proyecto. La segunda es una URL de la receta que se instaló. Haré clic para abrirla. + +¡Sí! Las recetas de Symfony viven en un repositorio especial llamado `symfony/recipes`. Se trata de un gran directorio organizado por nombre de paquete. Hay un directorio `symfony` que contiene las recetas de todos los paquetes que empiezan por `symfony/`. El que acabamos de ver... está aquí abajo: `twig-bundle`. Y luego hay diferentes versiones de la receta en función de tu versión del paquete. Nosotros estamos utilizando la última versión 5.4. + +Cada receta tiene un archivo `manifest.json`, que controla lo que hace. El sistema de recetas sólo puede realizar un conjunto específico de operaciones, como añadir nuevos archivos a tu proyecto y modificar algunos archivos concretos. Por ejemplo, esta sección `bundles`le dice a flex que añada esta línea a nuestro archivo `config/bundles.php`. + +Si volvemos a ejecutar `git status`... ¡sí! Ese archivo ha sido modificado. Si lo difundimos: + +```terminal-silent +git diff config/bundles.php +``` + +Ha añadido dos líneas, probablemente una para cada una de las dos recetas. + +## ¿Bolsos Symfony? + +Por cierto, `config/bundles.php` no es un archivo en el que tengas que pensar mucho. Un bundle, en la tierra de Symfony, es básicamente un plugin. Así que si instalas un nuevo bundle en tu aplicación, eso te da nuevas características de Symfony. Para activar ese bundle, su nombre tiene que estar en este archivo. + +Así que lo primero que hizo la receta para Twig-bundle, gracias a esta línea de aquí arriba, fue activarse dentro de `bundles.php`... para que no tuviéramos que hacerlo manualmente. Las recetas son como una instalación automática. + +## Archivos nuevos y copiados + +La segunda sección del manifiesto se llama `copy-from-recipe`. Es sencillo: dice que hay que copiar los directorios `config/` y `templates/` de la receta en el proyecto. Si nos fijamos... la receta contiene un archivo `config/packages/twig.yaml`... y también un archivo `templates/base.html.twig`. + +De vuelta al terminal, ejecuta de nuevo `git status`. Vemos estos dos archivos en la parte inferior:`config/packages/twig.yaml`... y dentro de `templates/`, `base.html.twig`. + +Esto me encanta. Piénsalo: si instalas una herramienta de plantillas en tu aplicación, vamos a necesitar alguna configuración en algún lugar que le diga a esa herramienta de plantillas en qué directorio debe buscar nuestras plantillas. Ve a ver ese archivo`config/packages/twig.yaml`. Hablaremos más de estos archivos Yaml en el próximo tutorial. Pero a alto nivel, este archivo controla cómo se comporta Twig, el motor de plantillas de Symfony. Y fíjate en la clave `default_path` establecida en `%kernel.project_dir%/templates`. No te preocupes por esta sintaxis porcentual: es una forma elegante de referirse a la raíz de nuestro proyecto. + +La cuestión es que esta configuración dice + +> ¡Hey Twig! Cuando busques plantillas, búscalas en el directorio `templates/`. + +Y la receta incluso ha creado ese directorio con un archivo de diseño dentro. Lo usaremos en unos minutos. + +## symfony.lock y el compromiso de los archivos + +El último archivo no explicado que se ha modificado es `symfony.lock`. Esto no es importante: sólo mantiene un registro de las recetas que se han instalado... y deberías confirmarlo. + +De hecho, deberíamos confirmar todo esto. La receta puede darnos archivos, pero luego son nuestros para modificarlos. Ejecuta: + +```terminal +git add . +``` + +Entonces: + +```terminal +git status +``` + +Genial. ¡Vamos a confirmarlo! + +```terminal +git commit -m "Adding Twig and its beautiful recipe" +``` + +## Actualizar las recetas + +¡Ya está! Por cierto, es posible que dentro de unos meses haya cambios en algunas de las recetas que has instalado. Y si los hay, cuando ejecutes + +```terminal +composer recipes +``` + +verás un pequeño "actualización disponible" junto a ellas. Ejecuta `composer recipes:update`para actualizar a la última versión. + +Ah, y antes de que se me olvide, además de `symfony/recipes`, también hay un repositorio`symfony/recipes-contrib`. Así que las recetas pueden vivir en cualquiera de estos dos lugares. Las recetas de `symfony/recipes` están aprobadas por el equipo central de Symfony, por lo que su calidad está un poco más controlada. Aparte de eso, no hay ninguna diferencia. + +## Nuestro proyecto comenzó como un archivo + +Ahora, el sistema de recetas es tan potente que cada uno de los archivos de nuestro proyecto se añadió mediante una receta Puedo demostrarlo. Ve a https://github.com/symfony/skeleton. + +Cuando ejecutamos originalmente ese comando `symfony new` para iniciar nuestro proyecto, lo que realmente hizo fue clonar este repositorio... y luego ejecutó `composer install` dentro de él, que descarga todo en el directorio `vendor/`. + +Sí Nuestro proyecto -el que vemos aquí- era originalmente un único archivo: `composer.json`. Pero luego, cuando se instalaron los paquetes, las recetas de esos paquetes añadieron todo lo que vemos. Ejecuta: + +```terminal +composer recipes +``` + +de nuevo. Una de las recetas es para algo llamado `symfony/console`. Comprueba sus detalles: + +```terminal +composer recipes symfony/console +``` + +Y... ¡sí! ¡La receta de `symfony/console` añadió el archivo `bin/console`! La receta de `symfony/framework-bundle` -uno de los otros paquetes que se instaló originalmente- añadió casi todo lo demás, incluido el archivo `public/index.php`. ¿No es genial? + +Bien, a continuación: ¡hemos instalado Twig! ¡Así que volvamos al trabajo y utilicémoslo para renderizar algunas plantillas! Te va a encantar Twig. diff --git a/sfcasts/ep1/es/flex-recipes.vtt b/sfcasts/ep1/es/flex-recipes.vtt new file mode 100644 index 0000000..b90c31f --- /dev/null +++ b/sfcasts/ep1/es/flex-recipes.vtt @@ -0,0 +1,391 @@ +WEBVTT + +00:00:01.086 --> 00:00:05.066 align:middle +Acabamos de instalar un nuevo paquete +ejecutando composer require templates. + +00:00:05.336 --> 00:00:10.746 align:middle +Normalmente, al hacerlo, Compos er +actualizará los archivos composer.json + +00:00:10.806 --> 00:00:14.046 align:middle +y composer.lock, pero nada más. + +00:00:14.276 --> 00:00:19.436 align:middle +Pero cuando lo ejecutamos: +git status Hay otros cambios. + +00:00:19.816 --> 00:00:22.346 align:middle +Esto es gracias al sistema de +recetas de Flex. Cada vez que + +00:00:23.156 --> 00:00:27.956 align:middle +instalamos un nuevo paquete, Flex +comprueba en un repositorio central + +00:00:28.266 --> 00:00:30.966 align:middle +si ese paquete tiene una receta. + +00:00:31.386 --> 00:00:34.106 align:middle +Y si la tiene, la instala. + +00:00:34.816 --> 00:00:36.836 align:middle +¿Dónde viven estas recetas? + +00:00:37.646 --> 00:00:39.506 align:middle +En la nube... + +00:00:39.816 --> 00:00:41.756 align:middle +o más concretamente en GitHub. + +00:00:42.206 --> 00:00:42.836 align:middle +Compruébalo. + +00:00:43.076 --> 00:00:49.256 align:middle +Ejecuta: composer recipes Este es un +comando añadido a Composer por Flex. + +00:00:50.036 --> 00:00:53.036 align:middle +Enumera todas las recetas que se han instalado. + +00:00:53.036 --> 00:01:00.786 align:middle +Y si quieres más información sobre una, ejecútala: composer +recipes symfony/twig-bundle Esta es una de las recetas que se + +00:01:00.786 --> 00:01:03.066 align:middle +acaba de ejecutar. + +00:01:03.726 --> 00:01:04.896 align:middle +Y... ¡guay! + +00:01:05.616 --> 00:01:07.596 align:middle +¡Nos muestra un par de cosas bonitas! + +00:01:08.496 --> 00:01:12.436 align:middle +La primera es un árbol de los archivos +que ha añadido a nuestro proyecto. + +00:01:13.276 --> 00:01:16.996 align:middle +La segunda es una URL de la +receta que se ha instalado. + +00:01:17.266 --> 00:01:19.206 align:middle +Haré clic para abrirla. + +00:01:20.476 --> 00:01:26.366 align:middle +¡Sí! Las recetas de Symfony viven en un +repositorio especial llamado symfony/recipes. + +00:01:27.216 --> 00:01:31.106 align:middle +Se trata de un gran directorio +organizado por nombre de paquete. + +00:01:31.946 --> 00:01:36.936 align:middle +Hay un directorio symfony que contiene las recetas +de todos los paquetes que empiezan por symfony/. + +00:01:37.086 --> 00:01:40.246 align:middle +El que acabamos de ver... + +00:01:40.666 --> 00:01:43.726 align:middle +está aquí abajo: twig-bundle. + +00:01:44.126 --> 00:01:49.876 align:middle +Y luego hay diferentes versiones de la receta +en función de tu versión del paquete. + +00:01:50.276 --> 00:01:53.296 align:middle +Nosotros estamos utilizando +la última versión 5.4. + +00:01:54.416 --> 00:01:59.326 align:middle +Cada receta tiene un archivo +manifest.json, que controla lo que hace. + +00:02:00.186 --> 00:02:06.446 align:middle +El sistema de recetas sólo puede realizar un conjunto +específico de operaciones, como añadir nuevos archivos + +00:02:06.446 --> 00:02:10.596 align:middle +a tu proyecto y modificar +algunos archivos concretos. + +00:02:10.666 --> 00:02:19.036 align:middle +Por ejemplo, esta sección bundles le dice a flex que +añada esta línea a nuestro archivo config/bundles.php. + +00:02:20.676 --> 00:02:24.186 align:middle +Si ejecutamos de nuevo git status... + +00:02:24.316 --> 00:02:26.366 align:middle +¡sí! Ese archivo ha sido modificado. + +00:02:27.416 --> 00:02:35.616 align:middle +Si lo difundimos: Ha añadido dos líneas, +probablemente una para cada una de las dos recetas. + +00:02:36.446 --> 00:02:42.086 align:middle +Por cierto, config/bundles.php no es un +archivo en el que tengas que pensar mucho. + +00:02:42.556 --> 00:02:46.996 align:middle +Un bundle, en la tierra de +Symfony, es básicamente un plugin. + +00:02:46.996 --> 00:02:52.826 align:middle +Así que si instalas un nuevo bundle en tu aplicación, +eso te da nuevas características de Symfony. + +00:02:53.436 --> 00:02:58.116 align:middle +Para activar ese bundle, su nombre +tiene que estar en este archivo. + +00:02:58.236 --> 00:03:04.066 align:middle +Así que lo primero que hizo la receta para +Twig-bundle, gracias a esta línea de aquí arriba, + +00:03:04.396 --> 00:03:08.686 align:middle +fue activarse dentro de bundles.php... + +00:03:08.686 --> 00:03:11.456 align:middle +para que no tuviéramos +que hacerlo manualmente. + +00:03:12.216 --> 00:03:14.906 align:middle +Las recetas son como una +instalación automática. + +00:03:15.816 --> 00:03:20.126 align:middle +La segunda sección del manifiesto +se llama copy-from-recipe. + +00:03:20.586 --> 00:03:25.446 align:middle +Esto es sencillo: dice que hay que copiar +los directorios config/ y templates/ + +00:03:25.446 --> 00:03:27.906 align:middle +de la receta en el proyecto. + +00:03:28.776 --> 00:03:29.796 align:middle +Si nos fijamos... + +00:03:30.386 --> 00:03:34.456 align:middle +la receta contiene un archivo +config/packages/twig.yaml... + +00:03:35.026 --> 00:03:39.146 align:middle +y también un archivo templates/base.html.twig. + +00:03:40.106 --> 00:03:42.586 align:middle +De vuelta al terminal, +ejecuta de nuevo git status. + +00:03:43.646 --> 00:03:48.646 align:middle +Vemos estos dos archivos en la parte +inferior: config/packages/twig.yaml... + +00:03:49.136 --> 00:03:52.796 align:middle +y dentro de templates/, base.html.twig. + +00:03:53.506 --> 00:03:54.936 align:middle +Esto me encanta. + +00:03:55.256 --> 00:03:58.926 align:middle +Piénsalo: si instalas una herramienta +de plantillas en tu aplicación, vamos a + +00:03:59.286 --> 00:04:05.896 align:middle +necesitar alguna configuración en algún lugar que le +diga a esa herramienta de plantillas qué directorio + +00:04:05.896 --> 00:04:09.266 align:middle +debe buscar dentro para +encontrar nuestras plantillas. + +00:04:09.266 --> 00:04:13.866 align:middle +Ve a ver ese archivo config/packages/twig.yaml. + +00:04:14.806 --> 00:04:18.746 align:middle +Hablaremos más de estos archivos +Yaml en el próximo tutorial. + +00:04:18.916 --> 00:04:26.186 align:middle +Pero a alto nivel, este archivo controla cómo se +comporta Twig, el motor de plantillas de Symfony. + +00:04:26.846 --> 00:04:32.956 align:middle +Y fíjate en la clave default_path +establecida en %kernel.project_dir%/templates. + +00:04:33.536 --> 00:04:39.426 align:middle +No te preocupes por esta sintaxis porcentual: es una forma +elegante de referirse a la raíz de nuestro proyecto. + +00:04:40.306 --> 00:04:43.626 align:middle +La cuestión es que esta +configuración dice: ¡Hey Twig! + +00:04:44.006 --> 00:04:48.326 align:middle +Cuando busques plantillas, búscalas +en el directorio templates/. + +00:04:48.516 --> 00:04:53.726 align:middle +Y la receta incluso ha creado ese +directorio con un archivo de diseño dentro. + +00:04:54.446 --> 00:04:57.166 align:middle +Lo utilizaremos en unos minutos. + +00:04:57.236 --> 00:05:02.236 align:middle +El último archivo no explicado +que se modificó es symfony.lock. + +00:05:02.606 --> 00:05:07.826 align:middle +Esto no es importante: sólo lleva la cuenta +de las recetas que se han instalado... + +00:05:07.826 --> 00:05:09.186 align:middle +y deberías confirmarlo. + +00:05:09.846 --> 00:05:12.216 align:middle +De hecho, deberíamos confirmar todo esto. + +00:05:12.606 --> 00:05:18.576 align:middle +La receta puede darnos archivos, pero +luego son nuestros para modificarlos. + +00:05:18.576 --> 00:05:19.596 align:middle +Ejecuta: git add . + +00:05:20.216 --> 00:05:23.976 align:middle +Entonces: git status Genial. + +00:05:24.296 --> 00:05:25.296 align:middle +¡Vamos a confirmarlo! + +00:05:25.846 --> 00:05:31.526 align:middle +git commit -m "Adding Twig and its beautiful +recipe" ¡Ya está hecho! Por cierto, es + +00:05:32.286 --> 00:05:37.026 align:middle +posible que dentro de unos +meses haya cambios en algunas + +00:05:37.026 --> 00:05:38.766 align:middle +de las recetas que has instalado. + +00:05:39.106 --> 00:05:43.846 align:middle +Y si los hay, cuando ejecutes +verás un pequeño composer recipes + +00:05:44.066 --> 00:05:45.956 align:middle +"actualización disponible" junto a ellos. + +00:05:46.746 --> 00:05:51.456 align:middle +Ejecuta para actualizar a la última +versión. composer recipes:update + +00:05:52.436 --> 00:05:56.876 align:middle +Ah, y antes de que se me olvide, +además de , symfony/recipes + +00:05:57.176 --> 00:06:02.076 align:middle +también hay un repositorio +. symfony/recipes-contrib + +00:06:02.586 --> 00:06:05.876 align:middle +Así que las recetas pueden vivir en +cualquiera de estos dos lugares. Las recetas + +00:06:06.816 --> 00:06:11.536 align:middle +de están aprobadas por el equipo +central de Symfony, symfony/recipes + +00:06:11.796 --> 00:06:14.576 align:middle +por lo que su calidad está +un poco más controlada. + +00:06:15.086 --> 00:06:16.956 align:middle +Aparte de eso, no hay ninguna diferencia. + +00:06:17.976 --> 00:06:22.826 align:middle +Ahora bien, el sistema de recetas es +tan potente que todos los archivos + +00:06:22.826 --> 00:06:25.946 align:middle +de nuestro proyecto se +añadieron mediante una receta + +00:06:26.446 --> 00:06:27.116 align:middle +Puedo demostrarlo. + +00:06:27.116 --> 00:06:30.916 align:middle +Ve a https://github.com/symfony/skeleton. + +00:06:32.106 --> 00:06:36.386 align:middle +Cuando ejecutamos originalmente ese comando +para iniciar nuestro symfony new proyecto, + +00:06:36.936 --> 00:06:40.846 align:middle +lo que realmente hizo fue +clonar este repositorio... + +00:06:40.956 --> 00:06:43.606 align:middle +y luego ejecutar dentro +de él, composer install + +00:06:43.876 --> 00:06:46.756 align:middle +que descarga todo en el directorio . vendor/ + +00:06:47.356 --> 00:06:51.056 align:middle +Sí Nuestro proyecto -el que vemos aquí- + +00:06:51.366 --> 00:06:55.976 align:middle +era originalmente un único +archivo: . composer.json + +00:06:56.626 --> 00:06:59.196 align:middle +Pero luego, cuando se instalaron los paquetes, + +00:06:59.476 --> 00:07:04.326 align:middle +las recetas de esos paquetes +añadieron todo lo que vemos. + +00:07:05.536 --> 00:07:07.146 align:middle +Ejecuta: de nuevo. composer recipes + +00:07:08.596 --> 00:07:11.916 align:middle +Una de las recetas es para +algo llamado symfony/console. + +00:07:12.306 --> 00:07:18.776 align:middle +Comprueba sus detalles: Y... +composer recipes symfony/console + +00:07:18.976 --> 00:07:25.226 align:middle +¡sí! ¡La receta de añadió el archivo +! symfony/console bin/console La receta de + +00:07:26.116 --> 00:07:30.876 align:middle +symfony/framework-bundle +-uno de los otros paquetes + +00:07:30.876 --> 00:07:34.796 align:middle +que se instaló originalmente- +añadió casi todo lo demás, + +00:07:35.146 --> 00:07:38.936 align:middle +incluido el archivo . public/index.php + +00:07:39.306 --> 00:07:40.596 align:middle +¿No es genial? + +00:07:41.716 --> 00:07:44.356 align:middle +Bien, a continuación: ¡hemos instalado Twig! + +00:07:44.746 --> 00:07:48.026 align:middle +¡Así que volvamos al trabajo y utilicémoslo +para renderizar algunas plantillas! + +00:07:48.396 --> 00:07:50.126 align:middle +Te va a encantar Twig diff --git a/sfcasts/ep1/es/flex.md b/sfcasts/ep1/es/flex.md new file mode 100644 index 0000000..700400a --- /dev/null +++ b/sfcasts/ep1/es/flex.md @@ -0,0 +1,94 @@ +# Symfony Flex: Aliases, Paquetes y Recetas + +Symfony es un conjunto de librerías que nos proporciona toneladas de herramientas: herramientas para registrar, hacer consultas a la base de datos, enviar correos electrónicos, renderizar plantillas y hacer llamadas a la API, por nombrar algunas. Si las cuentas, como hice yo, Symfony consta de unas 100 bibliotecas distintas. ¡Vaya! + +Ahora quiero empezar a convertir nuestras páginas en verdaderas páginas HTML... en lugar de devolver sólo texto. Pero no vamos a meter un montón de HTML en nuestras clases de PHP, qué asco. En su lugar, vamos a renderizar una plantilla. + +## La filosofía de Symfony de empezar poco a poco e instalar funciones + +Pero, ¿adivina qué? ¡No hay ninguna biblioteca de plantillas en nuestro proyecto! ¿Qué? Pero yo creía que acababas de decir que Symfony tiene una herramienta para renderizar plantillas!? ¡Mentira! + +Bueno... Symfony sí tiene una herramienta para eso. Pero nuestra aplicación utiliza actualmente muy pocas de las bibliotecas de Symfony. Las herramientas que tenemos hasta ahora no suponen mucho más que un sistema de ruta-controlador-respuesta. Si necesitas renderizar una plantilla o hacer una consulta a la base de datos, no tenemos esas herramientas instaladas en nuestra app... todavía. + +De hecho, me encanta esto de Symfony. En lugar de empezar con un proyecto gigantesco, con todo lo que necesitamos, más toneladas de cosas que no necesitamos, Symfony empieza de forma diminuta. Luego, si necesitas algo, lo instalas + +Pero antes de instalar una biblioteca de plantillas, en tu terminal, ejecuta + +```terminal +git status +``` + +Vamos a confirmar todo: + +```terminal +git add . +``` + +Puedo ejecutar con seguridad `git add .` -que añade todo lo que hay en mi directorio a git- porque uno de los archivos con los que venía nuestro proyecto originalmente era un archivo `.gitignore`, que ya ignora cosas como el directorio `vendor/`, el directorio `var/` y varias otras rutas. Si te preguntas qué son estas cosas raras de los marcadores, está relacionado con el sistema de recetas, del que vamos a hablar. + +En cualquier caso, ejecuta `git commit` y añade un mensaje: + +```terminal-silent +git commit -m "route -> controller -> response -> profit" +``` + +¡Perfecto! Y ahora, estamos limpios. + +## Instalar una biblioteca de plantillas (Twig) + +Bien, ¿cómo podemos instalar una biblioteca de plantillas? ¿Y qué bibliotecas de plantillas están disponibles para Symfony? ¿Y cuál es la recomendada? Bueno, por supuesto, una buena manera de responder a estas preguntas sería consultar la documentación de Symfony. + +Pero también podemos simplemente... ¡adivinar! En cualquier proyecto PHP, puedes añadir nuevas bibliotecas de terceros a tu aplicación diciendo "composer require" y luego el nombre del paquete. Todavía no sabemos el nombre del paquete que necesitamos, así que simplemente lo adivinaremos: + +```terminal +composer require templates +``` + +Ahora bien, si has utilizado Composer antes, puede que ahora mismo estés gritando a tu pantalla ¿Por qué? Porque en Composer, los nombres de los paquetes son siempre `something/something`. No es posible, literalmente, tener un paquete llamado simplemente `templates`. + +Pero mira: cuando ejecutamos esto, ¡funciona! Y arriba dice que está usando la versión 1 para `symfony/twig-pack`. Twig es el nombre del motor de plantillas de Symfony. + +## Alias de Flex + +Para entender esto, vamos a dar un pequeño paso atrás. Nuestro proyecto comenzó con un archivo`composer.json` que contiene varias bibliotecas de Symfony. Una de ellas se llama`symfony/flex`. Flex es un plugin de Composer. En realidad, añade tres superpoderes a Composer. + +***TIP +El servidor flex.symfony.com se cerró a favor de un nuevo sistema. ¡Pero aún puede ver una lista de todas las recetas +disponibles en ¡ https://bit.ly/flex-recipes! +*** + +El primero, que acabamos de ver, se llama aliases de Flex. Dirígete a https://flex.symfony.com para ver una página gigante llena de paquetes. Busca "plantillas". Aquí está. En `symfony/twig-pack`, dice Aliases: template, templates, twig y twig-pack. + +La idea que hay detrás de los alias de Flex es muy sencilla. Escribimos`composer require templates`. Y luego, internamente, Flex lo cambia por`symfony/twig-pack`. En última instancia, ése es el paquete que Composer instala. + +Esto significa que, la mayoría de las veces, puedes simplemente "composer require" lo que quieras, como `composer require logger`, `composer require orm`, `composer require icecream`, lo que sea. Es sólo un sistema de acceso directo. Lo importante es que, lo que realmente se instaló fue `symfony/twig-pack`. + +## Paquetes Flex + +Y eso significa que, en nuestro archivo `composer.json`, deberíamos ver ahora`symfony/twig-pack` bajo la clave `require`. Pero si te das la vuelta, ¡no está ahí! ¡Gracias! En su lugar, ha añadido `symfony/twig-bundle`, `twig/extra-bundle`, y `twig/twig`. + +Estamos asistiendo al segundo superpoder de Symfony Flex: desempaquetar paquetes. Copiamos el nombre del paquete original y... podemos encontrar ese repositorio en GitHub entrando en https://github.com/symfony/twig-pack. + +Y... sólo contiene un archivo: `composer.json`. Y esto requiere otros tres paquetes: los tres que acabamos de ver añadidos a nuestro proyecto. + +Esto se llama paquete Symfony. Es... realmente un paquete falso que nos ayuda a instalar otros paquetes. Resulta que, si quieres añadir un motor de plantillas rico a tu aplicación, es recomendable instalar estos tres paquetes. Pero en lugar de hacer que los añadas manualmente, puedes hacer que Composer requiera `symfony/twig-pack` y los obtenga automáticamente. Cuando instalas un "paquete", como éste, Flex lo "desempaqueta" automáticamente: encuentra los tres paquetes de los que depende el paquete y los añade a tu archivo `composer.json`. + +Así pues, los paquetes son un atajo para que puedas ejecutar un comando de `composer require` y conseguir que se añadan varias bibliotecas a tu proyecto. + +Bien, ¿cuál es el tercer y último superpoder de Flex? Me alegro de que lo preguntes Para averiguarlo, en tu terminal, ejecuta + +```terminal +git status +``` + +## Recetas de Flex + +Vaya. Normalmente, cuando ejecutas `composer require`, los únicos archivos que debería modificar -además de descargar paquetes en `vendor/` - son `composer.json` y`composer.lock`. El tercer superpoder de Flex es su sistema de recetas. + +Siempre que instales un paquete, ese paquete puede tener una receta. Si la tiene, además de descargar el paquete en el directorio `vendor/`, Flex también ejecutará su receta. Las recetas pueden hacer cosas como añadir nuevos archivos o incluso modificar algunos archivos existentes. + +Observa: si nos desplazamos un poco hacia arriba, ah sí: dice "configurando 2 recetas". Así que aparentemente había una receta para `symfony/twig-bundle` y también una receta para`twig/extra-bundle`. Y estas recetas aparentemente actualizaron el archivo `config/bundles.php`y añadieron un nuevo directorio y archivo. + +El sistema de recetas es genial. Todo lo que tenemos que hacer es que Composer requiera una nueva biblioteca y su receta añadirá todos los archivos de configuración u otra configuración necesaria para que podamos empezar a usar esa biblioteca inmediatamente Se acabó el seguir 5 pasos de "instalación" manual en un README. Cuando añades una biblioteca, funciona de forma inmediata. + +A continuación: Quiero profundizar un poco más en las recetas. Por ejemplo, ¿dónde viven? ¿Cuál es su color favorito? ¿Y qué ha añadido esta receta específicamente a nuestra aplicación y por qué? También voy a contarte un pequeño secreto: todos los archivos de nuestro proyecto -todos los archivos de `config/`, el directorio `public/`... todas estas cosas- se añadieron mediante una receta. Y lo demostraré. diff --git a/sfcasts/ep1/es/flex.vtt b/sfcasts/ep1/es/flex.vtt new file mode 100644 index 0000000..940b146 --- /dev/null +++ b/sfcasts/ep1/es/flex.vtt @@ -0,0 +1,426 @@ +WEBVTT + +00:00:01.066 --> 00:00:07.326 align:middle +Symfony es un conjunto de librerías que nos proporciona +toneladas de herramientas: herramientas para registrar, + +00:00:07.586 --> 00:00:11.336 align:middle +hacer consultas a la base de datos, enviar +correos electrónicos, renderizar plantillas + +00:00:11.336 --> 00:00:14.506 align:middle +y hacer llamadas a la API, +sólo por nombrar algunas. + +00:00:15.256 --> 00:00:22.436 align:middle +Si las cuentas, como hice yo, Symfony +consta de unas 100 bibliotecas distintas. + +00:00:22.706 --> 00:00:28.676 align:middle +¡Vaya! Ahora mismo, quiero empezar a convertir +nuestras páginas en verdaderas páginas HTML... + +00:00:28.906 --> 00:00:30.786 align:middle +en lugar de devolver sólo texto. + +00:00:31.176 --> 00:00:36.216 align:middle +Pero no vamos a meter un montón +de HTML en nuestras clases de PHP. + +00:00:36.526 --> 00:00:40.216 align:middle +Qué asco. En su lugar, vamos +a renderizar una plantilla. + +00:00:40.736 --> 00:00:41.396 align:middle +Pero, ¿adivina qué? + +00:00:41.676 --> 00:00:45.106 align:middle +¡No hay ninguna biblioteca de +plantillas en nuestro proyecto! + +00:00:45.806 --> 00:00:50.466 align:middle +¿Qué? Pero yo creía que acababas de decir que Symfony +tiene una herramienta para renderizar plantillas!? + +00:00:50.706 --> 00:00:52.876 align:middle +¡Mentira! Bueno... + +00:00:52.876 --> 00:00:54.836 align:middle +Symfony sí tiene una herramienta para eso. + +00:00:55.256 --> 00:01:00.046 align:middle +Pero nuestra aplicación utiliza actualmente +muy pocas de las bibliotecas de Symfony. + +00:01:00.786 --> 00:01:07.076 align:middle +Las herramientas que tenemos hasta ahora no suponen +mucho más que un sistema de ruta-controlador-respuesta. + +00:01:07.976 --> 00:01:11.106 align:middle +Si necesitas renderizar una plantilla o +hacer una consulta a la base de datos, + +00:01:11.326 --> 00:01:14.906 align:middle +no tenemos esas herramientas +instaladas en nuestra app... + +00:01:15.306 --> 00:01:18.566 align:middle +todavía. De hecho, me encanta esto de Symfony. + +00:01:19.206 --> 00:01:26.306 align:middle +En lugar de empezar con un proyecto gigantesco, con +todo lo que necesitamos, más toneladas de cosas + +00:01:26.306 --> 00:01:29.366 align:middle +que no necesitamos, Symfony +empieza de forma diminuta. + +00:01:29.766 --> 00:01:33.026 align:middle +Luego, si necesitas algo, lo instalas + +00:01:33.876 --> 00:01:37.616 align:middle +Pero antes de instalar una biblioteca +de plantillas, en tu terminal, ejecuta + +00:01:37.616 --> 00:01:41.356 align:middle +git status Vamos a confirmar todo: git add . + +00:01:41.356 --> 00:01:43.016 align:middle +Puedo ejecutar con seguridad git add . + +00:01:43.586 --> 00:01:47.416 align:middle +-que añade todo lo que hay en mi +directorio a git- porque uno de los archivos + +00:01:47.416 --> 00:01:53.756 align:middle +con los que venía nuestro proyecto originalmente +era un archivo .gitignore, que ya ignora cosas + +00:01:53.756 --> 00:01:57.906 align:middle +como el directorio vendor/, el +directorio var/ y varias otras rutas. + +00:01:58.966 --> 00:02:02.196 align:middle +Si te preguntas qué son estas +cosas raras de los marcadores, está + +00:02:02.376 --> 00:02:06.826 align:middle +relacionado con el sistema de +recetas, del que vamos a hablar. + +00:02:07.926 --> 00:02:13.046 align:middle +En cualquier caso, ejecuta git commit +y añade un mensaje: ¡Perfecto! + +00:02:13.266 --> 00:02:15.526 align:middle +Y ahora, estamos limpios. + +00:02:16.546 --> 00:02:20.786 align:middle +Bien, ¿cómo podemos instalar +una biblioteca de plantillas? + +00:02:21.126 --> 00:02:24.976 align:middle +¿Y qué bibliotecas de plantillas +están disponibles para Symfony? + +00:02:25.236 --> 00:02:26.916 align:middle +¿Y cuál es la recomendada? + +00:02:27.476 --> 00:02:32.156 align:middle +Bueno, por supuesto, una buena manera de +responder a estas preguntas sería consultar la + +00:02:32.156 --> 00:02:33.656 align:middle +documentación de Symfony. + +00:02:34.176 --> 00:02:35.716 align:middle +Pero también podemos simplemente... + +00:02:35.886 --> 00:02:42.546 align:middle +¡adivinar! En cualquier proyecto PHP, puedes añadir +nuevas bibliotecas de terceros a tu aplicación diciendo + +00:02:42.546 --> 00:02:45.706 align:middle +"composer require" y luego +el nombre del paquete. + +00:02:46.716 --> 00:02:54.726 align:middle +Todavía no sabemos el nombre del paquete que necesitamos, +así que lo adivinaré: composer require templates Ahora bien, + +00:02:54.726 --> 00:02:59.916 align:middle +si ya has utilizado Compos er , puede que +ahora mismo estés gritando a tu pantalla + +00:03:00.446 --> 00:03:06.946 align:middle +¿Por qué? Porque en Composer, los nombres de +los paquetes son siempre something/something. + +00:03:06.946 --> 00:03:12.356 align:middle +No es posible, literalmente, tener un +paquete que sólo se llame templates. + +00:03:12.696 --> 00:03:17.236 align:middle +Pero mira: cuando ejecutamos esto, ¡funciona! + +00:03:17.586 --> 00:03:23.436 align:middle +Y arriba dice que está usando la +versión 1 de symfony/twig-pack. + +00:03:24.146 --> 00:03:26.996 align:middle +Twig es el nombre del motor +de plantillas de Symfony. + +00:03:27.956 --> 00:03:31.336 align:middle +Para entender esto, vamos a +dar un pequeño paso atrás. + +00:03:31.856 --> 00:03:37.776 align:middle +Nuestro proyecto comenzó con un archivo composer.json +que contiene varias bibliotecas de Symfony. + +00:03:38.486 --> 00:03:41.766 align:middle +Una de ellas se llama symfony/flex. + +00:03:42.336 --> 00:03:44.766 align:middle +Flex es un plugin de Composer. + +00:03:45.076 --> 00:03:48.056 align:middle +Así que añade más funciones a Composer. + +00:03:48.746 --> 00:03:52.076 align:middle +En realidad, añade tres +superpoderes a Composer. + +00:03:52.746 --> 00:03:56.996 align:middle +El primero, que acabamos de +ver, se llama aliases de Flex. + +00:03:56.996 --> 00:04:02.966 align:middle +Dirígete a https://flex.symfony.com para +ver una página gigante llena de paquetes. + +00:04:04.826 --> 00:04:06.206 align:middle +Busca "plantillas". + +00:04:07.546 --> 00:04:08.126 align:middle +Aquí está. + +00:04:08.276 --> 00:04:15.116 align:middle +En symfony/twig-pack, dice Aliases: +template, templates, twig y twig-pack. + +00:04:15.956 --> 00:04:19.696 align:middle +La idea que hay detrás de los +alias de Flex es muy sencilla. + +00:04:20.106 --> 00:04:22.696 align:middle +Escribimos composer require templates. + +00:04:23.086 --> 00:04:29.716 align:middle +Y luego, internamente, Flex lo +cambia por symfony/twig-pack. + +00:04:30.556 --> 00:04:34.386 align:middle +En última instancia, ése es +el paquete que Composer instala. + +00:04:35.076 --> 00:04:40.526 align:middle +Esto significa que, la mayoría de las veces, puedes +simplemente "composer require" lo que quieras, + +00:04:40.886 --> 00:04:48.036 align:middle +como composer require logger, composer require +orm, composer require icecream, lo que sea. + +00:04:48.316 --> 00:04:50.246 align:middle +Es sólo un sistema de acceso directo. + +00:04:50.936 --> 00:04:57.556 align:middle +Lo importante es que, lo que realmente +se instaló fue symfony/twig-pack. + +00:04:57.976 --> 00:05:01.666 align:middle +Y eso significa que, en +nuestro archivo composer.json, + +00:05:01.976 --> 00:05:07.196 align:middle +deberíamos ver ahora symfony/twig-pack +bajo la clave require. + +00:05:08.036 --> 00:05:10.886 align:middle +Pero si te das la vuelta, ¡no está ahí! + +00:05:11.236 --> 00:05:19.316 align:middle +¡No hay nada que hacer! En su lugar, ha añadido +symfony/twig-bundle, twig/extra-bundle, y twig/twig. + +00:05:20.196 --> 00:05:26.166 align:middle +Estamos asistiendo al segundo superpoder +de Symfony Flex: desempaquetar paquetes. + +00:05:27.106 --> 00:05:29.426 align:middle +Copiamos el nombre del paquete original y... + +00:05:29.976 --> 00:05:33.316 align:middle +podemos encontrar ese +repositorio en GitHub entrando + +00:05:33.316 --> 00:05:38.626 align:middle +en https://github.com/symfony/twig-pack. + +00:05:38.626 --> 00:05:43.966 align:middle +Y... sólo contiene un archivo: composer.json. + +00:05:45.236 --> 00:05:51.366 align:middle +Y esto requiere otros tres paquetes: los tres +que acabamos de ver añadidos a nuestro proyecto. + +00:05:52.196 --> 00:05:54.706 align:middle +Esto se llama paquete Symfony. + +00:05:55.186 --> 00:06:00.636 align:middle +Es... realmente un paquete falso que nos +ayuda a instalar otros paquetes. Resulta + +00:06:01.316 --> 00:06:05.906 align:middle +que, si quieres añadir un motor de +plantillas rico a tu aplicación, es + +00:06:06.256 --> 00:06:09.426 align:middle +recomendable instalar estos tres paquetes. + +00:06:09.706 --> 00:06:12.286 align:middle +Pero en lugar de hacer que +los añadas manualmente, + +00:06:12.556 --> 00:06:17.356 align:middle +puedes hacer que Composer requiera +symfony/twig-pack y los obtenga automáticamente. + +00:06:18.236 --> 00:06:23.626 align:middle +Cuando instalas un "paquete", como éste, +Flex lo "desempaqueta" automáticamente: + +00:06:24.366 --> 00:06:28.176 align:middle +encuentra los tres paquetes +de los que depende el paquete + +00:06:28.316 --> 00:06:31.936 align:middle +y los añade a tu archivo composer.json. + +00:06:32.556 --> 00:06:37.966 align:middle +Así pues, los paquetes son un atajo para que +puedas ejecutar un comando composer require + +00:06:38.296 --> 00:06:41.656 align:middle +y conseguir que se añadan +varias bibliotecas a tu proyecto. + +00:06:42.656 --> 00:06:46.746 align:middle +Bien, ¿cuál es el tercer y +último superpoder de Flex? + +00:06:47.056 --> 00:06:48.286 align:middle +Me alegro de que lo preguntes + +00:06:49.026 --> 00:06:54.006 align:middle +Para averiguarlo, en tu terminal, +ejecuta git status Whoa. + +00:06:54.596 --> 00:07:00.096 align:middle +Normalmente, cuando ejecutas composer require, +los únicos archivos que debería modificar + +00:07:00.276 --> 00:07:07.126 align:middle +-además de descargar paquetes en vendor/ +- son composer.json y composer.lock. + +00:07:07.916 --> 00:07:11.836 align:middle +El tercer superpoder de Flex +es su sistema de recetas. + +00:07:12.506 --> 00:07:17.126 align:middle +Siempre que instales un paquete, +ese paquete puede tener una receta. + +00:07:17.596 --> 00:07:22.606 align:middle +Si la tiene, además de descargar +el paquete en el directorio vendor/, + +00:07:22.776 --> 00:07:25.656 align:middle +Flex también ejecutará su receta. + +00:07:26.346 --> 00:07:32.526 align:middle +Las recetas pueden hacer cosas como añadir nuevos +archivos o incluso modificar algunos archivos existentes. + +00:07:33.036 --> 00:07:39.786 align:middle +Observa: si nos desplazamos un poco hacia +arriba, ah sí: dice "configurando 2 recetas". + +00:07:40.276 --> 00:07:44.356 align:middle +Así que aparentemente había una +receta para symfony/twig-bundle + +00:07:44.576 --> 00:07:47.216 align:middle +y también una receta para twig/extra-bundle. + +00:07:47.516 --> 00:07:52.846 align:middle +Y estas recetas aparentemente +actualizaron el archivo config/bundles.php + +00:07:53.006 --> 00:07:55.196 align:middle +y añadieron un nuevo directorio y archivo. + +00:07:56.366 --> 00:07:59.046 align:middle +El sistema de recetas es muy bueno. + +00:07:59.436 --> 00:08:03.286 align:middle +Todo lo que tenemos que hacer es que +Composer requiera una nueva biblioteca + +00:08:03.896 --> 00:08:09.556 align:middle +y su receta añadirá todos los archivos de +configuración u otra configuración necesaria para + +00:08:09.696 --> 00:08:13.556 align:middle +que podamos empezar a usar +esa biblioteca inmediatamente + +00:08:14.006 --> 00:08:18.326 align:middle +Se acabó el seguir 5 pasos de +"instalación" manual en un README. + +00:08:19.036 --> 00:08:22.326 align:middle +Cuando añades una biblioteca, +funciona de forma inmediata. + +00:08:23.466 --> 00:08:26.676 align:middle +A continuación: Quiero profundizar +un poco más en las recetas. + +00:08:27.146 --> 00:08:28.606 align:middle +Por ejemplo, ¿dónde viven? + +00:08:29.026 --> 00:08:30.466 align:middle +¿Cuál es su color favorito? + +00:08:30.656 --> 00:08:34.836 align:middle +¿Y qué ha añadido esta receta específicamente +a nuestra aplicación y por qué? + +00:08:34.836 --> 00:08:40.416 align:middle +También voy a contarte un pequeño secreto: +todos los archivos de nuestro proyecto + +00:08:40.706 --> 00:08:43.616 align:middle +-todos los archivos de config/, +el directorio public/... + +00:08:43.996 --> 00:08:47.286 align:middle +todo este material - fue +añadido a través de una receta. + +00:08:47.776 --> 00:08:48.636 align:middle +Y lo demostraré diff --git a/sfcasts/ep1/es/generate-urls.md b/sfcasts/ep1/es/generate-urls.md new file mode 100644 index 0000000..f68932d --- /dev/null +++ b/sfcasts/ep1/es/generate-urls.md @@ -0,0 +1,102 @@ +# Generar Urls y bin/console + +Hay dos formas diferentes de interactuar con nuestra aplicación. La primera es a través del servidor web... ¡y eso es lo que hemos hecho! Llegamos a una URL y... entre bastidores, se ejecuta `public/index.php`, que arranca Symfony, llama al enrutamiento y ejecuta nuestro controlador. + +## Hola bin/console + +¿Cuál es la segunda forma de interactuar con nuestra aplicación? Todavía no la hemos visto: es a través de una herramienta de línea de comandos llamada `bin/console`. En tu terminal ejecuta: + +```terminal +php bin/console +``` + +... para ver un montón de comandos dentro de este script. Me encanta esta cosa. Está lleno de cosas que nos ayudan a depurar, con el tiempo tendrá comandos de generación de código, comandos para establecer secretos: todo tipo de cosas buenas que iremos descubriendo poco a poco. + +Pero quiero señalar que... ¡no hay nada especial en este script de `bin/console`! Es sólo un archivo: hay literalmente un directorio `bin/` con un archivo `console`dentro. Probablemente nunca necesitarás abrir este archivo ni pensar en él, pero es útil. Ah, y en la mayoría de los sistemas, puedes simplemente ejecutar: + +```terminal +./bin/console +``` + +... que se ve mejor. O a veces puedes ver que ejecute: + +```terminal +symfony console +``` + +... que no es más que otra forma de ejecutar este archivo. Hablaremos más de esto en un futuro tutorial. + +## bin/consola debug:router + +El primer comando que quiero comprobar dentro de `bin/console` es `debug:router`: + +```terminal-silent +php bin/console debug:router +``` + +Esto es impresionante. Nos muestra todas las rutas de nuestra aplicación, como nuestras dos rutas para `/` y `/browse/{slug}`. ¿Qué son estas otras rutas? Vienen de la barra de herramientas de depuración web y del sistema de perfilado... y sólo están aquí mientras desarrollamos localmente. + +Bien, de vuelta a nuestro sitio.... en la parte superior de la página, tenemos dos enlaces no funcionales a la página de inicio y a la página de navegación. Vamos a conectarlos. Abre `templates/ +base.html.twig`... y busca las etiquetas `a`. Ya está. + +Así que sería muy fácil hacer que esto funcionara con sólo `href="/"`. Pero en lugar de eso, cada vez que enlacemos una página en Symfony, vamos a pedir al sistema de enrutamiento que nos genere una URL. Diremos + +> Por favor, genera la URL de la ruta de la página de inicio, o de la ruta de la página de navegación. + +Así, si alguna vez cambiamos la URL de una ruta, todos nuestros enlaces se actualizarán instantáneamente. Magia. + +## Cómo nombrar tu ruta + +Empecemos por la página de inicio. ¿Cómo le pedimos a Symfony que genere una URL para esta ruta? Bueno, primero tenemos que dar un nombre a la ruta. ¡Sorpresa! Cada ruta tiene un nombre interno. Puedes verlo en `debug:router`. Nuestras rutas se llaman `app_vinyl_homepage` y `app_vinyl_browse`. Huh, esos son los nombres exactos de mis tortugas mascota cuando era niño. + +¿De dónde vienen estos nombres? Por defecto, Symfony nos genera automáticamente un nombre, lo cual está bien. El nombre no se utiliza en absoluto hasta que generamos una URL a la misma. Y en cuanto necesitemos generar una URL a una ruta, recomiendo encarecidamente tomar el control de este nombre... sólo para asegurarnos de que nunca cambia accidentalmente. + +Para ello, busca la ruta y añade un argumento: `name` ajustado a, qué tal,`app_homepage`. Me gusta utilizar el prefijo `app_`: facilita la búsqueda del nombre de la ruta más adelante. + +[[[ code('25d040ab5a') ]]] + +Por cierto, los atributos de PHP 8 -como este atributo `Route` - están representados por clases PHP reales y físicas. Si mantienes pulsado command o ctrl, puedes abrirlo y mirar dentro. Esto es genial: el método `__construct()` muestra todas las diferentes opciones que puedes pasar al atributo. + +Por ejemplo, hay un argumento `name`... y entonces estamos utilizando la sintaxis de argumentos con nombre de PHP para pasar esto al atributo. Abrir un atributo es una buena manera de conocer sus opciones. + +## Generar una URL desde Twig + +De todos modos, ahora que le hemos dado un nombre, vuelve a nuestro terminal y ejecuta de nuevo`debug:router`: + +```terminal-silent +php bin/console debug:router +``` + +Esta vez... ¡sí! ¡La ruta se llama `app_homepage`! Cópialo y vuelve a `base.html.twig`. Para generar una URL dentro de twig, di `{{` -porque vamos a imprimir algo- y luego utiliza una función Twig llamada `path()`. Pásale el nombre de la ruta. + +[[[ code('8a471c3074') ]]] + +Ya está Actualiza... ¡y el enlace de aquí arriba funciona! + +Falta un enlace más. Ya conocemos el primer paso: dar un nombre a la ruta. Así que `name:` y, qué tal, `app_browse`. + +[[[ code('c8ff1fc6dd') ]]] + +Copia eso, y... desplázate un poco hacia abajo. Aquí está: "Examinar mezclas". Cámbialo por `{{ path('app_browse') }}`. + +[[[ code('802a4800ee') ]]] + +Y ahora... ¡ese enlace también funciona! + +## Generar URLs con comodines + +Pero en esta página, tenemos algunos enlaces rápidos para ir a la página de exploración de un género específico. Y éstos aún no funcionan. + +Esto es interesante. Queremos generar una URL como antes... pero esta vez necesitamos pasar algo al comodín `{slug}`. Abre `browse.html.twig`. Así es como lo hacemos. La primera parte es la misma: `{{ path() }}` y luego el nombre de la ruta: `app_browse`. + +Si nos detuviéramos aquí, se generaría `/browse`. Para pasar valores a cualquier comodín de una ruta, `path()` tiene un segundo argumento: una matriz asociativa de esos valores. Y, de nuevo, al igual que en JavaScript, para crear una "matriz asociativa", utilizas`{` y `}`. Voy a pulsar intro para dividir esto en varias líneas... sólo para que sea legible. Dentro añade una clave `slug` a la matriz... y como este es el género "Pop", ponla en `pop`. + +¡Genial! Repitamos esto dos veces más: `{{ path('app_browse') }}` pasar las llaves para un array asociativo, con `slug` fijado en `rock`. Y luego una vez más aquí abajo... que haré muy rápidamente. + +[[[ code('4fa3d35edf') ]]] + +¡Vamos a ver si funciona! Actualiza. ¡Ah! La variable `rock` no existe. Seguro que alguno de vosotros me ha visto hacer eso. Me olvidé de las comillas, así que esto parece una variable. + +Inténtalo de nuevo. Ya está. Y prueba los enlaces... ¡sí! ¡Funcionan! + +Siguiente: hemos creado dos páginas HTML. Ahora vamos a ver cómo queda la creación de una ruta de la API JSON. \ No newline at end of file diff --git a/sfcasts/ep1/es/generate-urls.vtt b/sfcasts/ep1/es/generate-urls.vtt new file mode 100644 index 0000000..094d3d5 --- /dev/null +++ b/sfcasts/ep1/es/generate-urls.vtt @@ -0,0 +1,400 @@ +WEBVTT + +00:00:01.066 --> 00:00:03.706 align:middle +Hay dos formas diferentes de +interactuar con nuestra aplicación. + +00:00:04.516 --> 00:00:06.826 align:middle +La primera es a través del servidor web... + +00:00:07.296 --> 00:00:09.016 align:middle +¡y eso es lo que hemos hecho! + +00:00:09.526 --> 00:00:11.706 align:middle +Llegamos a una URL y... + +00:00:11.706 --> 00:00:17.556 align:middle +entre bastidores, se ejecuta +public/index.php, que arranca Symfony, + +00:00:17.726 --> 00:00:20.276 align:middle +llama al enrutamiento y +ejecuta nuestro controlador. + +00:00:21.556 --> 00:00:24.986 align:middle +¿Cuál es la segunda forma de +interactuar con nuestra aplicación? + +00:00:25.456 --> 00:00:31.216 align:middle +Todavía no la hemos visto: es a través de una +herramienta de línea de comandos llamada bin/console. + +00:00:32.246 --> 00:00:35.606 align:middle +En tu terminal ejecuta: php bin/console... + +00:00:35.696 --> 00:00:39.626 align:middle +para ver un montón de +comandos dentro de este script. + +00:00:40.526 --> 00:00:41.976 align:middle +Me encanta esta cosa. Está + +00:00:42.736 --> 00:00:48.346 align:middle +lleno de cosas que nos ayudan a depurar, eventualmente +tendrá comandos de generación de código, + +00:00:48.676 --> 00:00:51.916 align:middle +comandos para establecer secretos: +todo tipo de cosas buenas + +00:00:51.916 --> 00:00:54.316 align:middle +que vamos a descubrir poco a poco. + +00:00:55.136 --> 00:00:57.276 align:middle +Pero quiero señalar que... + +00:00:57.336 --> 00:01:01.286 align:middle +¡no hay nada especial en +este script de bin/console! + +00:01:01.866 --> 00:01:08.516 align:middle +Es sólo un archivo: hay literalmente un +directorio bin/ con un archivo console dentro. + +00:01:09.176 --> 00:01:14.756 align:middle +Probablemente nunca necesitarás abrir este +archivo ni pensar en él, pero es útil. + +00:01:15.446 --> 00:01:18.416 align:middle +Ah, y en la mayoría de los sistemas, +puedes simplemente ejecutar: ./bin/console + +00:01:18.416 --> 00:01:19.596 align:middle +... que se + +00:01:19.706 --> 00:01:20.856 align:middle +ve mejor. O + +00:01:21.576 --> 00:01:24.606 align:middle +a veces puedes ver que ejecute: +symfony console. .. que no es + +00:01:24.816 --> 00:01:28.286 align:middle +más que otra forma de ejecutar este archivo. + +00:01:29.176 --> 00:01:31.876 align:middle +Hablaremos más de esto +en un futuro tutorial. El + +00:01:33.046 --> 00:01:43.096 align:middle +primer comando que quiero comprobar dentro de +bin/console es debug:router: Esto es impresionante. + +00:01:43.546 --> 00:01:51.506 align:middle +Nos muestra todas las rutas de nuestra aplicación, +como nuestras dos rutas de / y /browse/{slug}. ¿Qué + +00:01:52.486 --> 00:01:54.316 align:middle +son estas otras rutas? Proceden + +00:01:55.296 --> 00:01:59.176 align:middle +de la barra de herramientas de depuración +web y del sistema de perfilado... y + +00:01:59.496 --> 00:02:02.576 align:middle +sólo están aquí mientras +desarrollamos localmente. Vale, + +00:02:03.846 --> 00:02:05.286 align:middle +de vuelta a nuestro sitio.... en la + +00:02:05.696 --> 00:02:10.106 align:middle +parte superior de la página, +tenemos dos enlaces no funcionales a + +00:02:10.166 --> 00:02:12.356 align:middle +la página de inicio y a la +página de navegación. Vamos a + +00:02:12.826 --> 00:02:13.966 align:middle +conectarlos. Abre + +00:02:15.316 --> 00:02:18.046 align:middle +templates/ base.html.twig ... y busca las + +00:02:18.306 --> 00:02:20.406 align:middle +etiquetas a. Ya + +00:02:21.836 --> 00:02:22.266 align:middle +está. Así que + +00:02:22.866 --> 00:02:28.686 align:middle +sería muy fácil hacer que esto +funcionara con sólo href="/". Pero + +00:02:29.456 --> 00:02:32.846 align:middle +en lugar de eso, cada vez que enlacemos +una página en Symfony, vamos a + +00:02:33.086 --> 00:02:38.106 align:middle +pedir al sistema de enrutamiento +que nos genere una URL. + +00:02:38.836 --> 00:02:46.076 align:middle +Diremos Por favor, genera la URL de la ruta de la página de +inicio, o de la ruta de la página de navegación. Entonces, + +00:02:46.826 --> 00:02:53.526 align:middle +si alguna vez cambiamos la URL de una ruta, todos +nuestros enlaces se actualizarán al instante. Mágico. + +00:02:54.346 --> 00:02:57.636 align:middle +Empecemos por la página de inicio. ¿Cómo + +00:02:58.136 --> 00:03:02.036 align:middle +le pedimos a Symfony que genere +una URL para esta ruta? Bueno + +00:03:02.926 --> 00:03:06.736 align:middle +, primero tenemos que dar un +nombre a la ruta. ¡Sorpresa! + +00:03:07.346 --> 00:03:11.626 align:middle +Cada ruta tiene un nombre interno. + +00:03:11.626 --> 00:03:14.536 align:middle +Puedes verlo en debug:router. Nuestras rutas + +00:03:15.946 --> 00:03:20.146 align:middle +se llaman app_vinyl_homepage +y app_vinyl_browse. Huh, + +00:03:20.516 --> 00:03:25.046 align:middle +esos son los nombres exactos de mis tortugas +mascota cuando era niño. ¿De dónde + +00:03:26.256 --> 00:03:28.066 align:middle +vienen estos nombres? Por + +00:03:28.546 --> 00:03:34.016 align:middle +defecto, Symfony nos genera automáticamente +un nombre, lo cual está bien. El nombre no se + +00:03:34.346 --> 00:03:38.836 align:middle +utiliza en absoluto hasta que +generamos una URL con él. Y en cuanto + +00:03:39.406 --> 00:03:42.976 align:middle +necesitemos generar una URL a una ruta, + +00:03:43.336 --> 00:03:46.596 align:middle +recomiendo encarecidamente tomar +el control de este nombre... + +00:03:46.926 --> 00:03:50.156 align:middle +para asegurarnos de que nunca +cambie accidentalmente. Para ello + +00:03:51.216 --> 00:03:58.776 align:middle +, busca la ruta y añade un argumento: +name ajustado a, qué tal, app_homepage. + +00:03:59.946 --> 00:04:05.906 align:middle +Me gusta utilizar el prefijo app_: facilita la búsqueda +del nombre de la ruta más adelante. Por cierto, los + +00:04:06.566 --> 00:04:10.936 align:middle +atributos de PHP 8 -como +este atributo Route - están + +00:04:11.076 --> 00:04:15.586 align:middle +representados por clases +PHP reales y físicas. Si + +00:04:16.406 --> 00:04:20.666 align:middle +mantienes pulsado command o ctrl, +puedes abrirlo y mirar dentro. Esto + +00:04:21.556 --> 00:04:25.426 align:middle +es genial: el método +__construct() muestra todas las + +00:04:25.426 --> 00:04:28.236 align:middle +diferentes opciones que +puedes pasar al atributo. Por + +00:04:29.176 --> 00:04:31.866 align:middle +ejemplo, hay un argumento name... y + +00:04:32.156 --> 00:04:38.476 align:middle +entonces estamos utilizando la sintaxis de argumentos +con nombre de PHP para pasar esto al atributo. Abrir + +00:04:39.466 --> 00:04:43.556 align:middle +un atributo es una buena manera de +conocer sus opciones. De todos modos, + +00:04:44.576 --> 00:04:48.316 align:middle +ahora que le hemos dado un nombre, +vuelve a nuestro terminal y + +00:04:48.316 --> 00:04:52.016 align:middle +ejecuta de nuevo debug:router: +Esta vez... ¡sí! + +00:04:52.386 --> 00:04:55.996 align:middle +¡La ruta se llama app_homepage! Cópialo + +00:04:56.886 --> 00:05:00.266 align:middle +y vuelve a base.html.twig. Para + +00:05:01.416 --> 00:05:06.896 align:middle +generar una URL dentro de Tw ig , di +{{ -porque vamos a imprimir algo- y + +00:05:07.166 --> 00:05:10.506 align:middle +utiliza una función Tw +ig llamada path(). Pásale + +00:05:12.316 --> 00:05:14.136 align:middle +el nombre de la ruta. Ya está + +00:05:15.086 --> 00:05:16.866 align:middle +Actualiza... ¡y + +00:05:19.106 --> 00:05:22.516 align:middle +el enlace de aquí arriba funciona! Falta un + +00:05:23.136 --> 00:05:24.496 align:middle +enlace más. + +00:05:25.406 --> 00:05:30.566 align:middle +Ya conocemos el primer paso: dar +un nombre a la ruta. Así que + +00:05:30.566 --> 00:05:34.396 align:middle +name: y, qué tal, app_browse. Copia + +00:05:35.396 --> 00:05:37.546 align:middle +eso, y... desplázate + +00:05:37.546 --> 00:05:38.496 align:middle +un poco hacia abajo. Aquí + +00:05:38.496 --> 00:05:42.926 align:middle +está: "Examinar mezclas". + +00:05:43.286 --> 00:05:46.966 align:middle +Cámbialo por {{ path('app_browse') }}. Y + +00:05:50.486 --> 00:05:51.646 align:middle +ahora... ¡ese + +00:05:52.146 --> 00:05:53.836 align:middle +enlace también funciona! Ah, + +00:05:54.696 --> 00:06:01.796 align:middle +pero en esta página tenemos algunos enlaces rápidos para +ir a la página de exploración de un género concreto. Y + +00:06:02.266 --> 00:06:04.086 align:middle +éstos aún no funcionan. Esto + +00:06:04.746 --> 00:06:06.166 align:middle +es interesante. + +00:06:06.606 --> 00:06:09.386 align:middle +Queremos generar una URL como antes... pero + +00:06:09.646 --> 00:06:15.306 align:middle +esta vez tenemos que pasar +algo al comodín {slug}. Abre + +00:06:16.536 --> 00:06:18.576 align:middle +browse.html.twig . Así es + +00:06:19.076 --> 00:06:20.526 align:middle +como lo hacemos. La + +00:06:21.346 --> 00:06:27.106 align:middle +primera parte es la misma: {{ path() }} y +luego el nombre de la ruta: app_browse. Si + +00:06:28.406 --> 00:06:31.616 align:middle +nos detuviéramos aquí, +se generaría /browse. Para + +00:06:32.066 --> 00:06:37.856 align:middle +pasar valores a cualquier comodín de una +ruta, path() tiene un segundo argumento: una + +00:06:38.466 --> 00:06:41.596 align:middle +matriz asociativa de esos valores. Y, + +00:06:42.176 --> 00:06:49.846 align:middle +de nuevo, al igual que en JavaScript, para crear +una "matriz asociativa", utilizas { y } . Voy a + +00:06:49.846 --> 00:06:55.086 align:middle +pulsar intro para dividir esto +en varias líneas... sólo + +00:06:55.266 --> 00:06:56.676 align:middle +para mantener las cosas legibles. Dentro + +00:06:57.416 --> 00:07:00.376 align:middle +añade una clave slug a la matriz... y + +00:07:01.046 --> 00:07:04.346 align:middle +como este es el género +"Pop", ponla en pop. ¡Genial! + +00:07:05.396 --> 00:07:11.376 align:middle +Repitamos esto dos veces más: {{ +path('app_browse') }} pasar las + +00:07:11.896 --> 00:07:17.236 align:middle +llaves para un array asociativo, +con slug fijado en rock. Y + +00:07:18.526 --> 00:07:20.756 align:middle +luego una vez más aquí abajo... que + +00:07:21.046 --> 00:07:23.376 align:middle +haré muy rápidamente. ¡ Vamos a + +00:07:23.446 --> 00:07:27.226 align:middle +ver si funciona! Actualiza. + +00:07:27.846 --> 00:07:29.136 align:middle +Ah! La variable + +00:07:29.746 --> 00:07:31.646 align:middle +rock no existe. + +00:07:32.186 --> 00:07:34.176 align:middle +Seguro que alguno de vosotros +me ha visto hacer eso. Me + +00:07:34.796 --> 00:07:37.896 align:middle +olvidé de las comillas, así que +esto parece una variable. Inténtalo + +00:07:39.806 --> 00:07:40.426 align:middle +de nuevo. Ya está. + +00:07:41.806 --> 00:07:42.846 align:middle +Y + +00:07:43.446 --> 00:07:44.526 align:middle +prueba los enlaces... ¡sí! + +00:07:45.236 --> 00:07:46.766 align:middle +¡Funcionan! A continuación: + +00:07:47.876 --> 00:07:51.146 align:middle +hemos creado dos páginas HTML. Ahora vamos a + +00:07:51.566 --> 00:07:56.006 align:middle +ver cómo queda la creación +de una ruta de la API JSON diff --git a/sfcasts/ep1/es/js-vendor-libs.md b/sfcasts/ep1/es/js-vendor-libs.md new file mode 100644 index 0000000..f99fc5d --- /dev/null +++ b/sfcasts/ep1/es/js-vendor-libs.md @@ -0,0 +1,63 @@ +# Instalación de código de terceros en nuestro JS/CSS + +Ahora tenemos un nuevo y bonito sistema de JavaScript y CSS que vive completamente dentro del directorio`assets/`. Vamos a trasladar nuestros estilos públicos a éste. Abre`public/styles/app.css`, copia todo esto, borra todo el directorio... y pégalo en el nuevo `app.css`. Gracias a `encore_entry_link_tags()` en`base.html.twig`, el nuevo CSS se está incluyendo... y ya no necesitamos la antigua etiqueta`link`. + +Ve a comprobarlo. Refresca y... ¡todavía se ve muy bien! + +## Instalación de bibliotecas JavaScript/CSS de terceros + +Vuelve a `base.html.twig`. ¿Qué pasa con estas etiquetas de enlace externo para bootstrap y FontAwesome? Bueno, puedes mantener totalmente estos enlaces CDN. Pero también podemos procesar estas cosas a través de Encore. ¿Cómo? Instalando Bootstrap y FontAwesome como bibliotecas de proveedor e importándolas. + +Elimina todas estas etiquetas de enlace... y luego actualiza. ¡Vaya! Vuelve a parecer que he diseñado este sitio. Vamos... primero a volver a añadir bootstrap. Busca tu terminal. Ya que el comando watch se está ejecutando, abre una nueva pestaña de terminal y ejecútalo: + +```terminal +yarn add bootstrap --dev +``` + +Esto hace tres cosas. Primero, añade `bootstrap` a nuestro archivo `package.json`. Segundo, descarga bootstrap en nuestro directorio `node_modules/`... lo encontrarías aquí abajo. Y tercero, actualiza el archivo `yarn.lock` con la versión exacta de bootstrap que acaba de descargar. + +Si nos detuviéramos ahora... ¡esto no supondría ninguna diferencia! Hemos descargado bootstrap -yay- pero no lo estamos utilizando. + +Para usarlo, tenemos que importarlo. Entra en `app.css`. Al igual que en los archivos JavaScript, podemos importar desde dentro de los archivos CSS diciendo `@import` y luego el archivo. Podemos hacer referencia a un archivo en el mismo directorio con `./other-file.css`. O, si quieres importar algo del directorio `node_modules/` en CSS, hay un truco: un `~` y luego el nombre del paquete: `bootstrap`. + +[[[ code('43abc8b3a6') ]]] + +Eso es todo En cuanto hicimos eso, la función de vigilancia de Encore reconstruyó nuestro archivo `app.css`... ¡que ahora incluye Bootstrap! Observa: actualiza la página y... ¡volvemos a estar de vuelta! ¡Qué bien! + +Las otras dos cosas que nos faltan son `FontAwesome` y una fuente específica. Para añadirlas, vuelve al terminal y ejecútalas: + +```terminal +yarn add @fontsource/roboto-condensed --dev +``` + +Revelación completa: hice algunas búsquedas antes de grabar para saber los nombres de todos los paquetes que necesitamos. Puedes buscar los paquetes en https://npmjs.com. + +Añadamos también el último que necesitamos: + +```terminal +yarn add @fortawesome/fontawesome-free --dev +``` + +De nuevo, esto descargó las dos bibliotecas en nuestro proyecto... pero no las utiliza automáticamente todavía. Como esas bibliotecas contienen archivos CSS, vuelve a nuestro archivo`app.css` e impórtalos: `@import '~'` y luego `@fortawesome/fontawesome-free`. Y `@import '~@fontsource/roboto-condensed'`. + +[[[ code('c72cdebd5f') ]]] + +El primer paquete debería arreglar este icono... y el segundo debería hacer que la fuente cambie en toda la página. Observa el tipo de letra cuando refrescamos... ¡ha cambiado! Pero... los iconos siguen estando algo rotos. + +## Importar archivos específicos de node_modules/ + +Para ser totalmente honesto, no estoy seguro de por qué esto no funciona fuera de la caja. Pero la solución es bastante interesante. Mantén pulsado command en un Mac -o ctrl en caso contrario- y haz clic en esta cadena `fontawesome-free`. + +Cuando usas esta sintaxis, va a tu directorio `node_modules/`, a`@fortawesome/fontawesome-free`... y entonces, si no pones ningún nombre de archivo después de esto, hay un mecanismo en el que esta biblioteca le dice a Webpack qué archivo CSS debe importar. Por defecto, importa este archivo `fontawesome.css`. Por alguna razón... eso no funciona. Lo que queremos es este `all.css`. + +Y podemos importarlo añadiendo la ruta: `/css/all.css`. No necesitamos el archivo minificado porque Encore se encarga de minificar por nosotros. + +[[[ code('0e00a5819a') ]]] + +Y ahora... ¡estamos de vuelta! + +La principal razón por la que me encanta Webpack Encore y este sistema es que nos permite utilizar importaciones adecuadas. Incluso podemos organizar nuestro JavaScript en pequeños archivos -poniendo clases o funciones en cada uno- y luego importarlos cuando los necesitemos. Ya no son necesarias las variables globales. + +Webpack también nos permite utilizar cosas más serias como React o Vue: incluso puedes ver, en `webpack.config.js`, los métodos para activarlos. + +Pero, por lo general, me gusta utilizar una encantadora biblioteca de JavaScript llamada Stimulus. Y quiero hablarte de ella a continuación. \ No newline at end of file diff --git a/sfcasts/ep1/es/js-vendor-libs.vtt b/sfcasts/ep1/es/js-vendor-libs.vtt new file mode 100644 index 0000000..8b09f2b --- /dev/null +++ b/sfcasts/ep1/es/js-vendor-libs.vtt @@ -0,0 +1,298 @@ +WEBVTT + +00:00:01.076 --> 00:00:04.456 align:middle +Ahora tenemos un nuevo y bonito +sistema de JavaScript y CSS + +00:00:04.456 --> 00:00:07.586 align:middle +que vive completamente dentro +del directorio assets/. + +00:00:08.376 --> 00:00:10.356 align:middle +Vamos a trasladar nuestros +estilos públicos a éste. + +00:00:11.076 --> 00:00:17.506 align:middle +Abre public/styles/app.css, copia +todo esto, borra todo el directorio... + +00:00:17.936 --> 00:00:20.416 align:middle +y pégalo en el nuevo app.css. + +00:00:20.416 --> 00:00:29.116 align:middle +Gracias al encore_entry_link_tags() en +base.html.twig, el nuevo CSS se está incluyendo... + +00:00:29.246 --> 00:00:31.806 align:middle +y ya no necesitamos la antigua etiqueta link. + +00:00:32.906 --> 00:00:33.496 align:middle +Ve a comprobarlo. + +00:00:34.326 --> 00:00:35.636 align:middle +Refresca y... + +00:00:35.936 --> 00:00:37.576 align:middle +¡todavía se ve muy bien! + +00:00:38.426 --> 00:00:40.736 align:middle +Vuelve a base.html.twig. + +00:00:41.426 --> 00:00:45.876 align:middle +¿Qué pasa con estas etiquetas de enlace +externo para bootstrap y FontAwesome? + +00:00:46.686 --> 00:00:49.766 align:middle +Bueno, puedes mantener +totalmente estos enlaces CDN. + +00:00:50.216 --> 00:00:53.676 align:middle +Pero también podemos procesar +estas cosas a través de Encore. + +00:00:54.456 --> 00:01:01.096 align:middle +¿Cómo? Instalando Bootstrap y FontAwesome +como bibliotecas de proveedor e importándolas. + +00:01:01.956 --> 00:01:03.546 align:middle +Elimina todas estas etiquetas de enlace... + +00:01:03.726 --> 00:01:05.186 align:middle +y luego actualiza. + +00:01:07.406 --> 00:01:11.356 align:middle +¡Vaya! Vuelve a parecer +que he diseñado este sitio. + +00:01:12.066 --> 00:01:14.096 align:middle +Vamos... primero a volver a añadir bootstrap. + +00:01:14.686 --> 00:01:15.676 align:middle +Busca tu terminal. + +00:01:16.586 --> 00:01:20.596 align:middle +Ya que el comando watch se está ejecutando, +abre una nueva pestaña de terminal y ejecútalo: + +00:01:20.776 --> 00:01:26.696 align:middle +yarn add bootstrap --dev Esto hace tres cosas. + +00:01:27.656 --> 00:01:31.116 align:middle +Primero, añade bootstrap a +nuestro archivo package.json. + +00:01:32.356 --> 00:01:36.316 align:middle +En segundo lugar, descarga bootstrap +en nuestro directorio node_modules/... + +00:01:36.566 --> 00:01:37.756 align:middle +que encontrarás aquí abajo. + +00:01:39.236 --> 00:01:44.306 align:middle +Y tercero, actualiza el archivo +yarn.lock con la versión exacta + +00:01:44.306 --> 00:01:46.526 align:middle +de bootstrap que acaba de descargar. + +00:01:47.656 --> 00:01:48.606 align:middle +Si nos detuviéramos ahora... + +00:01:49.096 --> 00:01:51.276 align:middle +¡esto no supondría ninguna diferencia! + +00:01:51.636 --> 00:01:56.366 align:middle +Hemos descargado bootstrap -yay- +pero no lo estamos utilizando. + +00:01:57.226 --> 00:01:59.876 align:middle +Para usarlo, tenemos que importarlo. + +00:02:00.546 --> 00:02:02.196 align:middle +Entra en app.css. + +00:02:03.516 --> 00:02:09.056 align:middle +Al igual que en los archivos JavaScript, podemos +importar desde dentro de los archivos CSS + +00:02:09.126 --> 00:02:11.976 align:middle +diciendo @import y luego el archivo. + +00:02:12.736 --> 00:02:15.356 align:middle +Podemos hacer referencia a un archivo en +el mismo directorio con ./other-file.css. + +00:02:15.356 --> 00:02:17.446 align:middle +O, + +00:02:17.926 --> 00:02:24.196 align:middle +si quieres importar algo del directorio +node_modules/ en CSS, hay un truco: un + +00:02:24.446 --> 00:02:28.146 align:middle +~ y luego el nombre del +paquete: bootstrap. Eso es + +00:02:28.686 --> 00:02:29.566 align:middle +todo En + +00:02:30.216 --> 00:02:36.216 align:middle +cuanto hicimos eso, la función de vigilancia de +Encore reconstruyó nuestro archivo app.css... ¡que + +00:02:36.466 --> 00:02:39.016 align:middle +ahora incluye Bootstrap! Observa: + +00:02:39.456 --> 00:02:41.786 align:middle +actualiza la página y... + +00:02:42.456 --> 00:02:43.616 align:middle +¡volvemos a estar en ella! ¡ Qué + +00:02:43.746 --> 00:02:49.766 align:middle +bien! Las otras dos cosas que nos faltan son +FontAwesome y una fuente específica. Para + +00:02:50.566 --> 00:02:59.476 align:middle +añadirlas, vuelve al terminal y ejecútalas: +yarn add @fontsource/roboto-condensed --dev + +00:02:59.476 --> 00:03:05.186 align:middle +Revelación completa: hice algunas +búsquedas antes de grabar para + +00:03:05.396 --> 00:03:08.716 align:middle +saber los nombres de todos +los paquetes que necesitamos. + +00:03:09.486 --> 00:03:13.276 align:middle +Puedes buscar los paquetes +en https://npmjs.com. + +00:03:14.546 --> 00:03:20.826 align:middle +Añadamos también el último que necesitamos: +yarn add @fortawesome/fontawesome-free --dev + +00:03:20.896 --> 00:03:27.216 align:middle +De nuevo, esto descargó las dos +bibliotecas en nuestro proyecto... pero + +00:03:27.316 --> 00:03:30.046 align:middle +no las utiliza automáticamente todavía. Como + +00:03:30.816 --> 00:03:37.716 align:middle +esas bibliotecas contienen archivos CSS, vuelve +a nuestro archivo app.css e impórtalas: @import + +00:03:38.346 --> 00:03:43.626 align:middle +'~' entonces @fortawesome/fontawesome-free. Y + +00:03:44.666 --> 00:03:49.506 align:middle +@import '~@fontsource/roboto-condensed'. El + +00:03:50.216 --> 00:03:52.966 align:middle +primer paquete debería +arreglar este icono... y + +00:03:53.286 --> 00:03:57.066 align:middle +el segundo debería hacer que la fuente +cambie en toda la página. Observa + +00:03:57.796 --> 00:03:59.836 align:middle +el tipo de letra cuando actualizamos... + +00:04:00.636 --> 00:04:02.016 align:middle +¡sí que ha cambiado! Pero... + +00:04:02.416 --> 00:04:02.886 align:middle +los iconos + +00:04:02.936 --> 00:04:06.026 align:middle +siguen estando algo rotos. Para + +00:04:07.076 --> 00:04:11.236 align:middle +ser totalmente honesto, no estoy seguro de +por qué no funcionan fuera de la caja. Pero + +00:04:11.286 --> 00:04:13.316 align:middle +la solución es bastante +interesante. Mantén pulsado + +00:04:14.336 --> 00:04:20.366 align:middle +command en un Mac -o ctrl en caso contrario- y +haz clic en esta cadena fontawesome-free. Cuando + +00:04:21.326 --> 00:04:26.286 align:middle +usas esta sintaxis, va a tu +directorio node_modules/, a + +00:04:26.626 --> 00:04:30.036 align:middle +@fortawesome/fontawesome-free ... y + +00:04:30.546 --> 00:04:36.026 align:middle +entonces, si no pones ningún nombre de archivo +después de esto, hay un mecanismo en el que + +00:04:36.206 --> 00:04:41.586 align:middle +esta biblioteca le dice a Webpack +qué archivo CSS debe importar. Por + +00:04:42.316 --> 00:04:46.306 align:middle +defecto, importa este +archivo fontawesome.css. Por + +00:04:47.036 --> 00:04:48.256 align:middle +alguna razón... eso + +00:04:48.316 --> 00:04:49.566 align:middle +no funciona. Lo que + +00:04:50.116 --> 00:04:52.906 align:middle +queremos es este all.css. Y + +00:04:53.446 --> 00:05:00.176 align:middle +podemos importarlo añadiendo +la ruta: /css/all.css. + +00:05:00.686 --> 00:05:05.226 align:middle +No necesitamos el archivo minificado porque +Encore se encarga de minificar por nosotros. Y + +00:05:06.136 --> 00:05:06.876 align:middle +ahora ... ¡estamos + +00:05:07.606 --> 00:05:08.876 align:middle +de vuelta! La + +00:05:09.676 --> 00:05:14.086 align:middle +principal razón por la que me encanta +Webpack Encore y este sistema es que + +00:05:14.086 --> 00:05:16.966 align:middle +nos permite utilizar importaciones adecuadas. + +00:05:17.796 --> 00:05:23.976 align:middle +Incluso podemos organizar nuestro JavaScript en pequeños +archivos -poniendo clases o funciones en cada uno- y + +00:05:24.446 --> 00:05:26.876 align:middle +luego importarlos cuando los necesitemos. + +00:05:27.486 --> 00:05:30.356 align:middle +Ya no son necesarias las +variables globales. Webpack + +00:05:31.116 --> 00:05:37.396 align:middle +también nos permite utilizar cosas más +serias como React o Vue: incluso puedes ver, en + +00:05:37.566 --> 00:05:41.466 align:middle +webpack.config.js , los +métodos para activarlos. Pero + +00:05:42.096 --> 00:05:48.116 align:middle +, por lo general, me gusta utilizar una encantadora +biblioteca de JavaScript llamada Stimulus. Y + +00:05:48.596 --> 00:05:50.626 align:middle +quiero hablarte de ella a continuación diff --git a/sfcasts/ep1/es/json-api.md b/sfcasts/ep1/es/json-api.md new file mode 100644 index 0000000..7349883 --- /dev/null +++ b/sfcasts/ep1/es/json-api.md @@ -0,0 +1,48 @@ +# Ruta de la API JSON + +En un futuro tutorial, vamos a crear una base de datos para gestionar las canciones, los géneros y los discos de vinilo mezclados que nuestros usuarios están creando. Ahora mismo, estamos trabajando completamente con datos codificados... pero nuestros controladores -y- especialmente las plantillas no serán muy diferentes una vez que hagamos todo esto dinámico. + +Así que este es nuestro nuevo objetivo: quiero crear una ruta de la API que devuelva los datos de una sola canción como JSON. Vamos a usar esto en unos minutos para dar vida a este botón de reproducción. Por el momento, ninguno de estos botones hace nada, pero tienen un aspecto bonito. + + Crear el controlador JSON + +Los dos pasos para crear un punto final de la API son... exactamente los mismos que para crear una página HTML: necesitamos una ruta y un controlador. Como esta ruta de la API devolverá datos de canciones, en lugar de añadir otro método dentro de `VinylController`, vamos a crear una clase de controlador totalmente nueva. La forma en que organices este material depende enteramente de ti. + +Crea una nueva clase PHP llamada `SongController`... o `SongApiController` también sería un buen nombre. En su interior, ésta comenzará como cualquier otro controlador, extendiendo`AbstractController`. Recuerda: esto es opcional... pero nos proporciona métodos de acceso directo sin inconvenientes. + +A continuación, crea un `public function` llamado, qué tal, `getSong()`. Añade la ruta... y pulsa el tabulador para autocompletar esto de forma que PhpStorm añada la declaración de uso en la parte superior. Establece la URL como `/api/songs/{id}`, donde `id` será finalmente el id de la base de datos de la canción. + +Y como tenemos un comodín en la ruta, se nos permite tener un argumento `$id`. Por último, aunque no necesitamos hacerlo, como sabemos que nuestro controlador devolverá un objeto `Response`, podemos establecerlo como tipo de retorno. Asegúrate de autocompletar el del componente `HttpFoundation` de Symfony. + +Dentro del método, para empezar, `dd($id)`... sólo para ver si todo funciona. + +[[[ code('ad23fa7738') ]]] + +¡Vamos a hacerlo! Dirígete a `/api/songs/5` y... ¡lo tienes! Otra página nueva. + +De vuelta a ese controlador, voy a pegar algunos datos de la canción: finalmente, esto vendrá de la base de datos. Puedes copiarlo del bloque de código de esta página. Nuestro trabajo es devolverlo como JSON. + +Entonces, ¿cómo devolvemos JSON en Symfony? Devolviendo un nuevo `JsonResponse` y pasándole los datos. + +[[[ code('3450058bee') ]]] + +Lo sé... ¡demasiado fácil! Refresca y... ¡hola JSON! Ahora puedes estar pensando: + +> ¡Ryan! Nos has estado diciendo -repetidamente- que un controlador debe +> devolver siempre un objeto Symfony `Response`, que es lo que devuelve `render()`. +> ¿Ahora devuelve otro tipo de objeto `Response`? + +Vale, es justo... pero esto funciona porque `JsonResponse` es una Respuesta. Me explico: a veces es útil saltar a las clases principales para ver cómo funcionan. Para ello, en PHPStorm -si estás en un Mac mantén pulsado comando, si no, mantén pulsado control- y luego haz clic en el nombre de la clase a la que quieras saltar. Y... ¡sorpresa! `JsonResponse` +extiende `Response`. Sí, seguimos devolviendo un `Response`. Pero esta subclase está bien porque codifica automáticamente JSON nuestros datos y establece la cabecera`Content-Type` en `application/json`. + +## El método abreviado ->json() + +Ah, y de vuelta a nuestro controlador, podemos ser aún más perezosos diciendo`return $this->json($song)`... donde `json()` es otro método abreviado que viene de `AbstractController`. + +[[[ code('6499fe8656') ]]] + +Hacer esto no supone ninguna diferencia, porque sólo es un atajo para devolver ... ¡un `JsonResponse`! + +Si estás construyendo una API seria, Symfony tiene un componente`serializer` que es realmente bueno para convertir objetos en JSON... y luego JSON de nuevo en objetos. Hablamos mucho de él en nuestro tutorial de la Plataforma API, que es una potente biblioteca para crear APIs en Symfony. + +A continuación, vamos a aprender cómo hacer que nuestras rutas sean más inteligentes, por ejemplo, haciendo que un comodín sólo coincida con un número, en lugar de coincidir con cualquier cosa. diff --git a/sfcasts/ep1/es/json-api.vtt b/sfcasts/ep1/es/json-api.vtt new file mode 100644 index 0000000..a76679f --- /dev/null +++ b/sfcasts/ep1/es/json-api.vtt @@ -0,0 +1,250 @@ +WEBVTT + +00:00:01.066 --> 00:00:06.036 align:middle +En un futuro tutorial, vamos a crear una base de +datos para gestionar las canciones, los géneros + +00:00:06.036 --> 00:00:09.436 align:middle +y los discos de vinilo mezclados +que crean nuestros usuarios. + +00:00:10.266 --> 00:00:13.556 align:middle +Ahora mismo, estamos trabajando +completamente con datos codificados... + +00:00:13.976 --> 00:00:18.236 align:middle +pero nuestros controladores - y - +especialmente las plantillas no serán + +00:00:18.236 --> 00:00:20.896 align:middle +muy diferentes una vez que +hagamos todo esto dinámico. + +00:00:21.596 --> 00:00:26.066 align:middle +Así que este es nuestro nuevo objetivo: +quiero crear una ruta de la API + +00:00:26.066 --> 00:00:29.876 align:middle +que devuelva los datos de +una sola canción como JSON. + +00:00:30.606 --> 00:00:35.576 align:middle +Vamos a usar esto en unos minutos para +dar vida a este botón de reproducción. + +00:00:35.696 --> 00:00:40.136 align:middle +Por el momento, ninguno de estos botones +hace nada, pero tienen un aspecto bonito. + +00:00:41.016 --> 00:00:44.306 align:middle +Los dos pasos para crear +una ruta de la API son... + +00:00:44.656 --> 00:00:49.906 align:middle +exactamente los mismos que para crear una página +HTML: necesitamos una ruta y un controlador. + +00:00:49.906 --> 00:00:57.076 align:middle +Como esta ruta de la API devolverá datos de la +canción, en lugar de añadir otro método dentro + +00:00:57.076 --> 00:01:01.056 align:middle +de VinylController, vamos a crear una +clase de controlador totalmente nueva. + +00:01:01.736 --> 00:01:05.006 align:middle +La forma en que organices este +material depende enteramente de ti. + +00:01:05.726 --> 00:01:08.156 align:middle +Crea una nueva clase PHP +llamada SongController... + +00:01:08.446 --> 00:01:11.736 align:middle +o SongApiController también +sería un buen nombre. + +00:01:12.486 --> 00:01:17.846 align:middle +En su interior, ésta comenzará como cualquier +otro controlador, extendiendo AbstractController. + +00:01:18.556 --> 00:01:20.476 align:middle +Recuerda: eso es opcional... + +00:01:20.566 --> 00:01:23.576 align:middle +pero nos proporciona métodos +abreviados sin inconvenientes. + +00:01:23.576 --> 00:01:28.216 align:middle +A continuación, crea un public +function llamado, qué tal, getSong(). + +00:01:30.346 --> 00:01:31.826 align:middle +Añade la ruta... + +00:01:31.826 --> 00:01:37.586 align:middle +y pulsa el tabulador para autocompletarla y que PhpStorm +añada la declaración de uso en la parte superior. + +00:01:38.546 --> 00:01:49.286 align:middle +Establece la URL como /api/songs/{id}, donde id será +finalmente el id de la base de datos de la canción. + +00:01:50.106 --> 00:01:56.316 align:middle +Y como tenemos un comodín en la ruta, +se nos permite tener un argumento $id. + +00:01:56.406 --> 00:02:00.226 align:middle +Por último, aunque no necesitamos hacerlo, + +00:02:00.456 --> 00:02:04.646 align:middle +como sabemos que nuestro controlador +devolverá un objeto Response, + +00:02:04.906 --> 00:02:07.266 align:middle +podemos establecerlo como tipo de retorno. + +00:02:07.796 --> 00:02:13.086 align:middle +Asegúrate de autocompletar el del +componente HttpFoundation de Symfony. + +00:02:14.286 --> 00:02:18.086 align:middle +Dentro del método, para empezar, dd($id)... + +00:02:18.656 --> 00:02:20.626 align:middle +para ver si todo funciona. + +00:02:21.616 --> 00:02:22.426 align:middle +¡Vamos a hacerlo! + +00:02:23.116 --> 00:02:28.186 align:middle +Dirígete a /api/songs/5 y... + +00:02:28.496 --> 00:02:31.406 align:middle +¡lo tienes! Otra página nueva. De + +00:02:32.276 --> 00:02:37.666 align:middle +vuelta a ese controlador, voy a pegar +algunos datos de la canción: finalmente, + +00:02:37.666 --> 00:02:39.906 align:middle +esto vendrá de la base de datos. + +00:02:40.386 --> 00:02:43.056 align:middle +Puedes copiarlo del bloque +de código de esta página. + +00:02:43.796 --> 00:02:46.806 align:middle +Nuestro trabajo es devolver esto como JSON. + +00:02:47.466 --> 00:02:51.356 align:middle +Entonces, ¿cómo devolvemos JSON en Symfony? + +00:02:51.356 --> 00:02:56.156 align:middle +Devolviendo un nuevo JsonResponse +y pasándole los datos. + +00:02:56.156 --> 00:02:58.846 align:middle +Lo sé... ¡demasiado fácil! + +00:02:58.846 --> 00:03:01.066 align:middle +Refresca y... + +00:03:01.336 --> 00:03:03.026 align:middle +¡hola JSON! + +00:03:03.886 --> 00:03:06.506 align:middle +Ahora puedes estar pensando: ¡Ryan! + +00:03:06.676 --> 00:03:09.556 align:middle +Nos has estado diciendo -repetidamente- + +00:03:09.816 --> 00:03:13.846 align:middle +que un controlador debe devolver +siempre un objeto Symfony Response, + +00:03:14.136 --> 00:03:16.196 align:middle +que es lo que devuelve render(). + +00:03:16.766 --> 00:03:20.226 align:middle +¿Ahora devuelve otro tipo de objeto Response? + +00:03:20.766 --> 00:03:21.836 align:middle +Vale, es justo... + +00:03:22.326 --> 00:03:25.986 align:middle +pero esto funciona porque +JsonResponse es una Respuesta. + +00:03:26.416 --> 00:03:27.306 align:middle +Deja que me explique. + +00:03:27.776 --> 00:03:32.616 align:middle +A veces es útil saltar a las clases +principales para ver cómo funcionan. Para ello + +00:03:33.376 --> 00:03:38.736 align:middle +, en PHPStorm -si estás en un Mac mantén +pulsado comando, si no, mantén pulsado control- + +00:03:38.916 --> 00:03:42.006 align:middle +y luego haz clic en el nombre de +la clase a la que quieras saltar. + +00:03:42.566 --> 00:03:44.596 align:middle +Y... ¡sorpresa! + +00:03:44.846 --> 00:03:48.976 align:middle +JsonResponse extiende Response. + +00:03:49.086 --> 00:03:52.086 align:middle +Sí, seguimos devolviendo un Response. + +00:03:52.506 --> 00:03:57.636 align:middle +Pero esta subclase está bien porque codifica +automáticamente nuestros datos en JSON + +00:03:57.976 --> 00:04:02.366 align:middle +y establece la cabecera +Content-Type en application/json. + +00:04:03.296 --> 00:04:10.796 align:middle +Ah, y de vuelta a nuestro controlador, podemos ser aún +más perezosos diciendo return $this->json($song)... + +00:04:11.436 --> 00:04:16.846 align:middle +donde json() es otro método abreviado +que proviene de AbstractController. + +00:04:17.696 --> 00:04:23.886 align:middle +Hacer esto no supone ninguna diferencia, +porque sólo es un atajo para devolver ... + +00:04:24.036 --> 00:04:25.616 align:middle +¡un JsonResponse! + +00:04:26.186 --> 00:04:32.576 align:middle +Si estás construyendo una API seria, Symfony tiene +un componente serializer que es realmente bueno + +00:04:32.576 --> 00:04:34.676 align:middle +para convertir objetos en JSON... + +00:04:34.756 --> 00:04:37.476 align:middle +y luego JSON de nuevo en objetos. + +00:04:38.186 --> 00:04:41.386 align:middle +Hablamos mucho de él en nuestro +tutorial de la Plataforma API, + +00:04:41.646 --> 00:04:46.946 align:middle +que es una potente biblioteca +para crear APIs en Symfony. + +00:04:47.036 --> 00:04:50.296 align:middle +A continuación, vamos a aprender cómo hacer +que nuestras rutas sean más inteligentes, por + +00:04:50.646 --> 00:04:56.756 align:middle +ejemplo, haciendo que un comodín sólo coincida con +un número, en lugar de coincidir con cualquier cosa diff --git a/sfcasts/ep1/es/profiler.md b/sfcasts/ep1/es/profiler.md new file mode 100644 index 0000000..446608e --- /dev/null +++ b/sfcasts/ep1/es/profiler.md @@ -0,0 +1,89 @@ +# Perfilador: Tu mejor amigo para la depuración + +Es hora de instalar nuestro segundo paquete. Y éste es divertido. Vamos a confirmar nuestros cambios primero: así será más fácil comprobar los cambios que hace la receta del nuevo paquete. + +Añade todo: + +```terminal-silent +git add . +``` + +Parece que está bien, así que... confirma: + +```terminal-silent +git commit -m "Added some Tiwggy goodness" +``` + +Bonito. + +## El paquete de depuración + +Ahora ejecuta: + +```terminal +composer require debug +``` + +Así que sí, este es otro alias de Flex... y aparentemente es un alias de`symfony/debug-pack`. Y sabemos que un paquete es una colección de paquetes. Así que, en lugar de añadir esta única línea a nuestro archivo `composer.json`, si lo comprobamos, parece que ha añadido un nuevo paquete en la sección `require` -se trata de una biblioteca de registro- y... al final, ha añadido una nueva sección`require-dev` con otras tres bibliotecas. + +La diferencia entre `require` y `require-dev` no es demasiado importante: todos estos paquetes se descargaron en nuestra aplicación, pero como mejor práctica, si instalas una biblioteca que sólo está pensada para el desarrollo local, deberías ponerla en`require-dev`. ¡El pack lo hizo por nosotros! ¡Gracias pack! + +## Cambios en la receta + +De vuelta al terminal, ¡esto también instaló tres recetas! Ooh. Veamos qué han hecho. Limpio la pantalla y corro: + +```terminal +git status +``` + +Esto me resulta familiar: modificó `config/bundles.php` para activar tres nuevos bundles. De nuevo, los bundles son plugins de Symfony, que añaden más funciones a nuestra aplicación. + +También añadió varios archivos de configuración al directorio `config/packages/`. Hablaremos más de estos archivos en el próximo tutorial, pero, a alto nivel, controlan el comportamiento de esos bundles. + +## La barra de herramientas de depuración web y el perfilador + +¿Qué nos aportan estos nuevos paquetes? Para averiguarlo, dirígete a tu navegador y actualiza la página de inicio. ¡Santo cielo, Batman! Es la barra de herramientas de depuración web. Esto es una locura de depuración: una barra de herramientas llena de buena información. A la izquierda, puedes ver el controlador al que se ha llamado junto con el código de estado HTTP. También está la cantidad de tiempo que tardó la página, la memoria que utilizó y también cuántas plantillas se renderizaron a través de Twig: este es el bonito icono de Twig. + +En el lado derecho, tenemos detalles sobre el servidor web local Symfony que se está ejecutando e información sobre PHP. + +Pero aún no has visto la mejor parte: haz clic en cualquiera de estos iconos para saltar al perfilador. Esta es la barra de herramientas de depuración web... enloquecida. Está llena de datos sobre esa petición, como la petición y la respuesta, todos los mensajes de registro que se produjeron durante esa petición, información sobre las rutas y la ruta a la que se respondió, Twig te muestra qué plantillas se renderizaron y cuántas veces se renderizaron... y hay información de configuración aquí abajo. ¡Uf! + +Pero mi sección favorita es la de Rendimiento. Muestra una línea de tiempo de todo lo que ha ocurrido durante la petición. Esto es genial por dos razones. La primera es bastante obvia: puedes usarla para encontrar qué partes de tu página son lentas. Así, por ejemplo, nuestro controlador tardó 20,4 milisegundos. Y dentro de la ejecución del controlador, la plantilla de la página de inicio se renderizó en 3,9 milisegundos y `base.html.twig`se renderizó en 2,8 milisegundos. + +La segunda razón por la que esto es realmente genial es que descubre todas las capas ocultas de Symfony. Ajusta este umbral a cero. Antes, esto sólo mostraba las cosas que tardaban más de un milisegundo. Ahora lo muestra todo. No tienes que preocuparte por la gran mayoría de las cosas, pero es superguay ver las capas de Symfony: las cosas que ocurren antes y después de que se ejecute tu controlador. Tenemos un tutorial de inmersión profunda para Symfony si quieres aprender más sobre estas cosas. + +La barra de herramientas de depuración web y el perfilador también crecerán con nuestra aplicación. En un futuro tutorial, cuando instalemos una librería para hablar con la base de datos, de repente tendremos una nueva sección que enumera todas las consultas a la base de datos que hizo una página y el tiempo que tardó cada una. + +## funciones dump() y dd() + +Bien, el paquete de depuración instaló la barra de herramientas de depuración web. También ha instalado una biblioteca de registro que utilizaremos más adelante. Y ha instalado un paquete que nos proporciona dos fantásticas funciones de depuración. + +Dirígete a `VinylController`. Imagina que estamos haciendo un desarrollo y necesitamos ver cómo es esta variable `$tracks`. En este caso es bastante obvio, pero a veces querrás ver lo que hay dentro de un objeto complejo. + +Para ello, digamos `dd($tracks)`, donde "dd" significa "dump" y "die". + +[[[ code('841aecc2d1') ]]] + +Así que si refrescamos... ¡sí! Eso vuelca la variable y mata la página. Y esto es mucho más potente -y más bonito- que usar `var_dump()`: podemos ampliar secciones y ver datos profundos con mucha facilidad. + +En lugar de `dd()`, también puedes utilizar `dump()`.. para volcar y vivir. Pero esto podría no aparecer donde esperas. En lugar de imprimirse en el centro de la página, aparece abajo en la barra de herramientas de depuración de la web, bajo el icono del objetivo. + +[[[ code('13b985bb4b') ]]] + +Si es demasiado pequeño, haz clic para ver una versión más grande en el perfilador. + +## Volcado en Twig + +También puedes utilizar este `dump()` en Twig. Elimina el volcado del controlador... y luego en la plantilla, justo antes del `ul`, `dump(tracks)`. + +[[[ code('ee2dc39650') ]]] + +Y esto... se ve exactamente igual. Excepto que cuando haces el volcado en Twig, sí que se vuelca justo en el centro de la página + +Y aún más útil, sólo en Twig, puedes utilizar `dump()` sin argumentos. + +[[[ code('43c4067dd7') ]]] + +Esto volcará todas las variables a las que tengamos acceso. Así que aquí está la variable `title`,`tracks` y, ¡sorpresa! Hay una tercera variable llamada `app`. Es una variable global que tenemos en todas las plantillas... y nos da acceso a cosas como la sesión y los datos del usuario. Y... ¡lo hemos descubierto por curiosidad! + +Así que ahora que tenemos estas increíbles herramientas de depuración, pasemos a nuestro siguiente trabajo... que es hacer este sitio menos feo. ¡Es hora de añadir CSS y un diseño adecuado para dar vida a nuestro sitio! diff --git a/sfcasts/ep1/es/profiler.vtt b/sfcasts/ep1/es/profiler.vtt new file mode 100644 index 0000000..d86bce0 --- /dev/null +++ b/sfcasts/ep1/es/profiler.vtt @@ -0,0 +1,359 @@ +WEBVTT + +00:00:01.176 --> 00:00:03.606 align:middle +Es hora de instalar nuestro segundo paquete. + +00:00:03.906 --> 00:00:05.686 align:middle +Y éste es divertido. Vamos a + +00:00:06.206 --> 00:00:11.266 align:middle +confirmar nuestros cambios primero: así +será más fácil comprobar los cambios + +00:00:11.416 --> 00:00:14.126 align:middle +que hace la receta del nuevo paquete. + +00:00:15.006 --> 00:00:21.016 align:middle +Añade todo: Parece que está bien, así que... + +00:00:21.096 --> 00:00:24.966 align:middle +confirmar: Precioso. + +00:00:25.526 --> 00:00:34.686 align:middle +Ahora ejecuta: composer require debug Así +que sí, este es otro alias de Flex... + +00:00:35.116 --> 00:00:39.976 align:middle +y aparentemente es un alias +de symfony/debug-pack. + +00:00:40.536 --> 00:00:44.876 align:middle +Y sabemos que un paquete es una +colección de paquetes. Así que en + +00:00:45.356 --> 00:00:50.996 align:middle +lugar de añadir esta única línea a nuestro archivo +composer.json, si lo comprobamos, parece que ha + +00:00:51.156 --> 00:00:55.476 align:middle +añadido un nuevo paquete +arriba, en la sección require - + +00:00:55.696 --> 00:00:58.736 align:middle +esta es una biblioteca de registro - y... + +00:00:58.896 --> 00:01:05.796 align:middle +al final, ha añadido una nueva sección +require-dev con otras tres bibliotecas. + +00:01:06.566 --> 00:01:11.646 align:middle +La diferencia entre require y +require-dev no es demasiado importante: + +00:01:11.936 --> 00:01:17.636 align:middle +todos estos paquetes se descargaron en nuestra +aplicación, pero como mejor práctica, + +00:01:17.956 --> 00:01:22.466 align:middle +si instalas una biblioteca que sólo +está pensada para el desarrollo local, + +00:01:22.756 --> 00:01:24.766 align:middle +deberías ponerla en require-dev. + +00:01:25.446 --> 00:01:27.396 align:middle +¡El pack lo hizo por nosotros! + +00:01:27.646 --> 00:01:28.406 align:middle +¡Gracias pack! + +00:01:29.346 --> 00:01:33.946 align:middle +De vuelta al terminal, ¡esto +también instaló tres recetas! + +00:01:33.946 --> 00:01:36.986 align:middle +Ooh. Veamos qué han hecho. + +00:01:37.286 --> 00:01:42.846 align:middle +Limpio la pantalla y corro: git status +Así que esto me resulta familiar: + +00:01:43.206 --> 00:01:48.606 align:middle +modificó config/bundles.php +para activar tres nuevos bundles. + +00:01:49.016 --> 00:01:54.426 align:middle +De nuevo, los bundles son plugins de Symfony, +que añaden más funciones a nuestra aplicación. + +00:01:55.096 --> 00:02:00.186 align:middle +También ha añadido varios archivos de +configuración al directorio config/packages/. + +00:02:00.776 --> 00:02:05.636 align:middle +Hablaremos más de estos archivos en el +próximo tutorial, pero, a alto nivel, + +00:02:05.826 --> 00:02:08.866 align:middle +controlan el comportamiento de esos bundles. + +00:02:09.726 --> 00:02:12.466 align:middle +Entonces, ¿qué nos han +aportado estos nuevos paquetes? + +00:02:12.876 --> 00:02:17.096 align:middle +Para averiguarlo, dirígete a tu navegador +y actualiza la página de inicio. + +00:02:18.026 --> 00:02:19.746 align:middle +¡Santo cielo, Batman! + +00:02:20.106 --> 00:02:22.336 align:middle +Es la barra de herramientas de depuración web. + +00:02:22.886 --> 00:02:27.706 align:middle +Esto es una locura de depuración: una barra +de herramientas llena de buena información. + +00:02:28.206 --> 00:02:33.956 align:middle +A la izquierda, puedes ver el controlador al que +se ha llamado junto con el código de estado HTTP. + +00:02:34.616 --> 00:02:38.846 align:middle +También está la cantidad de tiempo que +tardó la página, la memoria que utilizó + +00:02:39.086 --> 00:02:45.526 align:middle +y también cuántas plantillas se renderizaron a +través de Twig: este es el bonito icono de Twig. En el + +00:02:46.256 --> 00:02:48.056 align:middle +lado derecho, tenemos detalles + +00:02:48.056 --> 00:02:51.696 align:middle +sobre el servidor web local Symfony que se +está ejecutando e información sobre PHP. + +00:02:53.006 --> 00:02:59.966 align:middle +Pero aún no has visto la mejor parte: haz clic en +cualquiera de estos iconos para saltar al perfilador. + +00:03:00.846 --> 00:03:02.956 align:middle +Esta es la barra de herramientas +de depuración web... + +00:03:03.196 --> 00:03:04.186 align:middle +que se ha vuelto loca. Está + +00:03:04.636 --> 00:03:11.116 align:middle +llena de datos sobre esa petición , como la petición +y la respuesta, todos los mensajes de registro + +00:03:11.116 --> 00:03:17.706 align:middle +que se produjeron durante esa petición, información +sobre las rutas y la ruta a la que se respondió, Twig + +00:03:17.996 --> 00:03:22.786 align:middle +te muestra qué plantillas se renderizaron +y cuántas veces se renderizaron... + +00:03:23.406 --> 00:03:26.636 align:middle +y hay información de +configuración aquí abajo. + +00:03:26.936 --> 00:03:31.866 align:middle +¡Uf! Pero mi sección +favorita es la de Rendimiento. + +00:03:32.546 --> 00:03:36.556 align:middle +Muestra una línea de tiempo de todo lo +que ha ocurrido durante la petición. + +00:03:37.146 --> 00:03:39.186 align:middle +Esto es genial por dos razones. + +00:03:39.726 --> 00:03:44.766 align:middle +La primera es bastante obvia: puedes usarla para +encontrar qué partes de tu página son lentas. + +00:03:45.246 --> 00:03:49.586 align:middle +Así, por ejemplo, nuestro +controlador tardó 20,4 milisegundos. + +00:03:49.926 --> 00:03:56.276 align:middle +Y dentro de la ejecución del controlador, la plantilla +de la página de inicio se renderizó en 3, 9 milisegundos + +00:03:56.396 --> 00:04:00.716 align:middle +y base.html.twig se renderizó +en 2,8 milisegundos. + +00:04:01.566 --> 00:04:07.496 align:middle +La segunda razón por la que esto es realmente genial +es que descubre todas las capas ocultas de Symfony. + +00:04:08.526 --> 00:04:10.576 align:middle +Ajusta este umbral a cero. + +00:04:11.936 --> 00:04:17.196 align:middle +Antes, esto sólo mostraba las cosas +que tardaban más de un milisegundo. + +00:04:17.566 --> 00:04:19.916 align:middle +Ahora lo muestra todo. No tienes + +00:04:20.506 --> 00:04:25.886 align:middle +que preocuparte por la gran mayoría de +las cosas, pero es superguay ver las capas + +00:04:25.886 --> 00:04:30.086 align:middle +de Symfony: las cosas que ocurren antes + +00:04:30.086 --> 00:04:32.926 align:middle +y después de que se ejecute tu controlador. + +00:04:33.886 --> 00:04:39.636 align:middle +Tenemos un tutorial de inmersión profunda para +Symfony si quieres aprender más sobre estas cosas. + +00:04:39.706 --> 00:04:43.636 align:middle +La barra de herramientas de depuración web y el +perfilador también crecerán con nuestra aplicación. + +00:04:44.176 --> 00:04:48.856 align:middle +En un futuro tutorial, cuando instalemos una +librería para hablar con la base de datos, + +00:04:49.306 --> 00:04:54.376 align:middle +de repente tendremos una nueva sección que +enumera todas las consultas a la base de datos + +00:04:54.376 --> 00:04:57.536 align:middle +que ha hecho una página y el +tiempo que ha tardado cada una. + +00:04:58.906 --> 00:05:02.566 align:middle +Bien, el paquete de depuración instaló la +barra de herramientas de depuración web. + +00:05:03.016 --> 00:05:07.146 align:middle +También ha instalado una biblioteca de +registro que utilizaremos más adelante. + +00:05:07.526 --> 00:05:13.116 align:middle +Y ha instalado un paquete que nos proporciona +dos fantásticas funciones de depuración. + +00:05:14.306 --> 00:05:15.896 align:middle +Dirígete a VinylController. + +00:05:17.156 --> 00:05:20.206 align:middle +Imagina que estamos haciendo +un desarrollo y necesitamos + +00:05:20.206 --> 00:05:22.976 align:middle +ver cómo es esta variable $tracks. + +00:05:23.426 --> 00:05:27.156 align:middle +En este caso es bastante +obvio, pero a veces querrás + +00:05:27.156 --> 00:05:30.326 align:middle +ver lo que hay dentro de un objeto complejo. + +00:05:31.356 --> 00:05:37.206 align:middle +Para ello, digamos dd($tracks), +donde "dd" significa volcar y morir. + +00:05:37.506 --> 00:05:39.896 align:middle +Así que si refrescamos... + +00:05:40.886 --> 00:05:44.176 align:middle +¡sí! Eso vuelca la +variable y mata la página. + +00:05:44.716 --> 00:05:49.766 align:middle +Y esto es mucho más potente -y +más bonito- que usar var_dump(): + +00:05:50.466 --> 00:05:54.506 align:middle +podemos ampliar las secciones y ver los +datos en profundidad con mucha facilidad. + +00:05:54.506 --> 00:05:58.256 align:middle +En lugar de dd(), también +puedes utilizar dump(). + +00:05:58.776 --> 00:06:00.576 align:middle +para volcar y vivir. + +00:06:01.456 --> 00:06:04.286 align:middle +Pero esto podría no aparecer donde esperas. En + +00:06:04.886 --> 00:06:06.746 align:middle +lugar de imprimirse en el centro de la página, + +00:06:07.216 --> 00:06:11.756 align:middle +aparece abajo en la barra de herramientas de +depuración de la web, bajo el icono del objetivo. + +00:06:12.476 --> 00:06:17.266 align:middle +Si es demasiado pequeño, haz clic para ver +una versión más grande en el perfilador. + +00:06:18.716 --> 00:06:21.046 align:middle +También puedes utilizar este dump() en Twig. + +00:06:22.016 --> 00:06:23.406 align:middle +Elimina el volcado del controlador... + +00:06:23.866 --> 00:06:28.776 align:middle +y luego en la plantilla, justo +antes del ul, dump(tracks). + +00:06:31.136 --> 00:06:32.116 align:middle +Y esto... + +00:06:32.466 --> 00:06:34.256 align:middle +es exactamente igual. + +00:06:34.596 --> 00:06:39.986 align:middle +Excepto que cuando haces el volcado en +Twig , lo hace justo en medio de la página + +00:06:41.006 --> 00:06:47.506 align:middle +Y aún más útil, sólo en Twig , +puedes utilizar dump() sin argumentos. + +00:06:47.976 --> 00:06:51.116 align:middle +Esto volcará todas las variables +a las que tengamos acceso. + +00:06:51.836 --> 00:06:55.966 align:middle +Así que aquí está la variable +title, tracks y, ¡sorpresa! + +00:06:56.156 --> 00:06:58.956 align:middle +Hay una tercera variable llamada app. + +00:06:59.446 --> 00:07:03.296 align:middle +Esta es una variable global que +tenemos en todas las plantillas... + +00:07:03.456 --> 00:07:07.366 align:middle +y nos da acceso a cosas como la +sesión y los datos del usuario. + +00:07:07.856 --> 00:07:11.536 align:middle +Y... ¡lo hemos descubierto por curiosidad! + +00:07:12.166 --> 00:07:17.706 align:middle +Así que ahora que tenemos estas increíbles herramientas +de depuración, pasemos a nuestro siguiente trabajo... + +00:07:18.166 --> 00:07:21.046 align:middle +que es hacer este sitio menos feo. + +00:07:21.676 --> 00:07:28.026 align:middle +¡Es hora de añadir CSS y un diseño +adecuado para dar vida a nuestro sitio! diff --git a/sfcasts/ep1/es/route-controller.md b/sfcasts/ep1/es/route-controller.md new file mode 100644 index 0000000..6c8a439 --- /dev/null +++ b/sfcasts/ep1/es/route-controller.md @@ -0,0 +1,77 @@ +# Rutas, controladores y respuestas + +Tengo que decir que echo de menos los años 90. Bueno, no los beanie babies y... definitivamente no la forma de vestir de entonces, pero... las cintas de mezclas. Si no eras un niño en los 80 o los 90, quizá no sepas lo difícil que era compartir tus canciones favoritas con tus amigos. Oh sí, estoy hablando de un mashup de Michael Jackson, Phil Collins y Paula Abdul. La perfección. + +Para aprovechar esa nostalgia, pero con un toque hipster, vamos a crear una nueva aplicación llamada Mixed Vinyl: una tienda en la que los usuarios pueden crear cintas de mezclas, con Boyz || Men, Mariah Carey y Smashing Pumpkins... sólo que prensadas en un disco de vinilo. Hmm, puede que tenga que poner un tocadiscos en mi coche. + +La página que estamos viendo, que es súper bonita y cambia de color cuando refrescamos... no es una página real. Es sólo una forma de que Symfony nos diga "hola" y nos enlace a la documentación. Y por cierto, la documentación de Symfony es genial, así que no dudes en consultarla mientras aprendes. + +## Rutas y controladores + +Vale: todo framework web en cualquier lenguaje tiene el mismo trabajo: ayudarnos a crear páginas, ya sean páginas HTML, respuestas JSON de la API o arte ASCII. Y casi todos los marcos lo hacen de la misma manera: mediante un sistema de rutas y controladores. La ruta define la URL de la página y apunta a un controlador. El controlador es una función PHP que construye esa página. + +Así que ruta + controlador = página. Son matemáticas, gente. + +## Crear el controlador + +Vamos a construir estas dos cosas... un poco al revés. Así que primero, vamos a crear la función del controlador. En Symfony, la función del controlador es siempre un método dentro de una clase PHP. Te lo mostraré: en el directorio `src/Controller/`, crea una nueva clase PHP. Vamos a llamarla `VinylController`, pero el nombre puede ser cualquier cosa. + +[[[ code('6709e819e9') ]]] + +Y, ¡felicidades! ¡Es nuestra primera clase PHP! ¿Y adivina dónde vive? En el directorio `src/`, donde vivirán todas las clases PHP. Y en general, no importa cómo organices las cosas dentro de `src/`: normalmente puedes poner las cosas en el directorio que quieras y nombrar las clases como quieras. Así que da rienda suelta a tu creatividad. + +***TIP +En realidad, los controladores deben vivir en `src/Controller/`, a menos que cambies alguna configuración. La mayoría de las clases de PHP pueden vivir en cualquier lugar de `src/`. +*** + +Pero hay dos reglas importantes. En primer lugar, fíjate en el espacio de nombres que PhpStorm ha añadido sobre la clase: `App\Controller`. Independientemente de cómo decidas organizar tu directorio `src/`, el espacio de nombres de una clase debe coincidir con la estructura del directorio... empezando por `App`. Puedes imaginar que el espacio de nombres `App\` apunta al directorio`src/`. Entonces, si pones un archivo en un subdirectorio `Controller/`, necesita una parte `Controller` en su espacio de nombres. + +Si alguna vez metes la pata, por ejemplo, si escribes algo mal o te olvidas de esto, lo vas a pasar mal. PHP no podrá encontrar la clase: obtendrás un error de "clase no encontrada". Ah, y la otra regla es que el nombre de un archivo debe coincidir con el nombre de la clase dentro de él, más `.php`. Por lo tanto, `VinylController.php`. Seguiremos esas dos reglas para todos los archivos que creemos en `src/`. + +## Crear el controlador + +Volvemos a nuestra tarea de crear una función de controlador. Dentro, añade un nuevo método público llamado `homepage()`. Y no, el nombre de este método tampoco importa: prueba a ponerle el nombre de tu gato: ¡funcionará! + +Por ahora, sólo voy a poner una declaración `die()` con un mensaje. + +[[[ code('66d737c13c') ]]] + +## Crear la ruta + +¡Buen comienzo! Ahora que tenemos una función de controlador, vamos a crear una ruta, que define la URL de nuestra nueva página y apunta a este controlador. Hay varias formas de crear rutas en Symfony, pero casi todo el mundo utiliza atributos. + +Así es como funciona. Justo encima de este método, decimos `#[]`. Esta es la sintaxis de atributos de PHP 8, que es una forma de añadir configuración a tu código. Empieza a escribir `Route`. Pero antes de que termines, fíjate en que PhpStorm lo está autocompletando. Pulsa el tabulador para dejar que termine. + +Eso, muy bien, completó la palabra `Route` para mí. Pero lo más importante es que ha añadido una declaración `use` en la parte superior. Siempre que utilices un atributo, debes tener una declaración `use` correspondiente en la parte superior del archivo. + +Dentro de `Route`, pasa `/`, que será la URL de nuestra página. + +[[[ code('ffeff58236') ]]] + +Y... ¡listo! Esta ruta define la URL y apunta a este controlador... simplemente porque está justo encima de este controlador. + +¡Vamos a probarlo! Refresca y... ¡felicidades! ¡Symfony miró la URL, vio que coincidía con la ruta - `/` o sin barra es lo mismo para la página de inicio - ejecutó nuestro controlador y golpeó la declaración `die`! + +Ah, y por cierto, sigo diciendo función del controlador. Comúnmente se llama simplemente "controlador" o "acción"... sólo para confundir. + +## Devolver una respuesta + +Bien, dentro del controlador -o acción- podemos escribir el código que queramos para construir la página, como hacer consultas a la base de datos, llamadas a la API, renderizar una plantilla, lo que sea. Al final vamos a hacer todo eso. + +Lo único que le importa a Symfony es que tu controlador devuelva un objeto`Response`. Compruébalo: escribe `return` y luego empieza a escribir `Response`. Woh: hay bastantes clases `Response` ya en nuestro código... ¡y dos son de Symfony! Queremos la de HTTP foundation. HTTP foundation es una de esas librerías de Symfony... y nos da bonitas clases para cosas como la Petición, la Respuesta y la Sesión. Pulsa el tabulador para autocompletar y termina eso. + +Oh, debería haber dicho devolver una nueva respuesta. Así está mejor. Ahora dale al tabulador. Cuando dejé que `Response` autocompletara la primera vez, muy importante, PhpStorm añadió esta declaración de uso en la parte superior. Cada vez que hagamos referencia a una clase o interfaz, tendremos que añadir una sentencia `use` al principio del archivo en el que estemos trabajando. + +Al dejar que PhpStorm autocompletara eso por mí, añadió la declaración `use` automáticamente. Lo haré cada vez que haga referencia a una clase. Ah, y si todavía eres un poco nuevo en lo que respecta a los espacios de nombres de PHP y las declaraciones `use`, echa un vistazo a nuestro breve y gratuito tutorial sobre espacios de nombres de PHP. + +De todos modos, dentro de `Response`, podemos poner lo que queramos devolver al usuario: HTML, JSON o, por ahora, un simple mensaje, como el título del vinilo Mixto en el que estamos trabajando: PB y jams. + +[[[ code('6750c9c02f') ]]] + +Bien, equipo, ¡vamos a ver qué pasa! Actualiza y... ¡PB y mermeladas! Puede que no parezca gran cosa, ¡pero acabamos de construir nuestra primera página Symfony totalmente funcional! ¡Ruta + controlador = beneficio! + +Y acabas de aprender la parte más fundamental de Symfony... y sólo estamos empezando. Ah, y como nuestros controladores siempre devuelven un objeto `Response`, es opcional, pero puedes añadir un tipo de retorno a esta función si lo deseas. Pero eso no cambia nada: sólo es una forma agradable de codificar. + +[[[ code('32c60928e5') ]]] + +A continuación me siento bastante seguro. Así que vamos a crear otra página, pero con una ruta mucho más elegante que coincide con un patrón comodín. diff --git a/sfcasts/ep1/es/route-controller.vtt b/sfcasts/ep1/es/route-controller.vtt new file mode 100644 index 0000000..4ac6b85 --- /dev/null +++ b/sfcasts/ep1/es/route-controller.vtt @@ -0,0 +1,445 @@ +WEBVTT + +00:00:01.246 --> 00:00:03.576 align:middle +Tengo que decir que echo de menos los años 90. + +00:00:04.076 --> 00:00:06.556 align:middle +Bueno, no los beanie babies y... + +00:00:06.556 --> 00:00:09.286 align:middle +definitivamente no la forma +de vestir de entonces, pero... + +00:00:09.566 --> 00:00:10.876 align:middle +las cintas de mezclas. + +00:00:11.536 --> 00:00:15.746 align:middle +Si no eras un niño en los 80 o los +90, quizá no sepas lo difícil que era + +00:00:15.746 --> 00:00:18.786 align:middle +compartir tus canciones +favoritas con tus amigos. + +00:00:19.386 --> 00:00:24.846 align:middle +Oh sí, estoy hablando de un mashup de +Michael Jackson, Phil Collins y Paula Abdul. + +00:00:25.056 --> 00:00:25.886 align:middle +La perfección. + +00:00:26.816 --> 00:00:31.406 align:middle +Para aprovechar esa nostalgia, +pero con un toque hipster, vamos a + +00:00:31.976 --> 00:00:35.306 align:middle +crear una nueva aplicación llamada Mixed Vinyl + +00:00:35.726 --> 00:00:40.416 align:middle +una tienda en la que los usuarios pueden +crear cintas de mezcla, con Boyz || Men, + +00:00:40.556 --> 00:00:42.856 align:middle +Mariah Carey y Smashing Pumpkins... + +00:00:43.136 --> 00:00:45.846 align:middle +sólo que prensadas en un disco de vinilo. + +00:00:46.496 --> 00:00:49.646 align:middle +Hmm, puede que tenga que poner +un tocadiscos en mi coche. + +00:00:50.666 --> 00:00:55.756 align:middle +La página que estamos viendo, que es súper +bonita y cambia de color cuando refrescamos... + +00:00:56.156 --> 00:00:57.946 align:middle +no es una página real. + +00:00:58.186 --> 00:01:02.916 align:middle +Es sólo una forma de que Symfony nos diga +"hola" y nos enlace a la documentación. + +00:01:03.406 --> 00:01:06.986 align:middle +Y por cierto, la documentación +de Symfony es estupenda, + +00:01:07.096 --> 00:01:09.666 align:middle +así que no dudes en +consultarla mientras aprendes. + +00:01:10.996 --> 00:01:17.996 align:middle +Vale: todo framework web en cualquier lenguaje +tiene el mismo trabajo: ayudarnos a crear páginas, + +00:01:18.356 --> 00:01:24.076 align:middle +ya sean páginas HTML, +respuestas API JSON o arte ASCII. + +00:01:24.596 --> 00:01:30.936 align:middle +Y casi todos los marcos lo hacen de la misma manera: +mediante un sistema de rutas y controladores. + +00:01:31.666 --> 00:01:35.586 align:middle +La ruta define la URL de la +página y apunta a un controlador. + +00:01:36.236 --> 00:01:40.216 align:middle +El controlador es una función +PHP que construye esa página. + +00:01:40.766 --> 00:01:43.736 align:middle +Así que ruta + controlador = página. + +00:01:44.106 --> 00:01:45.206 align:middle +Son matemáticas, gente. + +00:01:46.146 --> 00:01:48.016 align:middle +Vamos a construir estas dos cosas... + +00:01:48.186 --> 00:01:50.036 align:middle +más o menos a la inversa. + +00:01:50.396 --> 00:01:53.336 align:middle +Así que primero, vamos a crear +la función del controlador. + +00:01:54.156 --> 00:01:59.986 align:middle +En Symfony, la función del controlador es +siempre un método dentro de una clase PHP. + +00:02:00.736 --> 00:02:07.266 align:middle +Te lo mostraré: en el directorio +src/Controller/, crea una nueva clase PHP. + +00:02:07.326 --> 00:02:11.206 align:middle +Vamos a llamarla VinylController, pero +el nombre puede ser cualquier cosa. + +00:02:12.256 --> 00:02:13.956 align:middle +Y, ¡felicidades! + +00:02:14.126 --> 00:02:16.066 align:middle +¡Es nuestra primera clase PHP! + +00:02:16.476 --> 00:02:18.276 align:middle +¿Y adivina dónde vive? + +00:02:18.616 --> 00:02:23.686 align:middle +En el directorio src/, donde +vivirán todas las clases PHP. + +00:02:23.686 --> 00:02:28.876 align:middle +Y, en general, no importa cómo +organices las cosas dentro de src/: + +00:02:29.266 --> 00:02:32.796 align:middle +normalmente puedes poner las +cosas en el directorio que quieras + +00:02:32.796 --> 00:02:34.896 align:middle +y nombrar las clases como quieras. + +00:02:35.106 --> 00:02:37.056 align:middle +Así que da rienda suelta a tu creatividad. + +00:02:37.566 --> 00:02:39.766 align:middle +Pero hay dos reglas importantes. + +00:02:39.766 --> 00:02:46.716 align:middle +En primer lugar, fíjate en el espacio de nombres que +PhpStorm ha añadido sobre la clase: App\Controller. + +00:02:47.326 --> 00:02:50.786 align:middle +Sea como sea que decidas +organizar tu directorio src/, + +00:02:51.036 --> 00:02:55.346 align:middle +el espacio de nombres de una clase debe +coincidir con la estructura del directorio... + +00:02:55.536 --> 00:02:56.776 align:middle +empezando por App. + +00:02:57.556 --> 00:03:01.316 align:middle +Puedes imaginar que el espacio de +nombres App\ apunta al directorio src/. + +00:03:02.376 --> 00:03:06.046 align:middle +Entonces, si pones un archivo +en un subdirectorio Controller/, + +00:03:06.266 --> 00:03:09.326 align:middle +necesita una parte Controller +en su espacio de nombres. + +00:03:09.966 --> 00:03:13.386 align:middle +Si alguna vez metes la pata, por +ejemplo, si escribes algo mal + +00:03:13.416 --> 00:03:16.156 align:middle +o te olvidas de esto, lo vas a pasar mal. + +00:03:16.296 --> 00:03:22.286 align:middle +PHP no podrá encontrar la clase: obtendrás +un error de "clase no encontrada". + +00:03:23.156 --> 00:03:26.016 align:middle +Ah, y la otra regla es que el nombre de + +00:03:26.016 --> 00:03:31.516 align:middle +un archivo debe coincidir con el nombre +de la clase dentro de él, más .php. + +00:03:31.516 --> 00:03:34.886 align:middle +Por lo tanto, VinylController.php. + +00:03:34.886 --> 00:03:39.016 align:middle +Seguiremos esas dos reglas para todos +los archivos que creemos en src/. + +00:03:39.706 --> 00:03:42.296 align:middle +Volvamos a nuestra tarea de crear +una función de controlador. + +00:03:43.356 --> 00:03:46.596 align:middle +Dentro, añade un nuevo método +público llamado homepage(). + +00:03:47.316 --> 00:03:50.676 align:middle +Y no, el nombre de este +método tampoco importa: + +00:03:51.256 --> 00:03:54.026 align:middle +prueba a ponerle el nombre +de tu gato: ¡funcionará! + +00:03:54.106 --> 00:03:57.936 align:middle +Por ahora, sólo voy a poner una +declaración die() con un mensaje. + +00:04:01.006 --> 00:04:01.876 align:middle +¡Buen comienzo! + +00:04:02.646 --> 00:04:08.146 align:middle +Ahora que tenemos una función de controlador, +vamos a crear una ruta, que define la URL + +00:04:08.146 --> 00:04:11.306 align:middle +de nuestra nueva página y +apunta a este controlador. + +00:04:12.146 --> 00:04:18.836 align:middle +Hay varias formas de crear rutas en Symfony, +pero casi todo el mundo utiliza atributos. + +00:04:18.906 --> 00:04:19.956 align:middle +Así es como funciona. + +00:04:20.556 --> 00:04:22.126 align:middle +Justo encima de este método, decimos #[]. + +00:04:22.126 --> 00:04:32.386 align:middle +Esta es la sintaxis de atributos de PHP 8, que es +una forma de añadir configuración a tu código. + +00:04:32.386 --> 00:04:34.976 align:middle +Empieza a escribir Route. + +00:04:35.286 --> 00:04:40.006 align:middle +Pero antes de que termines, observa +que PhpStorm lo autocompleta. + +00:04:40.756 --> 00:04:42.886 align:middle +Pulsa el tabulador para dejar que termine. + +00:04:43.636 --> 00:04:46.766 align:middle +Eso, muy bien, completó +la palabra Route para mí. + +00:04:47.246 --> 00:04:52.066 align:middle +Pero lo más importante es que ha añadido +una declaración use en la parte superior. + +00:04:52.846 --> 00:04:57.386 align:middle +Siempre que utilices un atributo, debes tener una +declaración use correspondiente en la parte superior + +00:04:57.426 --> 00:04:58.916 align:middle +del archivo. + +00:04:59.756 --> 00:05:04.496 align:middle +Dentro de Route, pasa /, que +será la URL de nuestra página. + +00:05:05.076 --> 00:05:06.396 align:middle +Y... ¡listo! + +00:05:06.896 --> 00:05:10.716 align:middle +Esta ruta define la URL y +apunta a este controlador... + +00:05:11.086 --> 00:05:13.936 align:middle +simplemente porque está justo +encima de este controlador. + +00:05:15.096 --> 00:05:16.156 align:middle +¡Vamos a probarlo! + +00:05:17.036 --> 00:05:18.086 align:middle +Refresca y... + +00:05:18.816 --> 00:05:20.346 align:middle +¡felicidades! ¡ + +00:05:20.586 --> 00:05:26.726 align:middle +Symfony miró la URL, vio que coincidía +con la ruta - / o sin barra es lo mismo + +00:05:26.726 --> 00:05:31.176 align:middle +para la página de inicio - ejecutó nuestro +controlador y golpeó la declaración die ! + +00:05:31.776 --> 00:05:35.206 align:middle +Ah, y por cierto, sigo diciendo +función del controlador. + +00:05:35.556 --> 00:05:39.136 align:middle +Comúnmente se llama simplemente +"controlador" o "acción"... + +00:05:39.366 --> 00:05:40.656 align:middle +sólo para confundir las cosas. + +00:05:41.536 --> 00:05:47.706 align:middle +Vale, pues dentro del controlador -o acción- +podemos escribir el código que queramos + +00:05:47.806 --> 00:05:54.396 align:middle +para construir la página, como hacer consultas a la base de +datos, llamadas a la API, renderizar una plantilla, lo que sea. + +00:05:54.836 --> 00:05:57.386 align:middle +Todo eso lo vamos a hacer eventualmente. + +00:05:58.076 --> 00:06:03.876 align:middle +Lo único que le importa a Symfony es que +tu controlador devuelva un objeto Response. + +00:06:04.776 --> 00:06:10.376 align:middle +Compruébalo: escribe return y +luego empieza a escribir Response. + +00:06:10.406 --> 00:06:15.616 align:middle +Woh: ya hay unas cuantas clases +Response en nuestro código... + +00:06:15.866 --> 00:06:17.546 align:middle +¡y dos son de Symfony! + +00:06:18.296 --> 00:06:20.886 align:middle +Queremos la de HTTP foundation. + +00:06:21.566 --> 00:06:25.616 align:middle +HTTP foundation es una de +esas bibliotecas de Symfony... + +00:06:25.696 --> 00:06:30.376 align:middle +y nos proporciona bonitas clases para cosas +como la Petición, la Respuesta y la Sesión. + +00:06:31.206 --> 00:06:35.056 align:middle +Pulsa el tabulador para +autocompletar y termina eso. + +00:06:35.056 --> 00:06:37.246 align:middle +Oh, debería haber dicho +devolver una nueva respuesta. + +00:06:38.676 --> 00:06:39.406 align:middle +Así está mejor. + +00:06:40.186 --> 00:06:41.246 align:middle +Ahora dale al tabulador. + +00:06:42.326 --> 00:06:47.026 align:middle +Cuando dejé que Response autocompletara +la primera vez, muy importante, + +00:06:47.086 --> 00:06:50.366 align:middle +PhpStorm añadió esta declaración +de uso en la parte superior. + +00:06:51.236 --> 00:06:56.596 align:middle +Cada vez que hagamos referencia a una clase o +interfaz, tendremos que añadir una sentencia use al + +00:06:56.596 --> 00:06:58.676 align:middle +principio del archivo en +el que estemos trabajando. + +00:06:59.496 --> 00:07:04.626 align:middle +Al dejar que PhpStorm autocompletara eso por mí, +añadió la declaración use automáticamente. + +00:07:05.196 --> 00:07:08.456 align:middle +Lo haré cada vez que haga +referencia a una clase. + +00:07:09.156 --> 00:07:13.626 align:middle +Ah, y si todavía eres un poco nuevo en lo que respecta a los +espacios de nombres de PHP y las declaraciones use, echa un + +00:07:13.886 --> 00:07:18.146 align:middle +vistazo a nuestro breve y gratuito +tutorial sobre espacios de nombres de PHP. + +00:07:19.396 --> 00:07:27.436 align:middle +De todos modos, dentro de Response, podemos poner +lo que queramos devolver al usuario: HTML, JSON o, + +00:07:27.646 --> 00:07:35.466 align:middle +por ahora, un simple mensaje, como el título del +vinilo mixto en el que estamos trabajando: PB y jams. + +00:07:37.576 --> 00:07:39.806 align:middle +Bien, equipo, ¡vamos a ver qué pasa! + +00:07:39.806 --> 00:07:42.056 align:middle +Actualiza y... + +00:07:42.446 --> 00:07:43.836 align:middle +¡PB y mermeladas! + +00:07:44.446 --> 00:07:50.636 align:middle +Puede que no parezca gran cosa, ¡pero acabamos de construir +nuestra primera página Symfony totalmente funcional! + +00:07:51.006 --> 00:07:53.456 align:middle +¡Ruta + controlador = beneficio! + +00:07:53.806 --> 00:07:57.836 align:middle +Y acabas de aprender la parte +más fundamental de Symfony... + +00:07:58.176 --> 00:07:59.716 align:middle +y sólo estamos empezando. + +00:08:00.226 --> 00:08:04.926 align:middle +Ah, y como nuestros controladores siempre +devuelven un objeto Response, es opcional, + +00:08:05.236 --> 00:08:08.996 align:middle +pero puedes añadir un tipo de +retorno a esta función si lo deseas. + +00:08:09.446 --> 00:08:13.096 align:middle +Pero eso no cambia nada: sólo es +una forma agradable de codificar. + +00:08:14.266 --> 00:08:16.426 align:middle +A continuación me siento bastante seguro. + +00:08:16.496 --> 00:08:23.126 align:middle +Así que vamos a crear otra página, pero con una ruta +mucho más elegante que coincide con un patrón comodín diff --git a/sfcasts/ep1/es/route-requirements.md b/sfcasts/ep1/es/route-requirements.md new file mode 100644 index 0000000..fb50d7a --- /dev/null +++ b/sfcasts/ep1/es/route-requirements.md @@ -0,0 +1,71 @@ +# Rutas inteligentes: Sólo GET y Validar {Comodines} + +Ahora que tenemos una nueva página, en tu terminal, ejecuta de nuevo `debug:router`. + +```terminal-silent +php bin/console debug:router +``` + +Sí, ¡ahí está nuestra nueva ruta! Observa que la tabla tiene una columna llamada "Método" que dice "cualquiera". Esto significa que puedes hacer una petición a esta URL utilizando cualquier método HTTP -como GET o POST- y coincidirá con esa ruta. + +## Restringir las rutas sólo a GET o POST + +Pero el objetivo de nuestra nueva ruta API es permitir a los usuarios hacer una petición GET para obtener datos de la canción. Técnicamente, ahora mismo, también podrías hacer una petición POST a esto... y funcionaría perfectamente. Puede que no nos importe, pero a menudo con las APIs, querrás restringir una ruta para que sólo funcione con un método específico como GET, POST o PUT. ¿Podemos hacer que esta ruta, de alguna manera, sólo funcione con peticiones GET? + +Sí! Añadiendo otra opción a la `Route`. En este caso, se llama `methods`, ¡incluso se autocompleta! Establece esto como un array y, pon `GET`. + +[[[ code('cbd8c30440') ]]] + +Voy a mantener pulsado Comando y a hacer clic en la clase `Route` de nuevo... para que podamos ver que... ¡sí! `methods` es uno de los argumentos. + +Volvemos a `debug:router`: + +```terminal-silent +php bin/console debug:router +``` + +Bien. La ruta ahora sólo coincidirá con las peticiones GET. Es... un poco difícil probar esto, ya que un navegador siempre hace peticiones GET si vas directamente a una URL... pero aquí es donde otro comando de `bin/console` resulta útil: `router:match`. + +Si lo ejecutamos sin argumentos + +```terminal-silent +php bin/console router:match +``` + +Nos da un error, ¡pero muestra cómo se utiliza! Inténtalo: + +```terminal +php bin/console router:match /api/songs/11 +``` + +Y... ¡eso coincide con nuestra nueva ruta! Pero ahora pregúntate qué pasaría si hiciéramos una petición POST a esa URL con `--method=POST`: + +```terminal-silent +php bin/console router:match /api/songs/11 --method=POST +``` + +¡Ninguna ruta coincide con esta ruta con ese método! Pero dice que casi coincide con nuestra ruta. + +## Restringir los comodines de ruta mediante Regex + +Vamos a hacer una cosa más para restringir nuestra nueva ruta. Voy a añadir una pista de tipo `int` al argumento `$id`. + +[[[ code('eb386d1bd6') ]]] + +Eso... no cambia nada, excepto que ahora PHP tomará la cadena `id` de la URL que Symfony pasa a este método y la convertirá en un `int`, lo cual es... agradable porque entonces estamos tratando con un verdadero número entero en nuestro código. + +Puedes ver la sutil diferencia en la respuesta. Ahora mismo, el campo `id` es una cadena. Cuando actualizamos, `id` es ahora un número verdadero en JSON. + +Pero... si alguien se hiciera el remolón... y pasara a `/api/songs/apple`... ¡vaya! ¡Un error PHP, que, en producción, sería una página de error 500! Eso no me gusta. + +Pero... ¿qué podemos hacer? El error se produce cuando Symfony intenta llamar a nuestro controlador y le pasa ese argumento. Así que no podemos poner código en el controlador para comprobar si `$id` es un número: ¡es demasiado tarde! + +¿Y si, en cambio, pudiéramos decirle a Symfony que esta ruta sólo debe coincidir si el comodín `id` es un número? ¿Es posible? Totalmente + +Por defecto, cuando tienes un comodín, coincide con cualquier cosa. Pero puedes cambiarlo para que coincida con una expresión regular personalizada. Dentro de las llaves, justo después del nombre, añade un `<`, luego `>` y, entre medias, `\d+`. Es una expresión regular que significa "un dígito de cualquier longitud". + +[[[ code('b28748cdaa') ]]] + +¡Pruébalo! Actualiza y... ¡sí! A 404. No se ha encontrado ninguna ruta: simplemente no ha coincidido con esta ruta. Un 404 está muy bien... pero un error 500... eso es algo que queremos evitar. Y si volvemos a `/api/songs/5`... eso sigue funcionando. + +A continuación: si me preguntaras cuál es la parte más central e importante de Symfony, no lo dudaría: son los servicios. Descubramos qué es un servicio y cómo es la clave para liberar el potencial de Symfony. diff --git a/sfcasts/ep1/es/route-requirements.vtt b/sfcasts/ep1/es/route-requirements.vtt new file mode 100644 index 0000000..a9e19ac --- /dev/null +++ b/sfcasts/ep1/es/route-requirements.vtt @@ -0,0 +1,224 @@ +WEBVTT + +00:00:01.126 --> 00:00:06.836 align:middle +Ahora que tenemos una nueva página, en tu +terminal, ejecuta de nuevo debug:router. + +00:00:07.606 --> 00:00:10.046 align:middle +Sí, ¡ahí está nuestra nueva ruta! + +00:00:10.886 --> 00:00:15.526 align:middle +Observa que la tabla tiene una columna +llamada "Método" que dice "cualquiera". + +00:00:16.206 --> 00:00:22.146 align:middle +Esto significa que puedes hacer una petición +a esta URL utilizando cualquier método HTTP + +00:00:22.226 --> 00:00:26.046 align:middle +-como GET o POST- y coincidirá con esa ruta. + +00:00:26.566 --> 00:00:30.786 align:middle +Pero el objetivo de nuestra nueva ruta API es +permitir a los usuarios hacer una petición + +00:00:30.786 --> 00:00:34.326 align:middle +GET para obtener datos de la canción. + +00:00:35.016 --> 00:00:40.016 align:middle +Técnicamente, ahora mismo, también +podrías hacer una petición POST a este... + +00:00:40.016 --> 00:00:41.596 align:middle +y funcionaría perfectamente. + +00:00:42.146 --> 00:00:49.266 align:middle +Puede que no nos importe, pero a menudo con las APIs, +querrás restringir una ruta para que sólo funcione + +00:00:49.266 --> 00:00:52.906 align:middle +con un método específico +como GET, POST o PUT. + +00:00:53.836 --> 00:00:57.786 align:middle +¿Podemos hacer que esta ruta, de alguna +manera, sólo funcione con peticiones GET? + +00:00:58.256 --> 00:01:01.496 align:middle +Sí, añadiendo otra opción a Route. + +00:01:01.496 --> 00:01:06.546 align:middle +En este caso, se llama methods, +¡incluso se autocompleta! + +00:01:06.976 --> 00:01:09.836 align:middle +Establece esto como un array y, pon GET. + +00:01:09.906 --> 00:01:13.896 align:middle +Voy a mantener pulsado Comando y hacer +clic en la clase Route de nuevo... + +00:01:14.306 --> 00:01:15.866 align:middle +para que podamos ver que... + +00:01:16.216 --> 00:01:18.976 align:middle +yup! methods es uno de los argumentos. + +00:01:20.776 --> 00:01:24.636 align:middle +Vuelve a poner debug:router: Bien. + +00:01:25.276 --> 00:01:27.926 align:middle +Ahora la ruta sólo coincidirá +con las peticiones GET. Es... un + +00:01:28.646 --> 00:01:31.196 align:middle +poco difícil probar esto, + +00:01:31.376 --> 00:01:36.226 align:middle +ya que un navegador siempre hace peticiones +GET si vas directamente a una URL... + +00:01:36.796 --> 00:01:41.626 align:middle +pero aquí es donde resulta útil otro +comando de bin/console: router:match. + +00:01:42.256 --> 00:01:48.416 align:middle +Si lo ejecutamos sin argumentos Nos da un +error, ¡pero muestra cómo se utiliza! + +00:01:49.576 --> 00:01:56.156 align:middle +Inténtalo: php bin/console +router:match /api/songs/11 Y... + +00:01:56.486 --> 00:01:58.246 align:middle +¡que coincide con nuestra nueva ruta! + +00:01:59.356 --> 00:02:05.556 align:middle +Pero ahora pregúntate qué pasaría si +hiciéramos una petición POST a esa URL con + +00:02:05.896 --> 00:02:12.596 align:middle +--method=post : ¡Ninguna ruta +coincide con esta ruta con ese método! + +00:02:12.996 --> 00:02:17.376 align:middle +Pero dice que casi coincide con nuestra ruta. + +00:02:17.506 --> 00:02:20.716 align:middle +Vamos a hacer una cosa más para +ajustar nuestro nuevo punto final. + +00:02:20.716 --> 00:02:24.976 align:middle +Voy a añadir una pista de +tipo int al argumento $id. + +00:02:26.316 --> 00:02:33.436 align:middle +Eso... no cambia nada, excepto que +ahora PHP tomará la cadena id de la URL + +00:02:33.546 --> 00:02:39.826 align:middle +que Symfony pasa a este método y la +convertirá en un int, lo cual es... + +00:02:39.976 --> 00:02:44.336 align:middle +simplemente agradable porque entonces estamos tratando +con un verdadero número entero en nuestro código. + +00:02:45.276 --> 00:02:47.966 align:middle +Puedes ver la sutil diferencia en la respuesta. + +00:02:48.816 --> 00:02:51.536 align:middle +Ahora mismo, el campo id es una cadena. + +00:02:52.516 --> 00:02:57.326 align:middle +Cuando actualizamos, id es ahora +un número verdadero en JSON. + +00:02:58.046 --> 00:03:00.906 align:middle +Pero... si alguien se hiciera el remolón... + +00:03:01.036 --> 00:03:05.116 align:middle +y fuera a /api/songs/apple... + +00:03:06.166 --> 00:03:13.626 align:middle +¡vaya! ¡Un error PHP, que, en producción, +sería una página de error 500! + +00:03:14.046 --> 00:03:15.356 align:middle +Eso no me gusta. + +00:03:16.316 --> 00:03:18.016 align:middle +Pero... ¿qué podemos hacer? + +00:03:18.556 --> 00:03:24.666 align:middle +El error se produce cuando Symfony intenta llamar +a nuestro controlador y le pasa ese argumento. + +00:03:25.346 --> 00:03:29.736 align:middle +Así que no podemos poner código +en el controlador para comprobar + +00:03:29.736 --> 00:03:32.966 align:middle +si $id es un número: ¡es demasiado tarde! ¿Y + +00:03:33.276 --> 00:03:37.896 align:middle +si, en cambio, pudiéramos decirle a Symfony + +00:03:37.896 --> 00:03:44.586 align:middle +que esta ruta sólo debe coincidir +si el comodín id es un número? + +00:03:45.396 --> 00:03:46.516 align:middle +¿Es posible? + +00:03:46.956 --> 00:03:52.536 align:middle +Totalmente Por defecto, cuando tienes un +comodín, coincide con cualquier cosa. + +00:03:53.146 --> 00:03:57.276 align:middle +Pero puedes cambiarlo para que coincida +con una expresión regular personalizada. + +00:03:58.136 --> 00:04:10.166 align:middle +Dentro de las llaves, justo después del nombre, +añade un <, luego > y, en medio, \d+. + +00:04:10.166 --> 00:04:14.756 align:middle +Es una expresión regular que significa +"un dígito de cualquier longitud". + +00:04:15.626 --> 00:04:17.826 align:middle +¡Pruébalo! Actualiza y... + +00:04:18.286 --> 00:04:20.616 align:middle +¡sí! A 404. + +00:04:21.176 --> 00:04:25.146 align:middle +No se ha encontrado ninguna ruta: +simplemente no ha coincidido con esta ruta. + +00:04:25.146 --> 00:04:27.136 align:middle +Un 404 es genial... + +00:04:27.536 --> 00:04:29.156 align:middle +pero un error 500... + +00:04:29.306 --> 00:04:31.176 align:middle +es algo que queremos evitar. + +00:04:31.976 --> 00:04:35.906 align:middle +Y si volvemos a /api/songs/5... + +00:04:36.146 --> 00:04:37.836 align:middle +eso sigue funcionando. + +00:04:38.736 --> 00:04:44.336 align:middle +A continuación: si me preguntaras cuál es +la parte más central e importante de Symfony, + +00:04:44.746 --> 00:04:47.926 align:middle +no lo dudaría: son los servicios. + +00:04:48.816 --> 00:04:54.966 align:middle +Descubramos qué es un servicio y cómo es +la clave para liberar el potencial de Symfony diff --git a/sfcasts/ep1/es/services.md b/sfcasts/ep1/es/services.md new file mode 100644 index 0000000..c441329 --- /dev/null +++ b/sfcasts/ep1/es/services.md @@ -0,0 +1,63 @@ +# Objetos de servicio + +Veo a Symfony como dos grandes partes. La primera parte es el sistema de ruta, controlador y respuesta. Es muy simple y bueno... ¡ya eres un experto en ello! La segunda mitad de Symfony se trata de los muchos objetos útiles que están flotando por ahí... ¡sólo esperando a que los usemos! + +## Objetos de servicio Hola + +Por ejemplo, cuando renderizamos una plantilla, lo que estamos haciendo en realidad es aprovechar un objeto Twig y pedirle que renderice una plantilla. También hay un objeto registrador, un objeto caché, un objeto de conexión a la base de datos, un objeto que ayuda a hacer peticiones a la API, ¡y muchos, muchos más! Y cuando instalas un nuevo paquete, eso te da aún más objetos útiles. + +La verdad es que todo lo que hace Symfony lo hace... uno de estos objetos útiles. Diablos, ¡hay incluso un objeto router que se encarga de encontrar la ruta adecuada para la página dada! + +En el mundo de Symfony, y realmente en el mundo de la programación orientada a objetos en general, estos "objetos que hacen trabajo" tienen un nombre especial: servicios. Pero no dejes que esa palabra te confunda. Cuando oigas servicio, piensa: ¡es un objeto que hace trabajo! Como un objeto de plantilla que representa una plantilla o un objeto de conexión a la base de datos que realiza consultas. + +Y como los objetos de servicio hacen trabajo, son básicamente... ¡herramientas que te ayudan a hacer tu trabajo! La segunda mitad de Symfony consiste en descubrir qué servicios están disponibles y cómo utilizarlos. + + El comando debug:autowiring + +Vamos a probar algo. En nuestro controlador, quiero registrar un mensaje... quizás algún mensaje de depuración. Como registrar un mensaje es un trabajo, lo hace un servicio. ¿Nuestra aplicación ya tiene un servicio de registro? Y si es así, ¿cómo lo conseguimos? + +Para averiguarlo, ve a tu terminal y ejecuta otro comando `bin/console`: + +```terminal +php bin/console debug:autowiring +``` + +Saluda a uno de los comandos más potentes de `bin/console`. Me encanta esta cosa! Esta lista todos los servicios que existen en nuestra aplicación. De acuerdo, en realidad no es la lista completa, pero esto muestra los servicios que probablemente necesites. Y aunque nuestra aplicación es pequeña, ¡hay muchas cosas aquí! Hay un servicio de sistema de archivos... y aquí abajo un servicio de caché. ¡Incluso hay un servicio Twig! + +¿Hay un servicio de registro? Puedes mirar en esta lista... o puedes volver a ejecutar este comando y buscar la palabra log: + +```terminal-silent +php bin/console debug:autowiring log +``` + +¡Excelente! Por ahora, ignora todo excepto la primera línea. Esta línea nos dice que hay un servicio de registro y que este objeto implementa una interfaz llamada `Psr\Log\LoggerInterface`. + +## Obtención de un servicio mediante autoconexión + +Vale, ¿y por qué nos ayuda saber eso? Porque si quieres un servicio, lo pides utilizando la sugerencia de tipo que se muestra en este comando. Se llama autoconexión. + +Vamos a probarlo. Dirígete a nuestro controlador y añade un segundo argumento. En realidad, el orden de los argumentos no importa. Lo que importa es que el nuevo argumento se indique con `LoggerInterface`. Pulsaré el tabulador para autocompletarlo... para que PhpStorm añada la declaración de uso en la parte superior. + +En este caso, el argumento puede llamarse como sea, como `$logger`. Cuando Symfony ve esta sugerencia de tipo, busca dentro de la lista `debug:autowiring`... y como hay una coincidencia, nos pasará el servicio de registro. + +Así que ahora conocemos dos tipos diferentes de argumentos que podemos tener en el controlador: puedes tener un argumento cuyo nombre coincida con un comodín de la ruta o un argumento cuyo tipo-hint coincida con uno de los servicios de nuestra app. + +## Utilizar el registrador + +Bien, ahora que sabemos que Symfony nos pasará el objeto de servicio logger, ¡vamos a utilizarlo! No sé, todavía, qué métodos puedo llamar en él pero... si decimos`$logger->`... PhpStorm... ¡nos lo dice! ¡Ha sido fácil! + +Voy a registrar algo en un nivel de prioridad `info()`. Digamos: + +> Devolución de la respuesta de la API para la canción + +Y luego el `$id`. + +[[[ code('77f869a1d1') ]]] + +En realidad, podemos hacer algo aún más genial con este servicio de registro. Añade `{song}` al mensaje... y añade un segundo argumento, que es una matriz de información extra que quieres adjuntar al mensaje de registro. Pasa `song`ajustado a `$id`. En un minuto, verás que el registrador imprimirá el id real en lugar de `{song}`. + +[[[ code('c3b3132647') ]]] + +En cualquier caso, este controlador es para nuestra ruta de la API. Así que vamos a refrescarlo. ¡Um... ok! Así que no hay error, ¡eso es bueno! ¿Pero ha funcionado? ¿Dónde se registra realmente el servicio de registro? + +Averigüémoslo a continuación, aprendamos un truco para ver el perfilador incluso para las peticiones de la API y luego aprovechemos nuestro segundo servicio directamente. diff --git a/sfcasts/ep1/es/services.vtt b/sfcasts/ep1/es/services.vtt new file mode 100644 index 0000000..c879cf4 --- /dev/null +++ b/sfcasts/ep1/es/services.vtt @@ -0,0 +1,303 @@ +WEBVTT + +00:00:00.996 --> 00:00:03.526 align:middle +Veo a Symfony como dos grandes partes. + +00:00:03.976 --> 00:00:08.076 align:middle +La primera parte es el sistema de +rutas, controladores y respuestas. + +00:00:08.566 --> 00:00:10.816 align:middle +Es muy sencillo y bueno... + +00:00:10.906 --> 00:00:12.986 align:middle +¡ya eres un experto en ello! + +00:00:13.546 --> 00:00:19.936 align:middle +La segunda mitad de Symfony son los numerosos +objetos útiles que andan por ahí... + +00:00:20.176 --> 00:00:22.096 align:middle +esperando a que los utilicemos + +00:00:22.606 --> 00:00:28.466 align:middle +Por ejemplo, cuando renderizamos una plantilla, +lo que estamos haciendo en realidad es aprovechar + +00:00:28.466 --> 00:00:32.336 align:middle +un objeto Twig y pedirle +que renderice una plantilla. + +00:00:33.096 --> 00:00:38.206 align:middle +También hay un objeto registrador, un objeto +caché, un objeto de conexión a la base de datos, + +00:00:38.476 --> 00:00:43.056 align:middle +un objeto que ayuda a hacer peticiones +a la API, ¡y muchos, muchos más! + +00:00:43.576 --> 00:00:49.016 align:middle +Y cuando instalas un nuevo paquete, +eso te da aún más objetos útiles. + +00:00:49.676 --> 00:00:53.506 align:middle +La verdad es que todo lo que hace Symfony es... + +00:00:53.506 --> 00:00:56.516 align:middle +en realidad lo hace uno +de estos objetos útiles. + +00:00:56.706 --> 00:01:00.496 align:middle +Diablos, ¡hay incluso un +objeto router que se encarga + +00:01:00.496 --> 00:01:03.356 align:middle +de encontrar la ruta adecuada +para la página dada! + +00:01:04.316 --> 00:01:09.936 align:middle +En el mundo de Symfony, y realmente en el mundo de +la programación orientada a objetos en general, + +00:01:10.176 --> 00:01:15.506 align:middle +estos "objetos que hacen trabajo" +tienen un nombre especial: servicios. + +00:01:16.166 --> 00:01:17.766 align:middle +Pero no dejes que esa palabra te confunda. + +00:01:18.186 --> 00:01:23.016 align:middle +Cuando oigas servicio, piensa: +¡es un objeto que hace trabajo! + +00:01:23.496 --> 00:01:26.316 align:middle +Como un objeto de plantilla +que representa una plantilla + +00:01:26.576 --> 00:01:29.906 align:middle +o un objeto de conexión a la base +de datos que realiza consultas. + +00:01:30.506 --> 00:01:34.056 align:middle +Y como los objetos de servicio +hacen trabajo, son básicamente... + +00:01:34.206 --> 00:01:36.936 align:middle +¡herramientas que te +ayudan a hacer tu trabajo! + +00:01:37.756 --> 00:01:41.356 align:middle +La segunda mitad de Symfony +consiste en descubrir + +00:01:41.356 --> 00:01:44.456 align:middle +qué servicios están +disponibles y cómo utilizarlos. + +00:01:45.346 --> 00:01:46.466 align:middle +Vamos a probar algo. + +00:01:47.146 --> 00:01:50.496 align:middle +En nuestro controlador, +quiero registrar un mensaje... + +00:01:50.646 --> 00:01:53.076 align:middle +tal vez un mensaje de depuración. + +00:01:53.996 --> 00:01:58.026 align:middle +Como registrar un mensaje es un +trabajo, lo hace un servicio. + +00:01:58.406 --> 00:02:01.616 align:middle +¿Nuestra aplicación ya +tiene un servicio de registro? + +00:02:02.136 --> 00:02:04.216 align:middle +Y si es así, ¿cómo lo conseguimos? + +00:02:05.316 --> 00:02:09.366 align:middle +Para averiguarlo, ve a tu terminal +y ejecuta otro comando bin/console: + +00:02:09.706 --> 00:02:19.256 align:middle +php bin/console debug:autowiring Saluda a uno +de los comandos más potentes de bin/console. + +00:02:19.596 --> 00:02:20.936 align:middle +¡Me encanta esta cosa! + +00:02:21.526 --> 00:02:25.186 align:middle +Esto lista todos los servicios que +existen en nuestra aplicación. + +00:02:25.716 --> 00:02:28.066 align:middle +Vale, en realidad no es la lista completa, + +00:02:28.546 --> 00:02:31.776 align:middle +pero esto muestra los servicios que +más probablemente necesitarás. + +00:02:31.776 --> 00:02:35.756 align:middle +Y aunque nuestra aplicación es +pequeña, ¡hay muchas cosas aquí! + +00:02:36.316 --> 00:02:38.096 align:middle +Hay un servicio de sistema de archivos... + +00:02:38.436 --> 00:02:40.686 align:middle +y aquí abajo un servicio de caché. + +00:02:41.136 --> 00:02:43.096 align:middle +¡Incluso hay un servicio Twig! + +00:02:43.866 --> 00:02:45.806 align:middle +¿Hay un servicio de registro? + +00:02:46.486 --> 00:02:47.676 align:middle +Puedes buscar en esta lista... + +00:02:48.036 --> 00:02:52.936 align:middle +o puedes volver a ejecutar este comando +y buscar la palabra log: ¡Excelente! + +00:02:53.546 --> 00:02:56.296 align:middle +Por ahora, ignora todo +excepto la primera línea. + +00:02:56.526 --> 00:03:00.026 align:middle +Esta línea nos dice que +existe un servicio de registro + +00:03:00.406 --> 00:03:06.346 align:middle +y que este objeto implementa una +interfaz llamada Psr\Log\LoggerInterface. + +00:03:07.366 --> 00:03:10.726 align:middle +Bien, ¿por qué nos ayuda saber eso? + +00:03:11.546 --> 00:03:18.626 align:middle +Porque si quieres un servicio, lo pides utilizando +la sugerencia de tipo que se muestra en este comando. + +00:03:19.096 --> 00:03:20.936 align:middle +Se llama autoconexión. + +00:03:21.806 --> 00:03:22.326 align:middle +Vamos a probarlo. + +00:03:22.736 --> 00:03:26.066 align:middle +Dirígete a nuestro controlador +y añade un segundo argumento. + +00:03:26.516 --> 00:03:29.576 align:middle +En realidad, el orden de +los argumentos no importa. + +00:03:30.046 --> 00:03:35.756 align:middle +Lo que importa es que el nuevo argumento +esté indicado con LoggerInterface. + +00:03:36.266 --> 00:03:38.336 align:middle +Pulsaré el tabulador para autocompletarlo... + +00:03:38.676 --> 00:03:42.366 align:middle +para que PhpStorm añada la declaración +de uso en la parte superior. + +00:03:42.366 --> 00:03:47.096 align:middle +En este caso, el argumento puede +llamarse como sea, como $logger. + +00:03:47.866 --> 00:03:52.876 align:middle +Cuando Symfony ve esta sugerencia de tipo, +busca dentro de la lista debug:autowiring... + +00:03:53.296 --> 00:03:57.626 align:middle +y como hay una coincidencia, nos +pasará el servicio de registro. + +00:03:58.276 --> 00:04:03.456 align:middle +Así que ahora conocemos dos tipos diferentes de +argumentos que podemos tener en el controlador: + +00:04:04.226 --> 00:04:08.446 align:middle +puedes tener un argumento cuyo nombre +coincida con un comodín de la ruta + +00:04:08.756 --> 00:04:13.796 align:middle +o un argumento cuyo tipo-indicación coincida +con uno de los servicios de nuestra aplicación. + +00:04:14.876 --> 00:04:20.786 align:middle +Bien, ahora que sabemos que Symfony nos pasará el +objeto de servicio logger, ¡vamos a utilizarlo! + +00:04:21.126 --> 00:04:24.906 align:middle +No sé, todavía, qué métodos +puedo llamar en él, pero... + +00:04:24.906 --> 00:04:26.536 align:middle +si decimos $logger->... + +00:04:27.046 --> 00:04:28.226 align:middle +PhpStorm... + +00:04:28.226 --> 00:04:30.056 align:middle +¡nos lo dice! ¡Ha sido fácil! + +00:04:30.976 --> 00:04:34.236 align:middle +Voy a registrar algo en un +nivel de prioridad info(). + +00:04:34.776 --> 00:04:39.276 align:middle +Digamos: Devolución de la respuesta de +la API para la canción Y luego el $id. + +00:04:41.346 --> 00:04:45.276 align:middle +En realidad, podemos hacer algo aún más +genial con este servicio de registro. + +00:04:46.406 --> 00:04:49.046 align:middle +Añade {song} al mensaje... + +00:04:49.466 --> 00:04:52.726 align:middle +y añade un segundo argumento, que es un array + +00:04:52.726 --> 00:04:56.216 align:middle +de información extra que quieres +adjuntar al mensaje de registro. + +00:04:57.036 --> 00:04:59.376 align:middle +Pasa song ajustado a $id. + +00:05:01.136 --> 00:05:07.196 align:middle +En un momento, verás que el logger +imprimirá el id real en lugar de {song}. + +00:05:08.756 --> 00:05:12.196 align:middle +En cualquier caso, este controlador +es para nuestra ruta de la API. + +00:05:12.686 --> 00:05:15.966 align:middle +Así que vamos a actualizarlo. + +00:05:16.966 --> 00:05:20.746 align:middle +Um... ¡bien! Así que no +hay error, ¡eso es bueno! + +00:05:21.176 --> 00:05:22.536 align:middle +¿Pero ha funcionado? + +00:05:23.236 --> 00:05:24.596 align:middle +¿Dónde se registra el servicio de registro... + +00:05:24.896 --> 00:05:26.436 align:middle +¿realmente se registra? + +00:05:27.276 --> 00:05:33.246 align:middle +Averigüémoslo a continuación, aprendamos un truco para +ver el perfilador incluso para las peticiones de la API + +00:05:33.506 --> 00:05:36.786 align:middle +y luego aprovechemos nuestro +segundo servicio directamente diff --git a/sfcasts/ep1/es/setup.md b/sfcasts/ep1/es/setup.md new file mode 100644 index 0000000..7186807 --- /dev/null +++ b/sfcasts/ep1/es/setup.md @@ -0,0 +1,101 @@ +# Hola Symfony + +Bienvenido. Hola. Hola, mi nombre es Ryan y tengo el absoluto placer de presentarte el hermoso y fascinante y productivo mundo de Symfony 6. En serio, me siento como Willie Wonka invitándote a mi fábrica de chocolate, excepto que, con suerte, con menos lesiones relacionadas con el azúcar. De todos modos, si eres nuevo en Symfony, estoy... ¡sinceramente un poco celoso! Te va a encantar el viaje... y espero que te conviertas en un desarrollador aún mejor por el camino: definitivamente vas a construir cosas geniales. + +La salsa secreta de Symfony es que empieza siendo diminuto, lo que hace que sea fácil de aprender. Pero luego, amplía sus características automáticamente a través de un sistema de recetas único. En Symfony 6, esas características incluyen nuevas herramientas de JavaScript y un nuevo sistema de seguridad... sólo por nombrar dos de las muchas cosas nuevas. + +Symfony también es rápido como un rayo, con un gran enfoque en la creación de una experiencia alegre para el desarrollador, pero sin sacrificar las mejores prácticas de programación. Sí: consigues amar la codificación y amar tu código. Lo sé... ha sonado cursi, pero es cierto. + +Así que ven conmigo y estarás en un mundo de pura elucidación. + +Es la primera vez que canto en estos tutoriales... y quizá la última. Empecemos. + +## Instalar el binario "symfony + +Dirígete a https://symfony.com/download. En esta página, encontrarás algunas instrucciones -que variarán en función de tu sistema operativo- sobre cómo descargar algo llamado el binario de Symfony. + +Esto... no es realmente Symfony. Es sólo una herramienta de línea de comandos que nos ayudará a iniciar nuevos proyectos Symfony y nos dará algunas buenas herramientas de desarrollo local. Es opcional, pero lo recomiendo encarecidamente + +Una vez que hayas instalado esto - yo ya lo he hecho - abre tu aplicación de terminal favorita. Yo estoy usando iTerm para mac, pero no importa. Si lo has configurado todo correctamente, deberías poder ejecutarlo: + +```terminal +symfony +``` + +O incluso mejor + +```terminal +symfony list +``` + +para ver una lista de todas las "cosas" que puede hacer este binario de symfony. Hay muchas cosas aquí: cosas que ayudan al desarrollo "local"... y también algunos servicios opcionales para el despliegue. Vamos a repasar las cosas que necesitas saber a lo largo del camino. + +## ¡Iniciemos una aplicación Symfony! + +Bien, queremos iniciar una nueva y brillante aplicación Symfony. Para ello, ejecuta: + +```terminal +symfony new mixed_vinyl +``` + +Donde "mixed_vinyl" es el directorio en el que se descargará la nueva app. Se trata de nuestro proyecto secreto para combinar la mejor parte de los años 90 -no, no el Internet de acceso telefónico, hablo de las cintas de mezcla- con el deleite auditivo de los discos. Más adelante hablaremos de ello. + +Entre bastidores, este comando utiliza Composer -el gestor de paquetes de PHP- para crear el nuevo proyecto. Más adelante hablaremos de ello. + +El resultado final es que podemos pasar a nuestro nuevo directorio `mixed_vinyl`. Abre esta carpeta en tu editor favorito. Yo estoy usando PhpStorm y lo recomiendo encarecidamente. + + Conociendo nuestro nuevo Proyecto + +¿Qué ha hecho ese comando `symfony new`? Ha arrancado un nuevo proyecto Symfony! Ooh. Y ya tenemos un repositorio git. Ejecuta: + +```terminal +git status +``` + +Sí: en la rama principal, nada que confirmar. Prueba: + +```terminal +git log +``` + +Genial. Después de descargar el nuevo proyecto, el comando confirmó todos los archivos originales automáticamente... lo cual fue muy agradable. Aunque me gustaría que el primer mensaje de confirmación fuera un poco más rockero. + +¡Lo que realmente quiero mostrarte es que nuestro nuevo proyecto es súper pequeño! Prueba este comando: + +```terminal +git show --name-only +``` + +¡Sí! Todo nuestro proyecto es... unos 17 archivos. Y aprenderemos sobre todos ellos a lo largo del camino. Pero quiero que te sientas cómodo: no hay mucho código aquí. + +Vamos a añadir funciones poco a poco. Pero si quieres empezar con un proyecto más grande y con más funciones, puedes hacerlo ejecutando el comando `symfony new` con `--webapp`. + +***TIP +Si quieres una nueva aplicación Symfony con todas las funciones, echa un vistazo a https://github.com/dunglas/symfony-docker +*** + +## Comprobación de los requisitos del sistema + +Antes de saltar a la codificación, vamos a asegurarnos de que nuestro sistema está listo. Ejecuta otro comando del binario de symfony: + +```terminal +symfony check:req +``` + +¡Parece que está bien! Si a tu instalación de PHP le falta alguna extensión... o hay algún otro problema... como que tu ordenador es en realidad una tetera, esto te lo hará saber. + +## Iniciar el servidor web de desarrollo + +Entonces: tenemos una nueva aplicación Symfony aquí... ¡y nuestro sistema está listo! Todo lo que necesitamos ahora es un subwoofer. Es decir, ¡un servidor web! Puedes configurar un servidor web real como Nginx o algo moderno como Caddy. Pero para el desarrollo local, el binario de Symfony puede ayudarnos. Corre: + +```terminal +symfony serve -d +``` + +Y... ¡tenemos un servidor web funcionando! ¡Vuelve! + +La primera vez que ejecutes esto, es posible que te pida que ejecutes otro comando para configurar un certificado SSL, lo cual está bien porque entonces el servidor soporta https. + +¡Momento de la verdad! Copia la URL, gira hacia tu navegador, aguanta la respiración y ¡woo! Hola página de bienvenida de Symfony 6... completa con extravagantes cambios de color cada vez que recargamos. + +A continuación: conozcamos -y hagámonos amigos- del código dentro de nuestra aplicación, para poder desmitificar lo que hace cada parte. Luego codificaremos. diff --git a/sfcasts/ep1/es/setup.vtt b/sfcasts/ep1/es/setup.vtt new file mode 100644 index 0000000..6282ab0 --- /dev/null +++ b/sfcasts/ep1/es/setup.vtt @@ -0,0 +1,325 @@ +WEBVTT + +00:00:00.056 --> 00:00:04.606 align:middle +Bienvenido. Hola. Hola, + +00:00:04.676 --> 00:00:11.826 align:middle +me llamo Ryan y tengo el absoluto +placer de presentarte el hermoso, + +00:00:12.176 --> 00:00:16.376 align:middle +fascinante y productivo mundo de Symfony 6. + +00:00:16.546 --> 00:00:21.186 align:middle +En serio, me siento como Willie Wonka +invitándote a mi fábrica de chocolate, + +00:00:21.546 --> 00:00:25.076 align:middle +excepto que con suerte con menos +lesiones relacionadas con el azúcar. + +00:00:25.596 --> 00:00:28.076 align:middle +De todos modos, si eres +nuevo en Symfony, estoy... + +00:00:28.076 --> 00:00:30.246 align:middle +sinceramente, ¡estoy un poco celoso! + +00:00:30.586 --> 00:00:32.496 align:middle +Te va a encantar el viaje... + +00:00:33.036 --> 00:00:36.316 align:middle +y espero que te conviertas en un +desarrollador aún mejor en el camino: + +00:00:36.906 --> 00:00:40.106 align:middle +definitivamente vas a construir cosas geniales. + +00:00:40.166 --> 00:00:45.686 align:middle +La salsa secreta de Symfony es que empieza siendo +pequeño, lo que hace que sea fácil de aprender. + +00:00:46.106 --> 00:00:51.446 align:middle +Pero luego, amplía sus características automáticamente +a través de un sistema de recetas único. + +00:00:52.336 --> 00:00:58.726 align:middle +En Symfony 6, esas características incluyen nuevas +herramientas de JavaScript y un nuevo sistema de seguridad... + +00:00:58.876 --> 00:01:01.836 align:middle +sólo por nombrar dos de las muchas novedades. + +00:01:02.346 --> 00:01:09.336 align:middle +Symfony también es rápido como un rayo, con un gran enfoque +en la creación de una experiencia alegre para el desarrollador, + +00:01:09.596 --> 00:01:13.736 align:middle +pero sin sacrificar las mejores +prácticas de programación. + +00:01:14.426 --> 00:01:17.956 align:middle +Sí: consigues amar la +codificación y amar tu código. + +00:01:18.406 --> 00:01:20.966 align:middle +Lo sé... ha sonado cursi, pero es cierto. + +00:01:21.106 --> 00:01:28.546 align:middle +Así que ven conmigo y estarás +en un mundo de pura elucidación. + +00:01:29.306 --> 00:01:32.166 align:middle +Es la primera vez que canto +en estos tutoriales... + +00:01:32.166 --> 00:01:33.346 align:middle +y quizá la última. + +00:01:33.656 --> 00:01:34.426 align:middle +Empecemos. + +00:01:34.426 --> 00:01:38.536 align:middle +Dirígete a https://symfony.com/download. + +00:01:39.256 --> 00:01:43.266 align:middle +En esta página, encontrarás algunas +instrucciones -que variarán en + +00:01:43.266 --> 00:01:48.716 align:middle +función de tu sistema operativo- sobre cómo +descargar algo llamado el binario de Symfony. + +00:01:49.146 --> 00:01:50.296 align:middle +Esto es... + +00:01:50.426 --> 00:01:52.196 align:middle +no es realmente Symfony. Es + +00:01:52.876 --> 00:01:57.536 align:middle +sólo una herramienta de línea de comandos que +nos ayudará a iniciar nuevos proyectos Symfony + +00:01:57.826 --> 00:02:00.716 align:middle +y nos dará algunas buenas +herramientas de desarrollo local. + +00:02:01.206 --> 00:02:03.986 align:middle +Es opcional, pero lo recomiendo encarecidamente + +00:02:04.546 --> 00:02:10.156 align:middle +Una vez que hayas instalado esto - yo ya lo he +hecho - abre tu aplicación de terminal favorita. + +00:02:10.776 --> 00:02:14.366 align:middle +Yo estoy usando iTerm +para mac, pero no importa. + +00:02:14.916 --> 00:02:20.566 align:middle +Si lo has configurado todo correctamente, +deberías poder ejecutarlo: symfony O incluso mejor + +00:02:20.566 --> 00:02:27.256 align:middle +symfony list para ver una lista de todas las +"cosas" que puede hacer este binario de symfony. + +00:02:28.116 --> 00:02:32.076 align:middle +Hay muchas cosas aquí: cosas que +ayudan al desarrollo "local"... + +00:02:32.336 --> 00:02:35.386 align:middle +y también algunos servicios +opcionales para el despliegue. + +00:02:35.646 --> 00:02:38.956 align:middle +Vamos a recorrer las cosas que +necesitas conocer a lo largo del camino. + +00:02:39.846 --> 00:02:44.266 align:middle +Bien, queremos iniciar una nueva +y brillante aplicación Symfony. + +00:02:44.746 --> 00:02:50.366 align:middle +Para ello, ejecuta: symfony +new mixed_vinyl Donde + +00:02:50.366 --> 00:02:54.766 align:middle +"mixed_vinyl" es el directorio en el que se +descargará la nueva aplicación. Se trata de + +00:02:55.006 --> 00:03:01.056 align:middle +nuestro proyecto ultra secreto para +combinar lo mejor de los años 90 + +00:03:01.056 --> 00:03:07.896 align:middle +-no, no el Internet de acceso telefónico, hablo de las +cintas de mezcla- con el deleite auditivo de los discos. + +00:03:08.286 --> 00:03:09.446 align:middle +Más adelante hablaremos de ello. + +00:03:10.076 --> 00:03:13.426 align:middle +Entre bastidores, este +comando utiliza Composer -el + +00:03:13.676 --> 00:03:17.646 align:middle +gestor de paquetes de PHP- +para crear el nuevo proyecto. + +00:03:17.976 --> 00:03:19.546 align:middle +Más adelante hablaremos de ello. + +00:03:19.846 --> 00:03:24.266 align:middle +El resultado final es que podemos pasar +a nuestro nuevo directorio mixed_vinyl. + +00:03:25.006 --> 00:03:27.356 align:middle +Abre esta carpeta en tu editor favorito. + +00:03:27.716 --> 00:03:31.266 align:middle +Yo estoy usando PhpStorm y lo +recomiendo encarecidamente. + +00:03:31.966 --> 00:03:34.696 align:middle +¿Qué ha hecho el comando symfony new? + +00:03:35.526 --> 00:03:38.076 align:middle +¡Arrancó un nuevo proyecto Symfony! + +00:03:38.436 --> 00:03:43.016 align:middle +Ooh. Y ya tenemos un repositorio git. + +00:03:43.786 --> 00:03:48.506 align:middle +Ejecuta: git status Sí: en la rama +main, no hay nada que confirmar. + +00:03:49.126 --> 00:03:51.716 align:middle +Prueba: git log Genial. + +00:03:51.766 --> 00:03:55.716 align:middle +Después de descargar el nuevo +proyecto, el comando confirmó todos los + +00:03:55.716 --> 00:03:57.796 align:middle +archivos originales automáticamente... + +00:03:58.256 --> 00:04:00.356 align:middle +lo que fue muy agradable. + +00:04:00.716 --> 00:04:04.986 align:middle +Aunque me gustaría que el primer mensaje +de commit fuera un poco más rockero. + +00:04:05.746 --> 00:04:10.156 align:middle +¡Lo que realmente quiero mostrarte es que +nuestro nuevo proyecto es súper pequeño! + +00:04:10.526 --> 00:04:15.666 align:middle +Prueba este comando: git +show --name-only ¡Sí! + +00:04:15.906 --> 00:04:18.006 align:middle +Todo nuestro proyecto es... + +00:04:18.046 --> 00:04:19.806 align:middle +unos 17 archivos. + +00:04:20.266 --> 00:04:22.656 align:middle +Y aprenderemos sobre todos +ellos a lo largo del camino. + +00:04:23.256 --> 00:04:27.386 align:middle +Pero quiero que te sientas cómodo: +no hay mucho código aquí. + +00:04:28.156 --> 00:04:30.196 align:middle +Vamos a añadir funciones poco a poco. + +00:04:30.466 --> 00:04:35.096 align:middle +Pero si quieres empezar con un proyecto +más grande y con más funciones, + +00:04:35.246 --> 00:04:39.926 align:middle +puedes hacerlo ejecutando el +comando symfony new con --webapp. + +00:04:40.846 --> 00:04:44.266 align:middle +Antes de saltar a la codificación, vamos a +asegurarnos de que nuestro sistema está listo. + +00:04:44.786 --> 00:04:52.246 align:middle +Ejecuta otro comando del binario de symfony: +symfony check:req ¡Parece que está bien! + +00:04:52.736 --> 00:04:55.736 align:middle +Si a tu instalación de PHP +le falta alguna extensión... + +00:04:55.926 --> 00:04:57.836 align:middle +o hay algún otro problema... + +00:04:58.056 --> 00:05:01.956 align:middle +como que tu ordenador es en realidad +una tetera, esto te lo hará saber. + +00:05:02.036 --> 00:05:05.956 align:middle +Entonces: tenemos una nueva +aplicación Symfony aquí... + +00:05:06.326 --> 00:05:08.196 align:middle +¡y nuestro sistema está listo! + +00:05:08.906 --> 00:05:11.116 align:middle +Todo lo que necesitamos ahora es un subwoofer. + +00:05:11.446 --> 00:05:12.856 align:middle +Es decir, ¡un servidor web! + +00:05:13.466 --> 00:05:18.936 align:middle +Puedes configurar un servidor web real +como Nginx o algo moderno como Caddy. + +00:05:19.296 --> 00:05:23.776 align:middle +Pero para el desarrollo local, el +binario de Symfony puede ayudarnos. + +00:05:23.776 --> 00:05:28.656 align:middle +Corre: symfony serve -d Y... + +00:05:29.096 --> 00:05:31.086 align:middle +¡tenemos un servidor web funcionando! + +00:05:31.416 --> 00:05:32.146 align:middle +¡Vuelve! + +00:05:32.796 --> 00:05:37.446 align:middle +La primera vez que ejecutes esto, es posible +que te pida que ejecutes otro comando para + +00:05:37.446 --> 00:05:44.386 align:middle +configurar un certificado SSL, lo cual está +bien porque entonces el servidor soporta https. + +00:05:44.386 --> 00:05:46.096 align:middle +¡Momento de la verdad! + +00:05:46.476 --> 00:05:54.026 align:middle +Copia la URL, gira a tu navegador, +aguanta la respiración y ¡guau! + +00:05:54.466 --> 00:05:56.986 align:middle +Hola página de bienvenida de Symfony 6... + +00:05:57.246 --> 00:06:00.396 align:middle +completa con los elegantes cambios +de color cada vez que recargamos. + +00:06:01.436 --> 00:06:06.516 align:middle +A continuación: conozcamos -y hagámonos amigos- +del código dentro de nuestra aplicación, + +00:06:06.856 --> 00:06:09.866 align:middle +para poder desmitificar lo que hace cada parte. + +00:06:10.326 --> 00:06:11.836 align:middle +Luego codificaremos diff --git a/sfcasts/ep1/es/stimulus-example.md b/sfcasts/ep1/es/stimulus-example.md new file mode 100644 index 0000000..db3779d --- /dev/null +++ b/sfcasts/ep1/es/stimulus-example.md @@ -0,0 +1,89 @@ +# Ejemplo de Stimulus en el mundo real + +Pongamos a prueba a Stimulus. Éste es nuestro objetivo: cuando hagamos clic en el icono de reproducción, haremos una petición Ajax a nuestra ruta de la API... la que está en `SongController`. Esto devuelve la URL donde se puede reproducir esta canción. Entonces usaremos eso en JavaScript para... ¡reproducir la canción! + +Toma `hello_controller.js` y cámbiale el nombre a, qué tal `song-controls_controller.js`. Dentro, sólo para ver si esto funciona, en `connect()`, registra un mensaje. El método`connect()` se llama cada vez que Stimulus ve un nuevo elemento coincidente en la página. + +[[[ code('e29d1ac07a') ]]] + +Ahora, en la plantilla, hola ya no va a funcionar, así que quita eso. Lo que quiero hacer es rodear cada fila de canciones con este controlador.... así que es este elemento`song-list`. Después de la clase, añade `{{ stimulus_controller('song-controls') }}`. + +[[[ code('bd43e92632') ]]] + +Vamos a probarlo Actualiza, comprueba la consola y... ¡sí! Golpeó nuestro código seis veces! Una vez por cada uno de estos elementos. Y cada elemento recibe su propia instancia de controlador, por separado. + +## Añadir acciones de Stimulus + +Bien, a continuación, cuando hagamos clic en reproducir, queremos ejecutar algún código. Para ello, podemos añadir una acción. Tiene este aspecto: en la etiqueta `a`, añade `{{ stimulus_action() }}` -otra función de acceso directo- y pásale el nombre del controlador al que estás adjuntando la acción - `song-controls` - y luego un método dentro de ese controlador que debe ser llamado cuando alguien haga clic en este elemento. ¿Qué te parece `play`. + +[[[ code('69caff7d11') ]]] + +Genial, ¿no? De vuelta en el controlador de la canción, ya no necesitamos el método `connect()`: no tenemos que hacer nada cada vez que veamos otra fila `song-list`. Pero sí necesitamos un método `play()`. + +Y al igual que con los escuchadores de eventos normales, éste recibirá un objeto `event`... y entonces podremos decir `event.preventDefault()` para que nuestro navegador no intente seguir el clic del enlace. Para probar, `console.log('Playing!')`. + +[[[ code('80418b94bb') ]]] + +¡Vamos a ver qué pasa! Actualiza y... haz clic. Ya funciona. Así de fácil es enganchar un oyente de eventos en Stimulus. Ah, y si inspeccionas este elemento... esa función`stimulus_action()` es sólo un atajo para añadir un atributo especial `data-action`que Stimulus entiende. + +## Instalar e importar Axios + +Bien, ¿cómo podemos hacer una llamada Ajax desde dentro del método `play()`? Bueno, podríamos utilizar la función integrada `fetch()` de JavaScript. Pero en su lugar, voy a instalar una biblioteca de terceros llamada Axios. En tu terminal, instálala diciendo: + +```terminal +yarn add axios --dev +``` + +Ahora sabemos lo que hace: descarga este paquete en nuestro directorio `node_modules`, y añade esta línea a nuestro archivo `package.json`. + +Ah, y nota al margen: puedes utilizar absolutamente jQuery dentro de Stimulus. No lo haré, pero funciona muy bien - y puedes instalar - e importar - jQuery como cualquier otro paquete. Hablamos de ello en nuestro tutorial de Stimulus. + +Bien, ¿cómo utilizamos la biblioteca `axios`? Importándola + +Al principio de este archivo, ya hemos importado la clase base `Controller` de`stimulus`. Ahora `import axios from 'axios'`. En cuanto lo hagamos, Webpack Encore cogerá el código fuente de `axios` y lo incluirá en nuestros archivos JavaScript construidos. + +[[[ code('f334d5443b') ]]] + +Ahora, aquí abajo, podemos decir `axios.get()` para hacer una petición GET. Pero... ¿qué debemos pasar para la URL? Tiene que ser algo como `/api/songs/5`... pero ¿cómo sabemos cuál es el "id" de esta fila? + +## Valores de Stimulus + +Una de las cosas más interesantes de Stimulus es que te permite pasar valores de Twig a tu controlador Stimulus. Para ello, declara qué valores quieres permitir que se pasen a través de una propiedad estática especial: `static values = {}`. Dentro, vamos a permitir que se pase un valor de `infoUrl`. Me acabo de inventar ese nombre: creo que pasaremos la URL completa a la ruta de la API. Establece esto como el tipo que será. Es decir, un `String`. + +Aprenderemos cómo pasamos este valor desde Twig a nuestro controlador en un minuto. Pero como tenemos esto, abajo, podemos referenciar el valor diciendo `this.infoUrlValue`. + +[[[ code('0684edc5d4') ]]] + +Entonces, ¿cómo lo pasamos? De vuelta en `homepage.html.twig`, añade un segundo argumento a `stimulus_controller()`. Este es un array de los valores que quieres pasar al controlador. Pasa a `infoUrl` el conjunto de la URL. + +Hmm, pero tenemos que generar esa URL. ¿Esa ruta tiene ya un nombre? No, añade `name: 'api_songs_get_one'`. + +[[[ code('e20f187c7b') ]]] + +Perfecto. Copia eso... y de nuevo en la plantilla, establece `infoURl` a `path()`, el nombre de la ruta... y luego una matriz con cualquier comodín. Nuestra ruta tiene un comodín`id`. + +En una aplicación real, estas rutas probablemente tendrían cada una un id de base de datos que podríamos pasar. Todavía no lo tenemos... así que para, en cierto modo, falsear esto, voy a utilizar`loop.index`. Esta es una variable mágica de Twig: si estás dentro de un bucle de Twig `for`, puedes acceder al índice -como 1, 2, 3, 4- utilizando `loop.index`. Así que vamos a usar esto como una identificación falsa. Ah, y no olvides decir `id:` y luego`loop.index`. + +[[[ code('f3a0755cb5') ]]] + +¡Hora de probar! Refresca. Lo primero que quiero que veas es que, cuando pasamos`infoUrl` como segundo argumento a `stimulus_controller`, lo único que hace es dar salida a un atributo muy especial `data` que Stimulus sabe leer. Así es como se pasa un valor a un controlador. + +Haz clic en uno de los enlaces de reproducción y... lo tienes. ¡A cada objeto controlador se le pasa su URL correcta! + +## Hacer la llamada Ajax + +¡Vamos a celebrarlo haciendo la llamada Ajax! Hazlo con `axios.get(this.infoUrlValue)` -sí, acabo de escribirlo-, `.then()` y una devolución de llamada utilizando una función de flecha que recibirá un argumento `response`. Esto se llamará cuando termine la llamada Ajax. Registra la respuesta para empezar. Ah, y corrige para usar `this.infoUrlValue`. + +[[[ code('de15183eff') ]]] + +Muy bien, actualiza... ¡y haz clic en el enlace de reproducción! ¡Sí! Ha volcado la respuesta... y una de sus claves es `data`... ¡que contiene el `url`! + +¡Es hora de dar la vuelta de la victoria! De vuelta a la función, podemos reproducir ese audio creando un nuevo objeto `Audio` -es un objeto JavaScript normal-, pasándole`response.data.url`... y llamando a continuación a `play()`. + +[[[ code('a92c517158') ]]] + +Y ahora... cuando le demos al play... ¡por fin! Música para mis oídos. + +Si quieres aprender más sobre Stimulus - esto ha sido un poco rápido - tenemos un tutorial entero sobre ello... y es genial. + +Para terminar este tutorial, vamos a instalar otra biblioteca de JavaScript. Ésta hará que nuestra aplicación se sienta instantáneamente como una aplicación de una sola página. Eso a continuación. \ No newline at end of file diff --git a/sfcasts/ep1/es/stimulus-example.vtt b/sfcasts/ep1/es/stimulus-example.vtt new file mode 100644 index 0000000..5818235 --- /dev/null +++ b/sfcasts/ep1/es/stimulus-example.vtt @@ -0,0 +1,422 @@ +WEBVTT + +00:00:01.016 --> 00:00:03.016 align:middle +Pongamos a prueba a Stimulus. + +00:00:03.746 --> 00:00:06.596 align:middle +Éste es nuestro objetivo: cuando hagamos +clic en el icono de reproducción, vamos a + +00:00:06.926 --> 00:00:10.366 align:middle +hacer una petición Ajax a +nuestra ruta de la API... + +00:00:10.716 --> 00:00:12.916 align:middle +el que está en SongController. + +00:00:13.776 --> 00:00:17.396 align:middle +Esto devuelve la URL donde se +puede reproducir esta canción. + +00:00:18.106 --> 00:00:20.786 align:middle +Luego lo utilizaremos en JavaScript para... + +00:00:20.936 --> 00:00:21.946 align:middle +¡reproducir la canción! + +00:00:23.286 --> 00:00:30.686 align:middle +Coge hello_controller.js y cámbiale el nombre +a, ¿qué tal song-controls_controller.js.? + +00:00:32.286 --> 00:00:37.466 align:middle +Dentro, sólo para ver si esto funciona, +en connect(), registra un mensaje. + +00:00:38.086 --> 00:00:43.016 align:middle +El método connect() se llama cada vez que Stimulus +ve un nuevo elemento coincidente en la página. + +00:00:44.136 --> 00:00:48.636 align:middle +Ahora, en la plantilla, hola ya no +va a funcionar, así que quita eso. + +00:00:50.106 --> 00:00:54.776 align:middle +Lo que quiero hacer es rodear cada fila +de canciones con este controlador.... + +00:00:55.086 --> 00:00:57.856 align:middle +así que es este elemento song-list. + +00:00:57.856 --> 00:01:03.936 align:middle +Después de la clase, añade {{ +stimulus_controller('song-controls') }}. + +00:01:03.976 --> 00:01:06.106 align:middle +Vamos a probarlo + +00:01:06.486 --> 00:01:10.296 align:middle +Refresca, comprueba la consola y... + +00:01:10.586 --> 00:01:13.516 align:middle +¡sí! ¡Ha golpeado +nuestro código seis veces! + +00:01:14.086 --> 00:01:16.446 align:middle +Una vez por cada uno de estos elementos. + +00:01:16.966 --> 00:01:21.296 align:middle +Y cada elemento tiene su propia +instancia de controlador, independiente. + +00:01:22.206 --> 00:01:26.856 align:middle +Bien, a continuación, cuando hagamos clic en +reproducir, queremos ejecutar algún código. + +00:01:27.616 --> 00:01:30.696 align:middle +Para ello, podemos añadir una acción. Se + +00:01:31.676 --> 00:01:36.896 align:middle +parece a esto: en la etiqueta a, +añade {{ stimulus_action() }} + +00:01:37.546 --> 00:01:42.866 align:middle +-otra función de acceso directo- y pásale el nombre +del controlador al que estás adjuntando la acción + +00:01:42.866 --> 00:01:48.876 align:middle +- song-controls - y luego un método dentro +de ese controlador que debe ser llamado + +00:01:48.876 --> 00:01:50.486 align:middle +cuando alguien haga clic en este elemento. + +00:01:50.916 --> 00:01:52.376 align:middle +¿Qué te parece play. + +00:01:53.406 --> 00:01:59.576 align:middle +Genial, ¿no? De vuelta en el controlador de la +canción, ya no necesitamos el método connect(): + +00:02:00.076 --> 00:02:05.006 align:middle +no necesitamos hacer nada cada vez +que veamos otra fila song-list. + +00:02:05.526 --> 00:02:08.366 align:middle +Pero sí necesitamos un método play(). + +00:02:08.366 --> 00:02:12.976 align:middle +Y al igual que con los oyentes de eventos +normales, éste recibirá un objeto event... + +00:02:13.416 --> 00:02:16.526 align:middle +y entonces podemos decir event.preventDefault() + +00:02:16.836 --> 00:02:20.286 align:middle +para que nuestro navegador no +intente seguir el clic del enlace. + +00:02:21.446 --> 00:02:24.206 align:middle +Para probar, console.log('Playing!'). + +00:02:26.576 --> 00:02:27.716 align:middle +¡Vamos a ver qué pasa! + +00:02:28.196 --> 00:02:29.476 align:middle +Refresca y... + +00:02:29.786 --> 00:02:32.726 align:middle +haz clic. Ya funciona. + +00:02:33.126 --> 00:02:37.256 align:middle +Así de fácil es enganchar un +receptor de eventos en Stimulus. + +00:02:37.736 --> 00:02:39.746 align:middle +Ah, y si inspeccionas este elemento... + +00:02:40.236 --> 00:02:44.296 align:middle +esa función stimulus_action() +es sólo un atajo + +00:02:44.296 --> 00:02:48.776 align:middle +para añadir un atributo especial +data-action que Stimulus entiende. + +00:02:50.316 --> 00:02:54.396 align:middle +Bien, ¿cómo podemos hacer una llamada +Ajax desde dentro del método play()? + +00:02:55.086 --> 00:02:58.826 align:middle +Bueno, podríamos utilizar la función +integrada fetch() de JavaScript. + +00:02:59.196 --> 00:03:03.836 align:middle +Pero en su lugar, voy a instalar una +biblioteca de terceros llamada Axios. + +00:03:04.796 --> 00:03:12.436 align:middle +En tu terminal, instálala diciendo: yarn add +axios --dev Ahora sabemos lo que hace esto: + +00:03:13.156 --> 00:03:16.756 align:middle +descarga este paquete en +nuestro directorio node_modules, + +00:03:17.376 --> 00:03:20.876 align:middle +y añade esta línea a +nuestro archivo package.json. + +00:03:21.726 --> 00:03:27.666 align:middle +Ah, y nota al margen: puedes utilizar +absolutamente jQuery dentro de Stimulus. + +00:03:28.166 --> 00:03:31.826 align:middle +No lo haré, pero funciona +muy bien - y puedes instalar - + +00:03:32.036 --> 00:03:35.246 align:middle +e importar - jQuery como +cualquier otro paquete. + +00:03:35.996 --> 00:03:38.036 align:middle +Hablamos de ello en nuestro +tutorial de Stimulus. + +00:03:38.906 --> 00:03:42.126 align:middle +Bien, ¿cómo utilizamos la biblioteca axios? + +00:03:42.576 --> 00:03:45.076 align:middle +Importándola + +00:03:45.076 --> 00:03:50.746 align:middle +Al principio de este archivo, ya hemos +importado la clase base Controller de stimulus. + +00:03:51.656 --> 00:03:55.176 align:middle +Ahora import axios from 'axios'. En + +00:03:56.456 --> 00:04:01.506 align:middle +cuanto lo hagamos, Webpack Encore +cogerá el código fuente de axios + +00:04:01.856 --> 00:04:05.236 align:middle +y lo incluirá en nuestros +archivos JavaScript construidos. + +00:04:05.966 --> 00:04:11.086 align:middle +Ahora, aquí abajo, podemos decir +axios.get() para hacer una pe tición GET. + +00:04:11.826 --> 00:04:14.656 align:middle +Pero... ¿qué debemos pasar para la URL? + +00:04:15.446 --> 00:04:19.906 align:middle +Tiene que ser algo como /api/songs/5... + +00:04:20.526 --> 00:04:24.906 align:middle +¿pero cómo sabemos cuál +es el "id" de esta fila? + +00:04:26.036 --> 00:04:30.706 align:middle +Una de las cosas más interesantes de +Stimulus es que te permite pasar valores + +00:04:30.706 --> 00:04:33.816 align:middle +de Tw ig a tu controlador Stimulus. + +00:04:34.896 --> 00:04:39.776 align:middle +Para ello, declara qué valores +quieres permitir que se pasen a + +00:04:39.776 --> 00:04:46.336 align:middle +través de una propiedad estática +especial: static values = {}. + +00:04:46.336 --> 00:04:52.746 align:middle +Dentro, vamos a permitir que +se pase un valor de infoUrl. + +00:04:53.616 --> 00:05:00.156 align:middle +Me acabo de inventar ese nombre: creo que +pasaremos la URL completa a la ruta de la API. + +00:05:01.026 --> 00:05:03.706 align:middle +Establece esto como el tipo que será. + +00:05:04.006 --> 00:05:05.096 align:middle +Así que, un String. + +00:05:07.026 --> 00:05:11.676 align:middle +Aprenderemos cómo pasamos este valor desde +Twig a nuestro controlador en un minuto. + +00:05:12.196 --> 00:05:19.816 align:middle +Pero como tenemos esto, abajo, podemos +referenciar el valor diciendo this.infoUrlValue. + +00:05:21.386 --> 00:05:23.426 align:middle +Entonces, ¿cómo lo pasamos? + +00:05:24.186 --> 00:05:29.636 align:middle +De vuelta en homepage.html.twig, añade un +segundo argumento a stimulus_controller(). + +00:05:30.456 --> 00:05:34.796 align:middle +Se trata de un array con los valores +que quieres pasar al controlador. + +00:05:35.296 --> 00:05:38.176 align:middle +Pasa a infoUrl el conjunto de la URL. + +00:05:39.176 --> 00:05:41.796 align:middle +Hmm, pero tenemos que generar esa URL. + +00:05:42.376 --> 00:05:44.186 align:middle +¿Esa ruta tiene ya un nombre? + +00:05:44.766 --> 00:05:49.716 align:middle +No Añade name: 'api_songs_get_one'. + +00:05:50.236 --> 00:05:52.866 align:middle +Perfecto. Copia eso... + +00:05:53.196 --> 00:05:59.356 align:middle +y de nuevo en la plantilla, pon infoURl +a path(), el nombre de la ruta... + +00:05:59.826 --> 00:06:02.536 align:middle +y luego un array con los comodines que haya. + +00:06:03.986 --> 00:06:06.496 align:middle +Nuestra ruta tiene un comodín id. + +00:06:07.526 --> 00:06:13.276 align:middle +En una aplicación real, estas rutas probablemente tendrían +cada una un id de base de datos que podríamos pasar. + +00:06:13.926 --> 00:06:15.496 align:middle +Nosotros aún no lo tenemos... + +00:06:15.496 --> 00:06:20.166 align:middle +así que para, en cierto modo, falsear +esto, voy a utilizar loop.index. + +00:06:20.316 --> 00:06:30.716 align:middle +Esta es una variable mágica de Twig : si estás dentro +de un bucle de Twig for , puedes acceder al índice + +00:06:30.846 --> 00:06:34.836 align:middle +-como 1, 2, 3, 4- utilizando loop.index. + +00:06:35.336 --> 00:06:37.766 align:middle +Así que vamos a utilizar esto +como una identificación falsa. + +00:06:38.236 --> 00:06:43.536 align:middle +Ah, y no olvides decir id: y luego loop.index. + +00:06:43.646 --> 00:06:44.526 align:middle +¡Hora de probar! + +00:06:45.186 --> 00:06:52.566 align:middle +Refresca. Lo primero que quiero que +veas es que, cuando pasamos infoUrl + +00:06:52.566 --> 00:06:54.856 align:middle +como segundo argumento a stimulus_controller, + +00:06:55.266 --> 00:07:01.766 align:middle +lo único que hace es dar salida a un atributo +muy especial data que Stimulus sabe leer. + +00:07:02.276 --> 00:07:04.996 align:middle +Así es como se pasa un valor a un controlador. + +00:07:05.736 --> 00:07:07.686 align:middle +Haz clic en uno de los +enlaces de reproducción y... + +00:07:08.436 --> 00:07:13.876 align:middle +lo tienes. ¡A cada objeto controlador +se le pasa su URL correcta! + +00:07:15.216 --> 00:07:17.826 align:middle +¡Vamos a celebrarlo haciendo +la llamada Ajax! Hazlo + +00:07:18.126 --> 00:07:25.976 align:middle +con axios.get(this.infoUrlValue) +-sí, acabo de escribirlo-, + +00:07:26.216 --> 00:07:32.556 align:middle +.then() y una devolución de llamada utilizando una +función de flecha que recibirá un argumento response. + +00:07:33.386 --> 00:07:36.256 align:middle +Esto se llamará cuando +termine la llamada Ajax. + +00:07:36.926 --> 00:07:38.666 align:middle +Registra la respuesta para empezar. + +00:07:40.066 --> 00:07:43.666 align:middle +Ah, y corrige para usar this.infoUrlValue. + +00:07:46.106 --> 00:07:47.636 align:middle +Muy bien, actualiza... + +00:07:47.786 --> 00:07:49.456 align:middle +y haz clic en el enlace de reproducción + +00:07:50.196 --> 00:07:52.596 align:middle +Sí! Se ha volcado la respuesta... + +00:07:53.156 --> 00:07:55.076 align:middle +y una de sus claves es data... + +00:07:55.336 --> 00:07:57.756 align:middle +¡que contiene el url! + +00:07:58.346 --> 00:08:00.226 align:middle +¡Es hora de dar la vuelta de la victoria! De + +00:08:01.366 --> 00:08:06.696 align:middle +vuelta a la función, podemos reproducir +ese audio creando un nuevo objeto Audio - + +00:08:07.146 --> 00:08:12.286 align:middle +es un objeto JavaScript normal - +pasándole response.data.url... + +00:08:13.656 --> 00:08:16.106 align:middle +y luego llamando a play() sobre éste. + +00:08:17.946 --> 00:08:18.796 align:middle +Y ahora... + +00:08:19.346 --> 00:08:20.786 align:middle +cuando le demos al play... + +00:08:22.936 --> 00:08:26.426 align:middle +¡por fin! Música para mis oídos. + +00:08:27.656 --> 00:08:31.266 align:middle +Si quieres saber más sobre Stimulus +-esto ha sido un poco rápido- + +00:08:31.496 --> 00:08:34.026 align:middle +tenemos un tutorial entero sobre él... + +00:08:34.026 --> 00:08:35.346 align:middle +y es genial. + +00:08:36.226 --> 00:08:41.456 align:middle +Para terminar este tutorial, vamos a +instalar otra biblioteca de JavaScript. + +00:08:42.166 --> 00:08:47.326 align:middle +Ésta hará que nuestra aplicación se sienta +instantáneamente como una aplicación de una sola página. + +00:08:47.716 --> 00:08:48.676 align:middle +Eso a continuación. diff --git a/sfcasts/ep1/es/stimulus.md b/sfcasts/ep1/es/stimulus.md new file mode 100644 index 0000000..06dbda1 --- /dev/null +++ b/sfcasts/ep1/es/stimulus.md @@ -0,0 +1,43 @@ +# Stimulus: Un JavaScript Sensato y Bonito + +Quiero hablar de Stimulus. Stimulus es una pequeña pero encantadora biblioteca de JavaScript que me encanta. Y Symfony tiene un soporte de primera clase para ella. También es muy utilizada por la comunidad de Ruby on Rails. + +## SPA vs. Aplicaciones "tradicionales" + +Hay dos filosofías en el desarrollo web. La primera es que devuelves el HTML de tu sitio, como hemos hecho en nuestra página de inicio y de navegación, y luego añades el comportamiento de JavaScript a ese HTML. La segunda filosofía es utilizar un marco de trabajo de JavaScript para construir todo tu HTML y JavaScript, lo que supone una aplicación de una sola página. + +La solución correcta depende de tu aplicación, pero a mí me gusta mucho el primer enfoque. Y utilizando Stimulus -así como otra herramienta de la que hablaremos en unos minutos llamada Turbo- podemos crear aplicaciones altamente interactivas que se ven y se sienten tan responsivas como una aplicación de una sola página. + +Tenemos un tutorial completo sobre Stimulus, pero vamos a probarlo. Ya puedes ver cómo funciona en el ejemplo de su documentación. Creas una pequeña clase JavaScript llamada controlador... y luego adjuntas ese controlador a uno o más elementos de la página. Y ya está Stimulus te permite adjuntar escuchas de eventos -como eventos de clic- y tiene otras cosas buenas. + +## Controladores Stimulus en nuestra aplicación + +En nuestra aplicación, cuando instalamos Encore, nos dio un directorio `controllers/`. Aquí es donde vivirán nuestros controladores Stimulus. Y en `app.js`, importamos`bootstrap.js`. No es un archivo que tengas que mirar mucho, pero es súper útil. Esto pone en marcha Stimulus -sí, ya está instalado- y registra todo lo que hay en el directorio `controllers/` como un controlador Stimulus. Esto significa que si quieres crear un nuevo controlador Stimulus, ¡sólo tienes que añadir un archivo a este directorio `controllers/`! + +Y obtenemos un controlador de Estímulos fuera de la caja llamado `hello_controller.js`. Todos los controladores de Estímulos siguen la práctica de nombrar algo con "guión bajo"`controller.js` o algo con guión `controller.js`. La parte que precede a `_controller` -por tanto, `hello` - se convierte en el nombre del controlador. + +## Adjuntar un controlador a un elemento + +Adjuntemos esto a un elemento. Abre `templates/vinyl/homepage.html.twig`. Veamos... en la parte principal de la página, voy a añadir un div... y luego para adjuntar el controlador a este elemento, añade `data-controller="hello"`. + +[[[ code('a818d762b0') ]]] + +¡Vamos a probarlo! Actualiza y... ¡sí! ¡Ha funcionado! El estímulo ha visto este elemento, ha instanciado el controlador... y luego nuestro código ha cambiado el contenido del elemento. El elemento al que está unido este controlador está disponible como `this.element`. + +## ¡El estímulo ve dinámicamente nuevos elementos! + +Así que... esto ya es muy bonito... porque conseguimos trabajar dentro de un objeto JavaScript ordenado... que está ligado a un elemento específico. + +Pero déjame mostrarte la parte más genial de Stimulus: lo que hace que cambie el juego. Inspecciona el elemento en las herramientas de tu navegador cerca del elemento. Voy a modificar el HTML del elemento padre. Justo encima de éste -aunque no importa dónde- añade otro elemento con `data-controller="hello"`. + +Y... ¡boom! ¡Vemos el mensaje! Esta es la característica estrella de Stimulus: puedes añadir estos elementos `data-controller` a la página cuando quieras. Por ejemplo, si haces una llamada Ajax... que añade HTML fresco a tu página, Stimulus se dará cuenta de ello y ejecutará los controladores a los que el nuevo HTML deba estar unido. Si alguna vez has tenido problemas en los que has añadido HTML a tu página mediante Ajax... pero el JavaScript de ese nuevo HTML está roto porque le faltan algunos escuchadores de eventos, pues Stimulus acaba de resolverlo. + +## La función stimulus_controller () + +Cuando usas Stimulus dentro de Symfony, obtenemos unas cuantas funciones de ayuda para hacernos la vida más fácil. Así, en lugar de escribir `data-controller="hello"` a mano, podemos decir`{{ stimulus_controller('hello') }}`. + +[[[ code('108098d4bf') ]]] + +Pero eso es sólo un atajo para renderizar ese atributo exactamente igual que antes. + +Bien, ahora que tenemos lo básico de Stimulus, vamos a utilizarlo para hacer algo real, como hacer una petición Ajax cuando hagamos clic en este icono de reproducción. Eso es lo siguiente. diff --git a/sfcasts/ep1/es/stimulus.vtt b/sfcasts/ep1/es/stimulus.vtt new file mode 100644 index 0000000..2ecd790 --- /dev/null +++ b/sfcasts/ep1/es/stimulus.vtt @@ -0,0 +1,251 @@ +WEBVTT + +00:00:01.136 --> 00:00:03.046 align:middle +Quiero hablar de Stimulus. + +00:00:03.346 --> 00:00:09.356 align:middle +Stimulus es una pequeña, pero deliciosa +biblioteca JavaScript que me encanta. + +00:00:09.626 --> 00:00:12.396 align:middle +Y Symfony tiene un soporte +de primera clase para ella. + +00:00:12.996 --> 00:00:16.536 align:middle +También es muy utilizada por +la comunidad de Ruby on Rails. + +00:00:17.526 --> 00:00:21.166 align:middle +Así que hay una especie de dos +filosofías en el desarrollo web. + +00:00:21.716 --> 00:00:25.246 align:middle +La primera es que devuelves el HTML de tu sitio, + +00:00:25.516 --> 00:00:28.336 align:middle +como hemos hecho en nuestra +página de inicio y de navegación. + +00:00:28.986 --> 00:00:32.406 align:middle +Y luego añades el comportamiento +de JavaScript a ese HTML. + +00:00:33.586 --> 00:00:37.796 align:middle +La segunda filosofía es utilizar un +marco de trabajo de JavaScript para + +00:00:38.056 --> 00:00:40.836 align:middle +construir todo tu HTML y JavaScript. + +00:00:41.506 --> 00:00:43.706 align:middle +Eso es una aplicación de una sola página. + +00:00:44.636 --> 00:00:49.446 align:middle +La solución correcta depende de tu aplicación, +pero me gusta mucho el primer enfoque. + +00:00:49.906 --> 00:00:55.606 align:middle +Y utilizando Stimulus -así como otra herramienta +de la que hablaremos en unos minutos, llamada Turbo- + +00:00:55.906 --> 00:01:02.396 align:middle +podemos crear aplicaciones altamente interactivas que se ven y se +sienten tan responsivas como una aplicación de una sola página. + +00:01:03.296 --> 00:01:07.666 align:middle +Tenemos un tutorial completo sobre +Stimulus, pero vamos a probarlo. + +00:01:08.596 --> 00:01:11.596 align:middle +Ya puedes ver cómo funciona en +el ejemplo de su documentación. + +00:01:12.236 --> 00:01:15.606 align:middle +Creas una pequeña clase +JavaScript llamada controlador... + +00:01:15.906 --> 00:01:20.616 align:middle +y luego adjuntas ese controlador a +uno o varios elementos de la página. + +00:01:21.316 --> 00:01:22.506 align:middle +Y ya está + +00:01:23.026 --> 00:01:28.356 align:middle +Stimulus te permite adjuntar escuchas de eventos +-como eventos de clic- y tiene otras cosas buenas. + +00:01:29.196 --> 00:01:33.956 align:middle +En nuestra aplicación, cuando instalamos +Encore, nos dio un directorio controllers/. + +00:01:34.116 --> 00:01:37.376 align:middle +Aquí es donde vivirán +nuestros controladores Stimulus. + +00:01:37.376 --> 00:01:41.776 align:middle +Y en app.js, importamos bootstrap.js. + +00:01:42.436 --> 00:01:47.266 align:middle +No es un archivo que necesites +mirar mucho, pero es súper útil. + +00:01:47.866 --> 00:01:53.396 align:middle +Esto inicia Stimulus -sí, ya está +instalado- y registra todo lo que + +00:01:53.396 --> 00:01:56.546 align:middle +hay en el directorio controllers/ +como un controlador Stimulus. + +00:01:57.186 --> 00:02:00.306 align:middle +Esto significa que si quieres crear +un nuevo controlador de Stimulus, + +00:02:00.756 --> 00:02:04.506 align:middle +¡sólo tienes que añadir un archivo +a este directorio controllers/! + +00:02:05.026 --> 00:02:09.206 align:middle +Y obtenemos un controlador de Estímulos +fuera de la caja llamado hello_controller.js. + +00:02:09.876 --> 00:02:13.836 align:middle +Todos los controladores de Estímulos +siguen la práctica de nombrar algo + +00:02:13.946 --> 00:02:18.596 align:middle +con "guión bajo" controller.js +o algo con guión controller.js. + +00:02:19.146 --> 00:02:25.046 align:middle +La parte que precede a _controller -por tanto, +hello - se convierte en el nombre del controlador. + +00:02:26.086 --> 00:02:27.786 align:middle +Adjuntemos esto a un elemento. + +00:02:28.696 --> 00:02:31.886 align:middle +Abre templates/vinyl/homepage.html.twig. + +00:02:32.956 --> 00:02:33.706 align:middle +Veamos... + +00:02:33.706 --> 00:02:37.756 align:middle +en la parte principal de la +página, voy a añadir un div... + +00:02:38.136 --> 00:02:44.276 align:middle +y luego para adjuntar el controlador a este +elemento, añado data-controller="hello". + +00:02:46.616 --> 00:02:47.846 align:middle +¡Vamos a probarlo! + +00:02:49.146 --> 00:02:50.346 align:middle +Actualiza y... + +00:02:51.046 --> 00:02:52.746 align:middle +¡sí! ¡Ha funcionado! + +00:02:52.986 --> 00:02:56.826 align:middle +El estímulo vio este elemento, +instanció el controlador... + +00:02:57.306 --> 00:03:00.256 align:middle +y entonces nuestro código +cambió el contenido del elemento. + +00:03:01.506 --> 00:03:07.296 align:middle +El elemento al que está unido este +controlador está disponible como this.element. + +00:03:08.076 --> 00:03:11.246 align:middle +Así que... esto ya es muy bonito... + +00:03:11.536 --> 00:03:15.276 align:middle +porque conseguimos trabajar dentro +de un bonito objeto JavaScript... + +00:03:15.386 --> 00:03:17.876 align:middle +que está vinculado a un elemento concreto. + +00:03:18.426 --> 00:03:24.116 align:middle +Pero déjame que te muestre la parte más genial +de Stimulus: lo que hace que cambie el juego. + +00:03:25.136 --> 00:03:28.236 align:middle +Inspecciona el elemento en las herramientas +de tu navegador cerca del elemento. + +00:03:28.386 --> 00:03:32.656 align:middle +Voy a modificar el HTML del elemento padre. + +00:03:34.136 --> 00:03:36.796 align:middle +Justo encima de éste +-aunque no importa dónde- + +00:03:37.146 --> 00:03:41.526 align:middle +añade otro elemento con +data-controller="hello". + +00:03:44.776 --> 00:03:46.726 align:middle +Y... ¡boom! + +00:03:47.046 --> 00:03:48.546 align:middle +¡Vemos el mensaje! + +00:03:48.866 --> 00:03:51.516 align:middle +Esta es la función asesina de Stimulus: + +00:03:52.226 --> 00:03:57.996 align:middle +puedes añadir estos elementos de +data-controller en la página cuando quieras. + +00:03:58.696 --> 00:04:01.736 align:middle +Por ejemplo, si haces una llamada Ajax... + +00:04:01.976 --> 00:04:09.406 align:middle +que añada nuevo HTML a tu página, Stimulus se +dará cuenta de ello y ejecutará los controladores + +00:04:09.406 --> 00:04:12.506 align:middle +a los que se deba adjuntar el nuevo HTML. + +00:04:13.316 --> 00:04:18.576 align:middle +Si alguna vez has tenido problemas al +añadir HTML a tu página mediante Ajax... + +00:04:18.726 --> 00:04:22.426 align:middle +pero el JavaScript de ese nuevo HTML está roto + +00:04:22.776 --> 00:04:27.726 align:middle +porque le faltan algunos escuchadores de +eventos, pues bien, Stimulus acaba de resolverlo. + +00:04:29.026 --> 00:04:34.046 align:middle +Cuando se utiliza Stimulus dentro de Symfony, se obtienen +unas cuantas funciones de ayuda para facilitar la vida. + +00:04:34.496 --> 00:04:38.746 align:middle +Así, en lugar de escribir +data-controller="hello" a mano, + +00:04:39.106 --> 00:04:42.456 align:middle +podemos decir {{ +stimulus_controller('hello') }}. + +00:04:43.626 --> 00:04:48.446 align:middle +Pero eso es sólo un atajo para renderizar +ese atributo exactamente igual que antes. + +00:04:49.396 --> 00:04:54.846 align:middle +Bien, ahora que tenemos los fundamentos de +Stimulus, usémoslo para hacer algo real, + +00:04:55.306 --> 00:04:59.186 align:middle +como hacer una petición Ajax cuando +hagamos clic en este icono de reproducción. + +00:04:59.706 --> 00:05:00.706 align:middle +Eso es lo siguiente. diff --git a/sfcasts/ep1/es/turbo.md b/sfcasts/ep1/es/turbo.md new file mode 100644 index 0000000..cfd3efc --- /dev/null +++ b/sfcasts/ep1/es/turbo.md @@ -0,0 +1,78 @@ +# Turbo: Supercarga tu aplicación + +Bienvenido al último capítulo de nuestro tutorial de introducción a Symfony 6. Si estás viendo esto, ¡lo estás petando! Y es hora de celebrarlo instalando un paquete más de Symfony. Pero antes de hacerlo, como sabes, me gusta confirmar todo primero... por si el nuevo paquete instala una receta interesante: + +```terminal-silent +git add . +git commit -m "Never gonna let you go..." +``` + +## Instalando symfony/ux-turbo + +Bien, vamos a instalar el nuevo paquete: + +```terminal +composer require symfony/ux-turbo +``` + +¿Ves ese "ux" en el nombre del paquete? Symfony UX es un conjunto de bibliotecas que añaden funcionalidad JavaScript a tu aplicación... a menudo con algo de código PHP para ayudar. Por ejemplo, hay una biblioteca para renderizar gráficos... y otra para usar un Cropper de imágenes con el sistema de formularios. + +## Recetas UX de Symfony + +Así que, como puedes ver, esto instaló una receta. OoOOo. Ejecuta + +```terminal +git status +``` + +para que podamos ver lo que ha hecho. La mayor parte es normal, como `config/bundles.php`que significa que habilitó el nuevo bundle. Los dos cambios interesantes son`assets/controllers.json` y `package.json`. Comprobemos primero `package.json`. + +Cuando instalas un paquete UX, lo que suele significar es que te estás integrando con una biblioteca JavaScript de terceros. Y así, la receta de ese paquete añade esa biblioteca a tu código. En este caso, la biblioteca JavaScript con la que nos estamos integrando se llama `@hotwired/turbo`. Además, el propio paquete PHP `symfony/ux-turbo` viene con algo de JavaScript adicional. Esta línea especial dice + +> ¡Hey Node! Quiero incluir un paquete llamado `@symfony/ux-turbo`... pero en lugar de +> de descargarlo, puedes encontrar su código en el directorio +> directorio `vendor/symfony/ux-turbo/Resources/assets`. + +Puedes buscar literalmente en esa ruta `vendor/symfony/ux-turbo/Resources/assets` +para encontrar un mini paquete JavaScript. Ahora, debido a que esto actualizó nuestro archivo `package.json`, tenemos que volver a instalar nuestras dependencias para descargarlo y tenerlo todo listo. + +De hecho, busca tu terminal que está ejecutando `yarn watch`. Tenemos un error! Dice que no se puede encontrar el archivo `@symfony/ux-turbo/package.json`, intenta ejecutar`yarn install --force`. + +¡Vamos a hacerlo! Pulsa control+C para detener esto... y luego ejecuta + +```terminal +yarn install --force +``` + +o `npm install --force`. Luego, reinicia Encore con: + +```terminal +yarn watch +``` + +El otro archivo que la receta modificó fue `assets/controllers.json`. Vamos a echarle un vistazo: `assets/controllers.json`. Esta es otra cosa que es exclusiva de Symfony UX. Normalmente, si queremos añadir un controlador Stimulus, lo ponemos en el directorio`controllers/`. Pero a veces, puede que instalemos un paquete PHP y que queramos añadir su propio controlador Stimulus en nuestra aplicación. Esta sintaxis dice básicamente + +> ¡Hey Stimulus! Ve a cargar este controlador Stimulus desde ese nuevo +> `@symfony/ux-turbo` paquete. + +Ahora bien, este controlador Stimulus en particular es un poco raro. No es uno que vayamos a utilizar directamente dentro de la función `stimulus_controller()` Twig. Es una especie de controlador falso. ¿Qué hace? Sólo con que se cargue, va a activar la biblioteca Turbo. + +## ¡Hola Turbo! Por la actualización de la página completa + +Sigo hablando de Turbo. ¿Qué es Turbo? Bueno, al ejecutar ese comando composer require... y luego reinstalar yarn, el JavaScript de Turbo está ahora activo y funcionando en nuestro sitio. ¿Qué hace? Es sencillo: convierte cada clic en un enlace y cada envío de un formulario de nuestro sitio en una llamada Ajax. Y eso hace que nuestro sitio sea rápido como un rayo. + +Compruébalo. Haz una última actualización completa. Y luego observa... si hago clic en Examinar, ¡no hay actualización completa de la página! Si hago clic en estos iconos, ¡no hay actualización! Turbo intercepta esos clics, hace una llamada Ajax a la URL, y luego pone ese HTML en nuestro sitio. Esto es enorme porque, de repente, nuestra aplicación se ve y se siente como una aplicación de una sola página... ¡sin que nosotros hagamos nada! + +## La barra de herramientas de depuración web y el perfilador de peticiones Ajax + +Ahora, otra cosa interesante que notarás es que, aunque las recargas de páginas completas han desaparecido, estas llamadas Ajax aparecen en la barra de herramientas de depuración web. Y puedes hacer clic para ir a ver el perfil de esa llamada Ajax muy fácilmente. Esta parte de la barra de herramientas de depuración web es aún más útil con las llamadas Ajax para una ruta de la API. Si pulsamos el icono de reproducción... ese 7 acaba de subir a 8... ¡y aquí está el perfilador de esa petición de la API! Abriré ese enlace en una nueva ventana. Esa es una forma súper fácil de llegar al perfilador de cualquier petición Ajax. + +Así que Turbo es increíble... y puede hacer más. Hay algunas cosas que debes saber sobre él antes de enviarlo a producción, y si te interesa, ¡sí! tenemos un tutorial completo sobre Turbo. Quería mencionarlo en este tutorial porque Turbo es más fácil si lo añades a tu aplicación desde el principio. + +Muy bien, ¡felicidades! ¡El primer tutorial de Symfony 6 está en los libros! Date una palmadita en la espalda... o mejor, busca a un amigo y choca los cinco. + +¡Y sigue adelante! Acompáñanos en el siguiente tutorial de esta serie, que te hará pasar de ser un desarrollador de Symfony en ciernes a alguien que realmente entiende lo que está pasando. Cómo funcionan los servicios, el sentido de todos estos archivos de configuración, los entornos Symfony, las variables de entorno y mucho más. Básicamente todo lo que necesitarás para hacer lo que quieras con Symfony. + +Y si tienes alguna pregunta o idea, estamos aquí para ti en la sección de comentarios debajo del vídeo. + +Muy bien amigos, ¡hasta la próxima! diff --git a/sfcasts/ep1/es/turbo.vtt b/sfcasts/ep1/es/turbo.vtt new file mode 100644 index 0000000..9081429 --- /dev/null +++ b/sfcasts/ep1/es/turbo.vtt @@ -0,0 +1,347 @@ +WEBVTT + +00:00:01.186 --> 00:00:05.446 align:middle +Bienvenido al último capítulo de nuestro +tutorial de introducción a Symfony 6. + +00:00:06.026 --> 00:00:08.326 align:middle +Si estás viendo esto, ¡lo estás petando! + +00:00:08.326 --> 00:00:12.686 align:middle +Y es hora de celebrarlo instalando +un paquete más de Symfony. + +00:00:13.106 --> 00:00:18.076 align:middle +Pero antes de hacerlo, como sabes, +me gusta confirmar todo primero... + +00:00:18.366 --> 00:00:25.076 align:middle +por si el nuevo paquete instala una receta +interesante: Bien, vamos a instalar el nuevo paquete: + +00:00:25.626 --> 00:00:33.216 align:middle +composer require symfony/ux-turbo ¿Ves +ese "ux" en el nombre del paquete? + +00:00:33.716 --> 00:00:39.056 align:middle +Symfony UX es un conjunto de bibliotecas que +añaden funcionalidad JavaScript a tu aplicación... + +00:00:39.656 --> 00:00:41.816 align:middle +a menudo con algo de código PHP para ayudar. + +00:00:42.576 --> 00:00:45.816 align:middle +Por ejemplo, hay una biblioteca +para renderizar gráficos... + +00:00:45.936 --> 00:00:49.806 align:middle +y otra para usar un Recortador de +imágenes con el sistema de formularios. + +00:00:50.526 --> 00:00:53.836 align:middle +Así que, como puedes ver, +esto instaló una receta. + +00:00:54.246 --> 00:00:58.486 align:middle +OoOOo. Ejecuta git status para +que podamos ver lo que ha hecho. + +00:00:59.236 --> 00:01:05.096 align:middle +La mayor parte es normal, como config/bundles.php +significa que habilitó el nuevo paquete. + +00:01:05.746 --> 00:01:11.716 align:middle +Los dos cambios interesantes son +assets/controllers.json y package.json. + +00:01:12.936 --> 00:01:15.056 align:middle +Comprobemos primero package.json. + +00:01:16.156 --> 00:01:21.346 align:middle +Cuando instalas un paquete UX, lo que suele +significar es que te estás integrando + +00:01:21.346 --> 00:01:23.866 align:middle +con una biblioteca JavaScript de terceros. + +00:01:24.156 --> 00:01:28.796 align:middle +Y así, la receta de ese paquete +añade esa biblioteca a tu código. + +00:01:28.796 --> 00:01:35.516 align:middle +En este caso, la biblioteca JavaScript con la que +nos estamos integrando se llama @hotwired/turbo. + +00:01:36.366 --> 00:01:43.796 align:middle +Además, el propio paquete PHP symfony/ux-turbo +viene con algo de JavaScript adicional. + +00:01:44.576 --> 00:01:46.876 align:middle +Esta línea especial dice: ¡Hey Node! + +00:01:47.076 --> 00:01:51.626 align:middle +Quiero incluir un paquete +llamado @symfony/ux-turbo... + +00:01:51.826 --> 00:01:56.416 align:middle +pero en lugar de descargarlo, +puedes encontrar su código + +00:01:56.416 --> 00:02:00.276 align:middle +en el directorio +vendor/symfony/ux-turbo/Resources/assets. + +00:02:01.056 --> 00:02:06.166 align:middle +Puedes buscar literalmente en esa ruta +vendor/symfony/ux-turbo/Resources/assets + +00:02:06.576 --> 00:02:08.886 align:middle +para encontrar un mini paquete JavaScript. + +00:02:10.016 --> 00:02:16.346 align:middle +Ahora, debido a que esto actualizó nuestro archivo +package.json, tenemos que volver a instalar nuestras dependencias + +00:02:16.546 --> 00:02:18.376 align:middle +para descargar y configurar todo esto. + +00:02:19.406 --> 00:02:22.716 align:middle +De hecho, busca tu terminal que +está ejecutando yarn watch. + +00:02:23.386 --> 00:02:24.896 align:middle +¡Tenemos un error! + +00:02:25.546 --> 00:02:30.786 align:middle +Dice que no se puede encontrar el +archivo @symfony/ux-turbo/package.json, + +00:02:31.276 --> 00:02:34.386 align:middle +intenta ejecutar yarn install --force. + +00:02:35.226 --> 00:02:35.856 align:middle +¡Vamos a hacerlo! + +00:02:36.536 --> 00:02:38.386 align:middle +Pulsa control+C para detener esto... + +00:02:39.056 --> 00:02:44.086 align:middle +y luego ejecuta yarn install +--force o npm install --force. + +00:02:45.876 --> 00:02:52.546 align:middle +A continuación, reinicia Encore con: yarn +watch El otro archivo que la receta modificó + +00:02:52.546 --> 00:02:55.126 align:middle +fue assets/controllers.json. + +00:02:55.916 --> 00:02:59.726 align:middle +Vamos a echarle un vistazo: +assets/controllers.json. + +00:03:00.586 --> 00:03:04.076 align:middle +Esta es otra cosa única de Symfony UX. + +00:03:04.836 --> 00:03:07.796 align:middle +Normalmente, si queremos +añadir un controlador Stimulus, + +00:03:08.046 --> 00:03:10.286 align:middle +lo ponemos en el directorio controllers/. + +00:03:10.936 --> 00:03:16.946 align:middle +Pero a veces, puede que instalemos +un paquete PHP y que queramos + +00:03:16.946 --> 00:03:20.586 align:middle +añadir su propio controlador +Stimulus en nuestra aplicación. + +00:03:21.516 --> 00:03:24.616 align:middle +Esta sintaxis dice +básicamente: ¡Hey Stimulus! + +00:03:24.836 --> 00:03:30.956 align:middle +Ve a cargar este controlador Stimulus +desde ese nuevo paquete @symfony/ux-turbo. + +00:03:31.726 --> 00:03:35.816 align:middle +Ahora bien, este controlador Stimulus +en particular es un poco raro. + +00:03:36.506 --> 00:03:39.536 align:middle +No es uno que vayamos a +utilizar directamente dentro + +00:03:39.536 --> 00:03:42.716 align:middle +de la función stimulus_controller() Twig. + +00:03:43.446 --> 00:03:45.846 align:middle +Es una especie de controlador falso. + +00:03:46.646 --> 00:03:47.656 align:middle +¿Qué hace? + +00:03:48.306 --> 00:03:53.156 align:middle +Sólo con que se cargue, va a +activar la biblioteca Turbo. + +00:03:53.996 --> 00:03:55.906 align:middle +Así que sigo hablando de Turbo. + +00:03:56.216 --> 00:03:57.926 align:middle +¿Qué es Turbo? + +00:03:58.496 --> 00:04:01.016 align:middle +Bueno, al ejecutar ese +comando composer require... + +00:04:01.256 --> 00:04:07.516 align:middle +y volviendo a instalar Yarn, el JavaScript de Turbo +ya está activo y funcionando en nuestro sitio. + +00:04:08.356 --> 00:04:09.746 align:middle +¿Qué hace? + +00:04:10.306 --> 00:04:17.776 align:middle +Es sencillo: convierte cada clic en un enlace y +cada envío de un formulario en una llamada Ajax. + +00:04:18.486 --> 00:04:21.466 align:middle +Y eso hace que nuestro sitio +sea rápido como un rayo. + +00:04:22.176 --> 00:04:22.686 align:middle +Compruébalo. + +00:04:23.196 --> 00:04:25.226 align:middle +Haz una última actualización completa. + +00:04:26.086 --> 00:04:27.106 align:middle +Y luego observa... + +00:04:27.806 --> 00:04:32.026 align:middle +si hago clic en Examinar, ¡no hay +actualización completa de la página! + +00:04:32.886 --> 00:04:35.686 align:middle +Si hago clic en estos iconos, +¡no hay actualización! + +00:04:36.346 --> 00:04:40.766 align:middle +Turbo intercepta esos clics, +hace una llamada Ajax a la URL + +00:04:41.006 --> 00:04:43.916 align:middle +y luego pone ese HTML en nuestro sitio. + +00:04:44.846 --> 00:04:52.676 align:middle +Esto es enorme porque, de forma gratuita, nuestra aplicación +se ve y se siente como una aplicación de una sola página... + +00:04:52.916 --> 00:04:54.766 align:middle +¡sin que nosotros hagamos nada! + +00:04:55.566 --> 00:05:00.316 align:middle +Ahora, otra cosa interesante que notarás es que, aunque +las recargas de páginas completas han desaparecido, + +00:05:00.816 --> 00:05:04.796 align:middle +estas llamadas Ajax aparecen en la barra +de herramientas de depuración web. + +00:05:05.366 --> 00:05:09.906 align:middle +Y puedes hacer clic para ir a ver el +perfil de esa llamada Ajax muy fácilmente. + +00:05:10.976 --> 00:05:18.166 align:middle +Esta parte de la barra de herramientas de depuración web es +aún más útil con las llamadas Ajax para una ruta de la API. + +00:05:19.106 --> 00:05:20.856 align:middle +Si pulsamos el icono de reproducción... + +00:05:21.416 --> 00:05:23.316 align:middle +ese 7 acaba de subir a 8... + +00:05:23.986 --> 00:05:27.476 align:middle +¡y aquí está el perfilador +de esa petición de la API! + +00:05:28.206 --> 00:05:30.106 align:middle +Abriré ese enlace en una nueva ventana. + +00:05:31.166 --> 00:05:35.856 align:middle +Esa es una forma súper fácil de llegar +al perfilador de cualquier petición Ajax. + +00:05:36.756 --> 00:05:38.796 align:middle +Así que Turbo es increíble... + +00:05:39.176 --> 00:05:40.936 align:middle +y puede hacer más. + +00:05:41.646 --> 00:05:44.636 align:middle +Hay algunas cosas que debes +saber sobre él antes de enviarlo + +00:05:44.636 --> 00:05:47.866 align:middle +a producción, y si estás interesado, ¡sí! + +00:05:48.196 --> 00:05:50.586 align:middle +Tenemos un tutorial completo sobre Turbo. + +00:05:51.206 --> 00:05:55.426 align:middle +Quería mencionarlo en este +tutorial porque Turbo es más fácil + +00:05:55.596 --> 00:05:57.726 align:middle +si lo añades a tu aplicación +desde el principio. + +00:05:58.576 --> 00:06:00.726 align:middle +Muy bien, ¡felicidades! + +00:06:00.936 --> 00:06:04.476 align:middle +¡El primer tutorial de +Symfony 6 está en los libros! + +00:06:04.846 --> 00:06:06.046 align:middle +Date una palmadita en la espalda... + +00:06:06.506 --> 00:06:10.076 align:middle +o mejor, busca a un amigo y choca los cinco. + +00:06:10.906 --> 00:06:12.326 align:middle +¡Y sigue adelante! Acompáñanos + +00:06:12.986 --> 00:06:16.326 align:middle +en el siguiente tutorial +de esta serie, que te hará + +00:06:16.326 --> 00:06:21.446 align:middle +pasar de ser un desarrollador de Symfony en ciernes a +alguien que realmente entiende lo que está pasando. + +00:06:22.176 --> 00:06:27.546 align:middle +Cómo funcionan los servicios, el sentido de todos estos +archivos de configuración, los entornos Symfony, las + +00:06:27.696 --> 00:06:30.326 align:middle +variables de entorno y mucho más. + +00:06:30.876 --> 00:06:34.986 align:middle +Básicamente todo lo que necesitarás +para hacer lo que quieras con Symfony. + +00:06:35.526 --> 00:06:38.876 align:middle +Y si tienes alguna pregunta +o idea, estamos aquí para ti + +00:06:38.876 --> 00:06:41.296 align:middle +en la sección de comentarios +debajo del vídeo. + +00:06:42.416 --> 00:06:44.126 align:middle +Muy bien amigos, ¡hasta la próxima! diff --git a/sfcasts/ep1/es/twig-inheritance.md b/sfcasts/ep1/es/twig-inheritance.md new file mode 100644 index 0000000..cf1ab6d --- /dev/null +++ b/sfcasts/ep1/es/twig-inheritance.md @@ -0,0 +1,101 @@ +# Herencia Twig + +Dirígete a https://twig.symfony.com... y haz clic para consultar su documentación. Hay mucho material bueno aquí. Pero lo que quiero que hagas es que te desplaces hasta la referencia a Twig. ¡Sí! + +## Etiquetas + +Lo primero que debes mirar, a la izquierda, son estas cosas llamadas etiquetas. Esta lista representa todas las cosas posibles que puedes utilizar con la sintaxis de hacer algo. Sí, siempre será `{%` y luego una de estas cosas, como `for` o `if`. Y sinceramente, sólo vas a utilizar unas 5 de ellas en el día a día. Si quieres saber la sintaxis de uno de ellos, sólo tienes que hacer clic para ver su documentación. + +## Filtros + +Además de las 20 etiquetas, Twig también tiene algo llamado filtros. Los filtros son básicamente funciones, pero con una sintaxis más moderna. Twig también tiene funciones, pero son menos: Twig prefiere los filtros: ¡son mucho más chulos! + +Por ejemplo, hay un filtro llamado `upper`. Usar un filtro es como usar la tecla`|` en la línea de comandos. Tienes un valor y luego lo "canalizas" en el filtro que quieres, como `upper`. + +¡Vamos a probar esto! Imprime `track.artist|upper`. + +[[[ code('73974377cd') ]]] + +Y ahora... ¡está en mayúsculas! Si quieres confundir a tus compañeros de trabajo, puedes canalizarlo a `lower`... que devuelve las cosas a minúsculas. No hay ninguna razón real para hacer esto, pero los filtros pueden encadenarse así. + +[[[ code('016bee2e4d') ]]] + +De todos modos, echa un vistazo a la lista de filtros porque probablemente haya algo que te resulte útil. + +Y... ¡eso es todo! Además de las funciones, también hay algo llamado "pruebas", que son útiles en las sentencias if: puedes decir cosas como "si el número es divisible por 5". + +## Herencia de Plantillas + +Vale, sólo una cosa más que aprender sobre Twig, y es genial. + +Mira el código fuente HTML de la página. Fíjate en que no hay estructura HTML: no hay etiquetas `html`, `head` o `body`. Literalmente el HTML que tenemos dentro de nuestra plantilla, es lo que obtenemos. Nada más. + +Entonces, ¿hay... algún tipo de sistema de diseño en Twig en el que podamos añadir un diseño base a nuestro alrededor? Por supuesto. Y es increíble. Se llama herencia de plantillas. Si tienes una plantilla y quieres que utilice algún diseño base, en la parte superior del archivo, utiliza una etiqueta "hacer algo" llamada `extends`. Pásale el nombre del archivo de diseño: `base.html.twig`. + +[[[ code('48dc6d8fec') ]]] + +Esto se refiere a esta plantilla de aquí. Antes de comprobarlo, si lo intentamos ahora, ¡vaya! Gran error: + +> Una plantilla que extiende otra no puede incluir contenido fuera de los bloques Twig. + +Para saber qué significa esto, abre `base.html.twig`. Este es tu archivo de diseño base... y eres totalmente libre de personalizarlo como quieras. Ahora mismo... es en su mayor parte sólo etiquetas HTML aburridas... excepto por una serie de estos "bloques". + +Los bloques son básicamente "agujeros" en los que una plantilla hija puede colocar contenido. Permíteme explicarlo de otra manera. Cuando decimos `extends 'base.html.twig'`, eso dice básicamente: + +> ¡Yo Twig! Cuando renderices esta plantilla, quiero que realmente renderices +> `base.html.twig`... y luego pongas mi contenido dentro de ella. + +Twig responde educadamente: + +> Vale, genial... Puedo hacerlo. Pero, ¿en qué parte de `base.html.twig` quieres que ponga +> todo tu contenido? ¿Quieres que lo ponga al final de la página? ¿En la parte +> parte superior? ¿En algún lugar al azar en el medio? + +La forma de decirle a Twig dónde poner nuestro contenido dentro de `base.html.twig` es anulando un bloque. Fíjate en que `base.html.twig` ya tiene un bloque llamado `body`... y ahí es justo donde queremos poner el HTML de nuestra plantilla. + +Para ponerlo ahí, en nuestra plantilla, rodea todo el contenido con`{% block body %}`... y luego `{% endblock %}`. + +[[[ code('01d2fbf6f5') ]]] + +A esto se le llama herencia de la plantilla porque estamos sobrescribiendo ese bloque `body` con este nuevo contenido. Así que ahora, cuando Twig renderice `base.html.twig`... y llegue a esta parte `block body`, va a imprimir el HTML `block body` de nuestra plantilla + +Observa: actualiza y... el error ha desaparecido. Y si ves el código fuente de la página, ¡tenemos una página HTML completa! + +## Nombres de los bloques + +Ah, y los nombres de estos bloques no son importantes. Si quieres cambiarles el nombre por el de tu personaje favorito de una sitcom de los 90, hazlo. Sólo recuerda actualizar también su nombre en cualquier plantilla hija. + +También puedes añadir más bloques. Cada bloque que añadas es otro punto de anulación potencial. + +## Contenido del bloque por defecto + +Ah, y habrás notado que los bloques pueden tener contenido por defecto. Mira la página ahora mismo: el título dice "Bienvenido". Eso es porque el bloque `title` tiene un contenido por defecto... y no lo vamos a anular. Vamos a cambiar el título por defecto a "Vinilo mixto". + +[[[ code('086e2afb43') ]]] + +Así que ahora ese será el título de todas las páginas de nuestro sitio... a menos que lo anulemos. En nuestra plantilla, ya sea por encima del cuerpo del bloque o por debajo -el orden de los bloques no importa-, añade `{% block title %}`, `{% endblock %}` y, en medio, "Crear un nuevo disco". + +[[[ code('4ebc00a77a') ]]] + +Y ahora... ¡sí! Esta página tiene un título personalizado. + +## Añadir al bloque padre (en lugar de sustituirlo) + +Ah, y puede que te preguntes + +> ¿Qué pasa si no quiero sustituir un bloque por completo.... sino que quiero +> añadir a un bloque? + +Eso es totalmente posible. En `base.html.twig`, el bloque `title` está configurado como "Vinilo mixto". Si quisiéramos añadirle nuestro título personalizado, podríamos decir "Crear un nuevo disco" y luego utilizar la etiqueta "decir algo" para imprimir una función llamada `parent()`. + +[[[ code('9729cce31f') ]]] + +Eso hace exactamente lo que esperarías: encuentra el contenido de la plantilla padre para este bloque... y lo imprime. Actualiza y... eso es muy bonito. + +## La herencia de plantillas es la herencia de clases + +Si alguna vez estás confundido sobre cómo funciona la herencia de plantillas, es útil, al menos para mí, pensar en ella exactamente como en la herencia orientada a objetos. Cada plantilla es como una clase y cada bloque es como un método. Así, la "clase" de la página de inicio extiende la "clase" de `base.html.twig`, pero anula dos de sus métodos. Si eso sólo te ha confundido, no te preocupes. + +Así que... eso es todo para Twig. Básicamente eres un experto en Twig, lo que me han dicho que es un tema popular en las fiestas. + +A continuación: una de las características más destacadas de Symfony son sus herramientas de depuración. Vamos a instalarlas y a comprobarlas. \ No newline at end of file diff --git a/sfcasts/ep1/es/twig-inheritance.vtt b/sfcasts/ep1/es/twig-inheritance.vtt new file mode 100644 index 0000000..50b5315 --- /dev/null +++ b/sfcasts/ep1/es/twig-inheritance.vtt @@ -0,0 +1,396 @@ +WEBVTT + +00:00:01.106 --> 00:00:03.686 align:middle +Dirígete a https://twig.symfony.com... + +00:00:03.926 --> 00:00:06.366 align:middle +y haz clic para consultar su documentación. + +00:00:08.136 --> 00:00:09.886 align:middle +Hay mucho material bueno aquí. + +00:00:10.196 --> 00:00:14.356 align:middle +Pero lo que quiero que hagas es que te +desplaces hasta la referencia a Twig. + +00:00:14.546 --> 00:00:20.886 align:middle +¡Sí! Lo primero que debes mirar, a la +izquierda, son estas cosas llamadas etiquetas. + +00:00:21.466 --> 00:00:27.626 align:middle +Esta lista representa todas las cosas posibles +que puedes utilizar con la sintaxis de hacer algo. + +00:00:28.286 --> 00:00:36.136 align:middle +Sí, siempre será {% y luego una +de estas cosas, como for o if. + +00:00:36.666 --> 00:00:42.006 align:middle +Y sinceramente, sólo vas a utilizar +unas 5 de ellas en el día a día. + +00:00:42.006 --> 00:00:47.666 align:middle +Si quieres saber la sintaxis de alguna de ellas, sólo +tienes que hacer clic para ver su documentación. + +00:00:49.436 --> 00:00:54.236 align:middle +Además de las 20 etiquetas, Twig +también tiene algo llamado filtros. + +00:00:54.426 --> 00:00:56.426 align:middle +Estos son una maravilla. + +00:00:56.636 --> 00:01:02.286 align:middle +Los filtros son básicamente funciones, +pero con una sintaxis más moderna. + +00:01:02.996 --> 00:01:06.686 align:middle +Twig también tiene funciones, pero son menos: + +00:01:07.196 --> 00:01:10.636 align:middle +Twig prefiere los filtros: +¡son mucho más chulos! + +00:01:10.696 --> 00:01:14.386 align:middle +Por ejemplo, hay un filtro llamado upper. + +00:01:15.076 --> 00:01:19.126 align:middle +Usar un filtro es como usar la +tecla | en la línea de comandos. + +00:01:19.436 --> 00:01:25.496 align:middle +Tienes un valor y luego lo "canalizas" +en el filtro que quieres, como upper. + +00:01:26.606 --> 00:01:28.036 align:middle +¡Vamos a probar esto! + +00:01:28.296 --> 00:01:33.006 align:middle +Imprime track.artist|upper. + +00:01:33.006 --> 00:01:33.856 align:middle +Y ahora... + +00:01:34.556 --> 00:01:35.866 align:middle +¡está en mayúsculas! + +00:01:35.946 --> 00:01:42.846 align:middle +Si quieres confundir a tus compañeros +de trabajo, puedes canalizarlo a lower... + +00:01:43.206 --> 00:01:45.596 align:middle +que devuelve las cosas a las minúsculas. + +00:01:46.126 --> 00:01:51.186 align:middle +No hay ninguna razón real para hacer esto, +pero los filtros pueden encadenarse así. + +00:01:52.236 --> 00:01:57.386 align:middle +De todos modos, echa un vistazo a la lista de filtros +porque probablemente haya algo que te resulte útil. + +00:01:58.156 --> 00:02:00.136 align:middle +Y... ¡eso es todo! + +00:02:00.806 --> 00:02:06.586 align:middle +Además de las funciones, también hay algo llamado +"pruebas", que son útiles en las sentencias if: + +00:02:07.206 --> 00:02:13.306 align:middle +puedes decir cosas como "si el +número es divisible por 5". + +00:02:13.306 --> 00:02:17.366 align:middle +Vale, sólo una cosa más que +aprender sobre Twig, y es genial. + +00:02:18.216 --> 00:02:20.406 align:middle +Mira el código fuente HTML de la página. + +00:02:21.606 --> 00:02:27.676 align:middle +Fíjate en que no hay estructura HTML: +no hay etiquetas html, head o body. + +00:02:28.146 --> 00:02:33.376 align:middle +Literalmente el HTML que tenemos dentro +de nuestra plantilla, es lo que obtenemos. + +00:02:33.636 --> 00:02:34.726 align:middle +Nada más. + +00:02:34.966 --> 00:02:36.336 align:middle +Entonces, ¿hay... + +00:02:36.336 --> 00:02:41.996 align:middle +algún tipo de sistema de maquetación en Twig en el que +podamos añadir una maquetación base a nuestro alrededor? + +00:02:42.646 --> 00:02:43.636 align:middle +Por supuesto. + +00:02:43.926 --> 00:02:45.456 align:middle +Y es increíble. + +00:02:45.806 --> 00:02:47.986 align:middle +Se llama herencia de plantillas. + +00:02:48.016 --> 00:02:54.676 align:middle +Si tienes una plantilla y quieres que utilice algún +diseño base, en la parte superior del archivo, + +00:02:54.896 --> 00:02:58.396 align:middle +utiliza una etiqueta "hacer +algo" llamada extends. + +00:02:59.076 --> 00:03:03.696 align:middle +Pásale el nombre del archivo +de diseño: base.html.twig. + +00:03:04.566 --> 00:03:06.556 align:middle +Esto se refiere a esta plantilla de aquí. + +00:03:07.566 --> 00:03:12.166 align:middle +Antes de comprobarlo, si lo +intentamos ahora, ¡vaya! + +00:03:12.336 --> 00:03:18.046 align:middle +Gran error: Una plantilla que extiende otra no +puede incluir contenido fuera de los bloques Twig. + +00:03:18.046 --> 00:03:22.566 align:middle +Para saber qué significa +esto, abre base.html.twig. + +00:03:24.336 --> 00:03:26.476 align:middle +Este es tu archivo de diseño base... + +00:03:26.866 --> 00:03:30.756 align:middle +y eres totalmente libre de +personalizarlo como quieras. + +00:03:31.716 --> 00:03:32.476 align:middle +Ahora mismo... + +00:03:32.756 --> 00:03:35.686 align:middle +se trata principalmente de +aburridas etiquetas HTML... + +00:03:36.056 --> 00:03:39.806 align:middle +excepto por una serie de "bloques". + +00:03:39.806 --> 00:03:45.516 align:middle +Los bloques son básicamente "agujeros" en los +que una plantilla hija puede colocar contenido. + +00:03:46.226 --> 00:03:48.376 align:middle +Permíteme explicarlo de otra manera. + +00:03:48.926 --> 00:03:54.566 align:middle +Cuando decimos extends 'base.html.twig', +eso dice básicamente: ¡Twig! + +00:03:54.876 --> 00:04:00.836 align:middle +Cuando renderices esta plantilla, quiero +que realmente renderices base.html.twig... + +00:04:01.096 --> 00:04:04.596 align:middle +y pongas mi contenido dentro de ella. + +00:04:05.226 --> 00:04:08.816 align:middle +Twig entonces responde +amablemente: Vale, genial... + +00:04:08.816 --> 00:04:09.696 align:middle +Puedo hacerlo. + +00:04:10.146 --> 00:04:15.506 align:middle +Pero ¿en qué lugar de base.html.twig +quieres que ponga todo tu contenido? + +00:04:15.916 --> 00:04:18.476 align:middle +¿Quieres que lo ponga al final de la página? + +00:04:18.686 --> 00:04:19.356 align:middle +¿En la parte superior? + +00:04:19.676 --> 00:04:21.556 align:middle +¿En algún lugar al azar en el medio? + +00:04:22.416 --> 00:04:29.426 align:middle +La forma de decirle a Twig dónde poner nuestro +contenido dentro de base.html.twig es anulando un bloque. + +00:04:30.636 --> 00:04:35.136 align:middle +Observa que base.html.twig ya +tiene un bloque llamado body... + +00:04:35.536 --> 00:04:39.856 align:middle +y es justo donde queremos poner +el HTML de nuestra plantilla. + +00:04:41.106 --> 00:04:47.466 align:middle +Para ponerlo ahí, en nuestra plantilla, rodea +todo el contenido con {% block body %}... + +00:04:47.466 --> 00:04:50.206 align:middle +y luego {% endblock %}. + +00:04:50.206 --> 00:04:55.026 align:middle +[[ code('01d2fbf6f5') ]] A esto se le +llama herencia de la plantilla porque + +00:04:55.026 --> 00:04:57.966 align:middle +estamos sobrescribiendo ese bloque body con + +00:04:59.036 --> 00:05:02.966 align:middle +este nuevo contenido. Así +que ahora, cuando Twig + +00:05:03.456 --> 00:05:09.186 align:middle +renderice base.html.twig... y llegue a +esta parte block body, va a imprimir el + +00:05:09.256 --> 00:05:13.396 align:middle +HTMLblock body de nuestra plantilla + +00:05:13.786 --> 00:05:15.646 align:middle +Watch: refresh y... el + +00:05:16.606 --> 00:05:21.286 align:middle +error ha desaparecido. Y si ves el código +fuente de la página, ¡tenemos una + +00:05:22.246 --> 00:05:25.606 align:middle +página HTML completa! Ah, y +los nombres de estos bloques + +00:05:26.016 --> 00:05:31.116 align:middle +no son importantes. Si quieres cambiarles el nombre +por el de tu personaje favorito de una sitcom de los 90 + +00:05:31.116 --> 00:05:36.406 align:middle +, hazlo. Sólo recuerda +actualizar también su nombre en + +00:05:36.436 --> 00:05:38.526 align:middle +cualquier plantilla hija. También puedes + +00:05:38.886 --> 00:05:43.206 align:middle +añadir más bloques. Cada +bloque que añadas es otro + +00:05:44.266 --> 00:05:48.556 align:middle +punto de anulación potencial. Ah, y te +habrás dado cuenta de que los bloques pueden + +00:05:49.836 --> 00:05:53.056 align:middle +tener contenido por defecto. +Mira la página ahora mismo: el + +00:05:53.606 --> 00:05:56.996 align:middle +título dice "Bienvenido". +Eso es porque el bloque title + +00:05:57.396 --> 00:05:59.706 align:middle +tiene contenido por defecto... y + +00:06:00.986 --> 00:06:03.966 align:middle +no lo estamos anulando. Vamos a +cambiar el título por defecto + +00:06:04.486 --> 00:06:08.456 align:middle +a "Vinilo mixto". Así que ahora ese +será el título de todas las páginas + +00:06:08.976 --> 00:06:11.306 align:middle +de nuestro sitio... a menos que + +00:06:12.286 --> 00:06:17.746 align:middle +lo anulemos. En nuestra plantilla, ya sea encima del +cuerpo del bloque o debajo -el orden de los bloques no + +00:06:18.266 --> 00:06:25.266 align:middle +importa-, añade {% block title %}, +{% endblock %} y, en medio, "Crear + +00:06:26.506 --> 00:06:27.416 align:middle +un nuevo + +00:06:28.116 --> 00:06:31.046 align:middle +disco". Y ahora... ¡sí! Esta página tiene + +00:06:32.006 --> 00:06:37.356 align:middle +un título personalizado. Y puede que te +preguntes ¿Qué pasa si no quiero sustituir + +00:06:37.626 --> 00:06:40.636 align:middle +un bloque por completo.... +sino que quiero añadirlo + +00:06:41.166 --> 00:06:42.536 align:middle +a un bloque? + +00:06:43.196 --> 00:06:48.216 align:middle +Eso es totalmente posible. En base.html.twig, +el bloque title está configurado + +00:06:48.276 --> 00:06:53.386 align:middle +como "Vinilo mixto". Si quisiéramos +añadirle nuestro título personalizado + +00:06:53.996 --> 00:06:58.816 align:middle +, podríamos decir "Crear un +nuevo disco" y luego utilizar la + +00:06:59.076 --> 00:07:01.816 align:middle +etiqueta "decir algo" para imprimir una + +00:07:02.436 --> 00:07:08.486 align:middle +función llamada parent(). Eso hace exactamente lo que +esperarías: encuentra el contenido de la plantilla padre + +00:07:08.716 --> 00:07:09.966 align:middle +para este bloque + +00:07:11.046 --> 00:07:12.196 align:middle +y + +00:07:12.626 --> 00:07:14.446 align:middle +lo imprime. Actualiza y... + +00:07:15.446 --> 00:07:21.686 align:middle +qué bien. Si alguna vez estás confundido sobre cómo +funciona la herencia de plantillas, es útil, al menos para + +00:07:21.906 --> 00:07:26.546 align:middle +mí, pensar en ella exactamente + +00:07:27.226 --> 00:07:31.986 align:middle +como en la herencia orientada a objetos. Cada +plantilla es como una clase y cada bloque es + +00:07:32.066 --> 00:07:38.236 align:middle +como un método. Así, la "clase" +de la página de inicio extiende + +00:07:38.326 --> 00:07:40.876 align:middle +la "clase" de base.html.twig, pero anula dos + +00:07:41.666 --> 00:07:44.506 align:middle +de sus métodos. Si eso +sólo te ha confundido, no + +00:07:44.506 --> 00:07:47.366 align:middle +te preocupes. Así que ... eso es + +00:07:47.836 --> 00:07:52.976 align:middle +todo para Twig. Básicamente eres un experto en +Twig, lo que me han dicho que es un tema popular + +00:07:53.916 --> 00:07:58.336 align:middle +en las fiestas. A continuación: una de las +características más destacadas de Symfony son + +00:07:58.886 --> 00:08:01.136 align:middle +sus herramientas de depuración. +Vamos a instalarlas y a comprobarlas. diff --git a/sfcasts/ep1/es/twig-service.md b/sfcasts/ep1/es/twig-service.md new file mode 100644 index 0000000..d74d917 --- /dev/null +++ b/sfcasts/ep1/es/twig-service.md @@ -0,0 +1,51 @@ +# El servicio Twig y el perfilador de peticiones de la API + +Como esta página acaba de cargarse sin ningún error, pensamos que acabamos de registrar con éxito un mensaje a través del servicio de registro. Pero... ¿dónde van los mensajes de registro? ¿Cómo podemos comprobarlo? + +El servicio de registro lo proporciona una biblioteca que hemos instalado antes, llamada monolog, que forma parte del paquete de depuración. Y puedes controlar su configuración dentro del archivo`config/packages/monolog.yaml`, incluyendo dónde se registran los mensajes de registro, por ejemplo, en qué archivo. Nos centraremos más en la configuración en el siguiente tutorial. + +## El perfilador de peticiones de la API + +Pero una forma de ver siempre los mensajes de registro de una petición es a través del perfilador Esto es muy útil. Ve a la página de inicio, haz clic en cualquier enlace de la barra de herramientas de depuración web... y luego ve a la sección Registros. Ahora veremos todos los mensajes de registro que se hicieron sólo durante esa última petición a la página de inicio. + +¡Genial! Excepto que... nuestro mensaje de registro se hace en una ruta de la API... ¡y las rutas de la API no tienen una barra de herramientas de depuración web en la que podamos hacer clic! ¿Estamos atascados? No! Actualiza esta página una vez más... y luego ve manualmente a `/_profiler`. Esta es... una especie de puerta secreta al sistema de perfiles... y esta página muestra las últimas diez peticiones realizadas en nuestro sistema. La segunda en la parte superior es la petición de la API que acabamos de hacer. Haz clic en el pequeño enlace del token para ver... ¡sí! ¡Estamos viendo el perfil de esa petición de la API! En la sección de Registros... ¡ahí está! + +> Respuesta de la API para la canción 5 + +... e incluso puedes ver la información extra que hemos pasado. + +## Renderizar una plantilla Twig manualmente + +Vale, los servicios son tan importantes que... Quiero hacer un ejemplo rápido más. Vuelve a `VinylController`. El método `render()` es realmente un atajo para obtener el servicio "Twig", llamar a algún método de ese objeto para renderizar la plantilla... y luego poner la cadena HTML final en un objeto `Response`. Es un gran atajo y deberías utilizarlo. + +Pero! Como reto, ¿podríamos renderizar una plantilla sin usar ese método? ¡Por supuesto! Hagámoslo. + +Primer paso: encontrar el servicio que hace el trabajo que necesitas hacer. Así que tenemos que encontrar el servicio Twig. Volvamos a hacer nuestro truco: + +```terminal +php bin/console debug:autowiring twig +``` + +Y... ¡sí! Al parecer, el tipo de pista que tenemos que utilizar es `Twig\Environment`. + +¡De acuerdo! Vuelve a nuestro método, añade un argumento, escribe `Environment`, y pulsa el tabulador para autocompletarlo y que PhpStorm añada la sentencia `use`. Vamos a llamarlo `$twig`. + +A continuación, en lugar de usar `render`, digamos `$html =` y luego `$twig->`. Al igual que con el registrador, no necesitamos saber qué métodos tiene esta clase, porque, gracias a la sugerencia de tipo, PhpStorm puede decirnos todos los métodos. El método `render()` parece que es probablemente lo que queremos. El primer argumento es el nombre de la cadena de la plantilla a renderizar y el argumento `$context` contiene las variables. Así que... tiene los mismos argumentos que ya estábamos pasando. + +Para ver si funciona, `dd($html)`. + +[[[ code('9dd37843fc') ]]] + +¡Hora de probar! Dirígete a la página de inicio... ¡y sí! ¡Acabamos de renderizar una plantilla manualmente! ¡Increíble! Y podemos terminar esta página envolviendo eso en una respuesta:`return new Response($html)`. + +[[[ code('a862fa6787') ]]] + +Y ahora... ¡la página funciona! Y entendemos que la verdadera forma de renderizar una plantilla es a través del servicio Twig. Algún día te encontrarás en una situación en la que necesites renderizar una plantilla pero no estés en un controlador... y por tanto no tengas el método abreviado `$this->render()`. Saber que hay un servicio Twig que puedes recuperar será la clave para resolver ese problema. Más sobre esto en el próximo tutorial. + +Pero en una aplicación real, en un controlador, no hay razón para hacer todo este trabajo extra. Así que voy a revertir esto... y volver a usar `render()`. Y... entonces ya no necesitamos autocablear ese argumento... e incluso podemos limpiar la declaración`use`. + +Aquí están los tres grandes, gigantescos e importantes puntos de partida. En primer lugar, Symfony está repleto de objetos que hacen su trabajo... a los que llamamos servicios. Los servicios son herramientas. Segundo, todo el trabajo en Symfony lo hace un servicio... incluso cosas como el enrutamiento. Y en tercer lugar, podemos utilizar los servicios para ayudarnos a realizar nuestro trabajo mediante la autoconexión de los mismos. + +En el próximo tutorial de esta serie, profundizaremos en este concepto tan importante. + +Pero antes de que terminemos este tutorial, quiero hablar de otra cosa increíble y asombrosa: Webpack Encore, la clave para escribir CSS y JavaScript de forma profesional. A lo largo de estos últimos capítulos, vamos a dar vida a nuestro sitio e incluso a hacerlo tan responsivo como una aplicación de una sola página. diff --git a/sfcasts/ep1/es/twig-service.vtt b/sfcasts/ep1/es/twig-service.vtt new file mode 100644 index 0000000..ae3c8b9 --- /dev/null +++ b/sfcasts/ep1/es/twig-service.vtt @@ -0,0 +1,309 @@ +WEBVTT + +00:00:00.036 --> 00:00:03.976 align:middle +Como esta página acaba de +cargarse sin ningún error, + +00:00:04.396 --> 00:00:09.416 align:middle +pensamos que acabamos de registrar con éxito +un mensaje a través del servicio de registro. + +00:00:09.606 --> 00:00:12.366 align:middle +Pero... ¿dónde van los mensajes de registro? + +00:00:12.796 --> 00:00:13.676 align:middle +¿Cómo podemos comprobarlo? + +00:00:14.486 --> 00:00:20.016 align:middle +El servicio de registro lo proporciona una biblioteca +que hemos instalado antes, llamada monolog. + +00:00:20.586 --> 00:00:22.366 align:middle +Formaba parte del paquete de depuración. + +00:00:22.896 --> 00:00:29.176 align:middle +Y puedes controlar su configuración dentro +del archivo config/packages/monolog.yaml, + +00:00:29.516 --> 00:00:33.666 align:middle +incluyendo dónde se registran los mensajes +de registro, como por ejemplo en qué archivo. + +00:00:34.076 --> 00:00:36.646 align:middle +Nos centraremos más en la +configuración en el próximo tutorial. + +00:00:37.186 --> 00:00:43.396 align:middle +Pero una forma de ver siempre los mensajes de +registro de una petición es a través del perfilador + +00:00:44.106 --> 00:00:45.606 align:middle +Esto es muy útil. + +00:00:46.856 --> 00:00:52.646 align:middle +Ve a la página de inicio, haz clic en cualquier enlace +de la barra de herramientas de depuración web... + +00:00:52.646 --> 00:00:54.896 align:middle +y luego ve a la sección Registros. + +00:00:55.886 --> 00:01:00.266 align:middle +Ahora veremos todos los mensajes de +registro que se hicieron sólo durante + +00:01:00.266 --> 00:01:02.756 align:middle +esa última petición a la página de inicio. + +00:01:03.186 --> 00:01:05.226 align:middle +¡Genial! Excepto que... + +00:01:05.306 --> 00:01:08.646 align:middle +nuestro mensaje de registro se +realiza en una ruta de la API... + +00:01:09.186 --> 00:01:14.516 align:middle +¡y los puntos finales de la API no tienen una barra de +herramientas de depuración web en la que podamos hacer clic! + +00:01:15.216 --> 00:01:16.486 align:middle +¿Estamos atascados? + +00:01:17.096 --> 00:01:20.306 align:middle +¡No! Actualiza esta página una vez más... + +00:01:20.746 --> 00:01:24.466 align:middle +y luego ve manualmente a /_profiler. + +00:01:27.246 --> 00:01:28.026 align:middle +Esto es... + +00:01:28.166 --> 00:01:31.386 align:middle +una especie de puerta secreta +al sistema de perfiles... + +00:01:31.816 --> 00:01:36.126 align:middle +y esta página muestra las diez últimas +peticiones realizadas a nuestro sistema. + +00:01:36.966 --> 00:01:41.146 align:middle +La segunda en la parte superior es la +petición de la API que acabamos de hacer. + +00:01:41.146 --> 00:01:44.736 align:middle +Haz clic en el pequeño +enlace del token para ver... + +00:01:45.286 --> 00:01:49.226 align:middle +¡sí! ¡Estamos viendo el perfil +de esa petición de la API! + +00:01:50.056 --> 00:01:51.466 align:middle +En la sección de Registros... + +00:01:51.766 --> 00:01:52.996 align:middle +¡ahí está! + +00:01:53.236 --> 00:01:55.976 align:middle +Devuelve la respuesta de la +API para la canción 5 ... + +00:01:56.606 --> 00:01:59.516 align:middle +e incluso puedes ver la +información extra que hemos pasado. + +00:02:00.636 --> 00:02:03.976 align:middle +Vale, los servicios son tan importantes que... + +00:02:03.976 --> 00:02:05.836 align:middle +Quiero hacer un ejemplo rápido más. + +00:02:06.256 --> 00:02:07.806 align:middle +Vuelve a VinylController. + +00:02:08.856 --> 00:02:14.026 align:middle +El método render() es en realidad un +atajo para obtener el servicio " Twig", + +00:02:14.296 --> 00:02:17.616 align:middle +llamar a algún método de ese objeto +para representar la plantilla... + +00:02:17.786 --> 00:02:22.896 align:middle +y luego poner la cadena HTML +final en un objeto Response. + +00:02:23.736 --> 00:02:26.936 align:middle +Es un gran atajo y deberías usarlo. + +00:02:27.396 --> 00:02:33.636 align:middle +Pero! Como reto, ¿podríamos renderizar +una plantilla sin utilizar ese método? + +00:02:34.236 --> 00:02:34.916 align:middle +¡Por supuesto! + +00:02:35.256 --> 00:02:36.306 align:middle +Hagámoslo. + +00:02:37.056 --> 00:02:41.316 align:middle +Primer paso: encontrar el servicio que +hace el trabajo que necesitas hacer. + +00:02:41.856 --> 00:02:44.816 align:middle +Así que tenemos que +encontrar el servicio Twig. + +00:02:44.816 --> 00:02:51.896 align:middle +Volvamos a hacer nuestro truco: php +bin/console debug:autowiring twig Y... + +00:02:52.146 --> 00:02:58.186 align:middle +¡sí! Al parecer, el tipo de pista que +tenemos que utilizar es Twig\Environment. + +00:02:59.206 --> 00:03:06.346 align:middle +¡De acuerdo! Vuelve a nuestro método, añade un argumento, +escribe Environment, y pulsa el tabulador para autocompletarlo + +00:03:06.346 --> 00:03:09.436 align:middle +y que PhpStorm añada la sentencia use. + +00:03:10.206 --> 00:03:11.376 align:middle +Llamémoslo $twig. + +00:03:12.736 --> 00:03:20.256 align:middle +A continuación, en lugar de usar render, +digamos $html = y luego $twig->. + +00:03:21.266 --> 00:03:27.146 align:middle +Al igual que con el registrador, no necesitamos +saber qué métodos tiene esta clase, porque, + +00:03:27.276 --> 00:03:31.906 align:middle +gracias a la sugerencia de tipo, PhpStorm +puede decirnos todos los métodos. + +00:03:32.576 --> 00:03:36.076 align:middle +El método render() parece que +es probablemente lo que queremos. + +00:03:36.816 --> 00:03:40.176 align:middle +El primer argumento es el nombre de +la cadena de la plantilla a renderizar + +00:03:40.326 --> 00:03:43.486 align:middle +y el argumento $context contiene las variables. + +00:03:43.706 --> 00:03:48.296 align:middle +Así que... tiene los mismos +argumentos que ya estábamos pasando. + +00:03:48.296 --> 00:03:52.366 align:middle +Para ver si funciona, dd($html). + +00:03:53.736 --> 00:03:54.726 align:middle +¡Hora de probar! + +00:03:55.516 --> 00:03:56.586 align:middle +Dirígete a la página de inicio... + +00:03:57.536 --> 00:04:02.046 align:middle +y ¡sí! ¡Acabamos de renderizar +una plantilla manualmente! + +00:04:02.256 --> 00:04:03.896 align:middle +¡Realmente increíble! + +00:04:04.576 --> 00:04:13.926 align:middle +Y podemos terminar esta página envolviendo eso +en una respuesta: return new Response($html). + +00:04:13.926 --> 00:04:14.606 align:middle +Y ahora... + +00:04:14.966 --> 00:04:16.916 align:middle +¡la página funciona! + +00:04:17.306 --> 00:04:22.766 align:middle +Y entendemos que la verdadera forma de renderizar +una plantilla es a través del servicio Twig. + +00:04:22.766 --> 00:04:28.786 align:middle +Algún día te encontrarás en una situación +en la que necesites renderizar una plantilla + +00:04:28.976 --> 00:04:31.446 align:middle +pero no estés en un controlador... + +00:04:31.806 --> 00:04:36.486 align:middle +y por tanto no tienes el método de +acceso directo $this->render(). + +00:04:37.486 --> 00:04:42.856 align:middle +Saber que hay un servicio Twig que puedes recuperar +será la clave para resolver ese problema. + +00:04:43.316 --> 00:04:45.506 align:middle +Más sobre esto en el próximo tutorial. + +00:04:45.986 --> 00:04:51.506 align:middle +Pero en una aplicación real, en un controlador, +no hay razón para hacer todo este trabajo extra. + +00:04:51.686 --> 00:04:53.846 align:middle +Así que voy a revertir esto... + +00:04:54.206 --> 00:04:56.086 align:middle +y volver a utilizar render(). + +00:04:56.516 --> 00:05:00.106 align:middle +Y... entonces ya no necesitamos +autocablear ese argumento... + +00:05:00.486 --> 00:05:03.006 align:middle +e incluso podemos limpiar la declaración use. + +00:05:04.326 --> 00:05:08.216 align:middle +Aquí están los tres grandes, gigantescos +e importantes puntos de partida. + +00:05:08.296 --> 00:05:13.126 align:middle +En primer lugar, Symfony está repleto +de objetos que hacen su trabajo... + +00:05:13.506 --> 00:05:15.536 align:middle +que llamamos servicios. + +00:05:15.536 --> 00:05:17.716 align:middle +Los servicios son herramientas. + +00:05:17.776 --> 00:05:22.166 align:middle +Segundo, todo el trabajo en +Symfony lo hace un servicio... + +00:05:22.516 --> 00:05:24.106 align:middle +incluso cosas como el enrutamiento. + +00:05:25.026 --> 00:05:32.056 align:middle +Y en tercer lugar, podemos utilizar los servicios para ayudarnos a +realizar nuestro trabajo mediante la autoconexión de los mismos. + +00:05:32.746 --> 00:05:38.066 align:middle +En el próximo tutorial de esta serie, +profundizaremos en este concepto tan importante. + +00:05:38.946 --> 00:05:45.756 align:middle +Pero antes de que terminemos este tutorial, +quiero hablar de otra cosa increíble y + +00:05:45.756 --> 00:05:52.876 align:middle +asombrosa: Webpack Encore, la clave para +escribir CSS y JavaScript de forma profesional. A + +00:05:53.576 --> 00:05:59.286 align:middle +lo largo de estos últimos capítulos, vamos +a dar vida a nuestro sitio e incluso a hacerlo + +00:05:59.286 --> 00:06:02.456 align:middle +tan responsivo como una +aplicación de una sola página diff --git a/sfcasts/ep1/es/twig.md b/sfcasts/ep1/es/twig.md new file mode 100644 index 0000000..fbb6296 --- /dev/null +++ b/sfcasts/ep1/es/twig.md @@ -0,0 +1,67 @@ +# Twig ❤️ + +Las clases de controlador de Symfony no necesitan extender una clase base. Mientras tu función de controlador devuelva un objeto `Response`, a Symfony no le importa el aspecto de tu controlador. Pero normalmente, extenderás una clase llamada`AbstractController`. + +¿Por qué? Porque nos da métodos de acceso directo. + +## Renderización de una plantilla + +Y el primer atajo es `render()`: el método para renderizar una plantilla. Así que devuelve `$this->render()` y le pasa dos cosas. La primera es el nombre de la plantilla. ¿Qué tal `vinyl/homepage.html.twig`. + +No es necesario, pero es habitual tener un directorio con el mismo nombre que la clase de tu controlador y un nombre de archivo que sea el mismo que el de tu método, pero puedes hacer lo que quieras. El segundo argumento es un array con las variables que quieras pasar a la plantilla. Vamos a pasar una variable llamada`title` y a ponerle el título de nuestra cinta de mezclas: "PB and Jams". + +[[[ code('28e791440a') ]]] + +Hecho aquí. Ah, pero, ¡examen sorpresa! ¿Qué crees que devuelve el método `render()`? Sí, es lo que siempre repito: un controlador siempre debe devolver un objeto `Response`. `render()` es sólo un atajo para renderizar una plantilla, obtener esa cadena y ponerla en un objeto `Response`. `render()` devuelve un objeto `Response`. + +## Crear la plantilla + +Sabemos por lo que hemos dicho antes, que cuando renderizas una plantilla, Twig busca en el directorio `templates/`. Así que crea un nuevo subdirectorio `vinyl/`... y dentro de él, un archivo llamado `homepage.html.twig`. Para empezar, añade un `h1` y luego imprime la variable `title` con una sintaxis especial de Twig: `{{ title }}`. Y... Añadiré un texto TODO codificado. + +[[[ code('285607e2a3') ]]] + +¡Vamos a ver si esto funciona! Estábamos trabajando en nuestra página web, así que ve allí y... ¡hola Twig! + +## Sintaxis de Twigs 3 + +Twig es una de las partes más bonitas de Symfony, y también una de las más fáciles. Vamos a repasar todo lo que necesitas saber... básicamente en los próximos diez minutos. + +Twig tiene exactamente tres sintaxis diferentes. Si necesitas imprimir algo, utiliza `{{`. A esto lo llamo la sintaxis "decir algo". Si digo `{{ saySomething }}`se imprimiría una variable llamada `saySomething`. Una vez que estás dentro de Twig, se parece mucho a JavaScript. Por ejemplo, si lo encierro entre comillas, ahora estoy imprimiendo la cadena `saySomething`. Twig tiene funciones... por lo que llamaría a la función e imprimiría el resultado. + +Así que la sintaxis nº 1 -la de "decir algo"- es `{{` + +La segunda sintaxis... no cuenta realmente. Es `{#` para crear un comentario... y ya está. + +[[[ code('285607e2a3') ]]] + +La tercera y última sintaxis la llamo "hacer algo". Esto es cuando no estás imprimiendo, estás haciendo algo en el lenguaje. Ejemplos de "hacer algo" serían las sentencias if, los bucles for o la configuración de variables. + +## El bucle for + +Vamos a probar un bucle `for`. Vuelve al controlador. Voy a pegar una lista de pistas... y luego pasaré una variable `tracks` a la plantilla ajustada a esa matriz. + +[[[ code('8123e02600') ]]] + +Ahora, a diferencia de `title`, tracks es una matriz... así que no podemos imprimirla. Pero, ¡podemos intentarlo! ¡Ja! Eso nos da una conversión de matriz a cadena. No, tenemos que hacer un bucle sobre las pistas. + +Añade una cabecera y un `ul`. Para hacer el bucle, usaremos la sintaxis "hacer algo", que es`{%` y luego la cosa que quieras hacer, como `for`, `if` o `set`. Te mostraré la lista completa de etiquetas de hacer algo en un minuto. Un bucle for tiene este aspecto:`for track in tracks`, donde pistas es la variable sobre la que hacemos el bucle y `track`será la variable dentro del bucle. + +Después de esto, añade `{% endfor %}`: la mayoría de las etiquetas "hacer algo" tienen una etiqueta de fin. Dentro del bucle, añade un `li` y luego utiliza la sintaxis de decir algo para imprimir `track`. + +[[[ code('dc265ad487') ]]] + +## Uso de Sub.keys + +Cuando lo probemos... ¡qué bien! Pero vamos a ponernos más complicados. De vuelta en el controlador, en lugar de utilizar un simple array, lo reestructuraré para que cada pista sea un array asociativo con las claves `song` y `artist`. Pondré ese mismo cambio para el resto. + +[[[ code('647cdcfd7e') ]]] + +¿Qué ocurre si lo probamos? Ah, volvemos a la conversión de "matriz a cadena". Cuando hacemos el bucle, cada pista es ahora una matriz. ¿Cómo podemos leer las claves `song`y `artist`? + +¿Recuerdas cuando dije que Twig se parece mucho a JavaScript? Pues bien, no debería sorprender que la respuesta sea `track.song` y `track.artist`. + +[[[ code('fc54d0388b') ]]] + +Y... eso hace que nuestra lista funcione. + +Ahora que ya tenemos lo básico de Twig, vamos a ver la lista completa de etiquetas "hacer algo", a conocer los "filtros" de Twig y a abordar el importantísimo sistema de herencia de plantillas. diff --git a/sfcasts/ep1/es/twig.vtt b/sfcasts/ep1/es/twig.vtt new file mode 100644 index 0000000..b802b51 --- /dev/null +++ b/sfcasts/ep1/es/twig.vtt @@ -0,0 +1,287 @@ +WEBVTT + +00:00:00.036 --> 00:00:05.516 align:middle +Las clases de controlador de Symfony +no necesitan extender una clase base. + +00:00:05.936 --> 00:00:10.076 align:middle +Mientras tu función de controlador +devuelva un objeto Response, a + +00:00:10.076 --> 00:00:13.426 align:middle +Symfony no le importa el +aspecto de tu controlador. + +00:00:13.926 --> 00:00:18.906 align:middle +Pero normalmente, extenderás una +clase llamada AbstractController. + +00:00:19.526 --> 00:00:23.276 align:middle +¿Por qué? Porque nos da +métodos de acceso directo. + +00:00:23.566 --> 00:00:28.516 align:middle +Y el primer atajo es render(): el +método para renderizar una plantilla. + +00:00:29.166 --> 00:00:32.926 align:middle +Así que devuelve $this->render() +y le pasa dos cosas. + +00:00:33.736 --> 00:00:35.896 align:middle +La primera es el nombre de la plantilla. + +00:00:36.346 --> 00:00:40.296 align:middle +¿Qué tal vinyl/homepage.html.twig. + +00:00:40.916 --> 00:00:45.606 align:middle +No es necesario, pero es habitual tener +un directorio con el mismo nombre que la + +00:00:45.606 --> 00:00:52.526 align:middle +clase de tu controlador y un nombre de archivo igual +al de tu método, pero puedes hacer lo que quieras. + +00:00:53.456 --> 00:00:59.176 align:middle +El segundo argumento es una matriz con las +variables que quieras pasar a la plantilla. + +00:01:00.006 --> 00:01:06.896 align:middle +Vamos a pasar una variable llamada title y a ponerle +el título de nuestra cinta de mezclas: "PB and Jams". + +00:01:08.576 --> 00:01:09.906 align:middle +Hecho aquí. + +00:01:10.176 --> 00:01:11.876 align:middle +Ah, pero, ¡dato sorpresa! + +00:01:12.206 --> 00:01:15.446 align:middle +¿Qué crees que devuelve el método render()? + +00:01:16.286 --> 00:01:23.506 align:middle +Sí, es lo que siempre repito: un controlador +debe devolver siempre un objeto Response. + +00:01:23.926 --> 00:01:28.446 align:middle +render() es sólo un atajo para renderizar +una plantilla, obtener esa cadena + +00:01:28.576 --> 00:01:30.786 align:middle +y ponerla en un objeto Response. + +00:01:31.186 --> 00:01:33.926 align:middle +render() devuelve un Response. + +00:01:34.026 --> 00:01:37.136 align:middle +Ya sabemos que, cuando +renderizas una plantilla, + +00:01:37.356 --> 00:01:39.466 align:middle +Twig busca en el directorio templates/. + +00:01:39.636 --> 00:01:42.586 align:middle +Así que crea un nuevo subdirectorio vinyl/... + +00:01:43.056 --> 00:01:47.606 align:middle +y dentro de éste, un archivo +llamado homepage.html.twig. + +00:01:48.876 --> 00:01:53.956 align:middle +Para empezar, añade un h1 y +luego imprime la variable title + +00:01:54.006 --> 00:01:59.396 align:middle +con una sintaxis especial de Twig: {{ title }}. + +00:01:59.476 --> 00:02:03.776 align:middle +Y... Añadiré un texto TODO codificado. + +00:02:05.776 --> 00:02:07.816 align:middle +¡Vamos a ver si esto funciona! + +00:02:08.486 --> 00:02:12.226 align:middle +Estábamos trabajando en nuestra +página web, así que ve allí y... + +00:02:12.546 --> 00:02:14.296 align:middle +¡hola Twig! + +00:02:14.816 --> 00:02:19.556 align:middle +Twig es una de las partes más bonitas de +Symfony, y también una de las más fáciles. + +00:02:19.986 --> 00:02:22.356 align:middle +Vamos a repasar todo lo que necesitas saber... + +00:02:22.536 --> 00:02:24.776 align:middle +básicamente en los próximos diez minutos. + +00:02:25.556 --> 00:02:28.496 align:middle +Twig tiene exactamente +tres sintaxis diferentes. + +00:02:28.576 --> 00:02:31.336 align:middle +Si necesitas imprimir algo, utiliza {{. + +00:02:31.336 --> 00:02:36.376 align:middle +A esto lo llamo la sintaxis "decir algo". + +00:02:37.056 --> 00:02:42.226 align:middle +Si digo {{ saySomething }} se imprimiría +una variable llamada saySomething. + +00:02:42.776 --> 00:02:47.286 align:middle +Una vez que estás dentro de Twig, +se parece mucho a JavaScript. + +00:02:47.366 --> 00:02:53.796 align:middle +Por ejemplo, si lo encierro entre comillas, +ahora estoy imprimiendo la cadena saySomething. + +00:02:54.816 --> 00:02:56.166 align:middle +Twig tiene funciones... + +00:02:56.946 --> 00:03:00.036 align:middle +por lo que llamaría a la función +e imprimiría el resultado. + +00:03:00.036 --> 00:03:08.666 align:middle +Así que la sintaxis nº 1 -la de "decir +algo"- es {{. La segunda sintaxis... + +00:03:08.696 --> 00:03:10.336 align:middle +no cuenta realmente. + +00:03:10.526 --> 00:03:14.136 align:middle +Es {# para crear un comentario... + +00:03:14.466 --> 00:03:17.236 align:middle +y eso es todo. + +00:03:17.286 --> 00:03:21.966 align:middle +La tercera y última sintaxis +la llamo "hacer algo". + +00:03:22.186 --> 00:03:26.886 align:middle +Esto es cuando no estás imprimiendo, +estás haciendo algo en el lenguaje. + +00:03:27.446 --> 00:03:33.416 align:middle +Ejemplos de "hacer algo" serían las sentencias if, +los bucles for o la configuración de variables. + +00:03:34.146 --> 00:03:35.566 align:middle +Probemos con un bucle for. + +00:03:36.256 --> 00:03:37.306 align:middle +Vuelve al controlador. + +00:03:37.866 --> 00:03:40.486 align:middle +Voy a pegar una lista de pistas... + +00:03:41.086 --> 00:03:46.346 align:middle +y luego pasaré una variable tracks +a la plantilla ajustada a esa lista. + +00:03:47.936 --> 00:03:51.106 align:middle +Ahora, a diferencia de title, +tracks es una matriz... + +00:03:51.426 --> 00:03:53.666 align:middle +por lo que no podemos imprimirlo sin más. + +00:03:53.966 --> 00:03:55.566 align:middle +Pero, ¡podemos intentarlo! + +00:03:57.076 --> 00:04:00.786 align:middle +¡Ja! Eso nos da una +conversión de matriz a cadena. + +00:04:01.406 --> 00:04:04.016 align:middle +No, tenemos que hacer un +bucle sobre las pistas. + +00:04:04.726 --> 00:04:06.726 align:middle +Añade una cabecera y un ul. + +00:04:08.176 --> 00:04:16.206 align:middle +Para hacer un bucle, utilizaremos la sintaxis +"hacer algo", que es {% y luego la cosa que + +00:04:16.206 --> 00:04:19.876 align:middle +quieras hacer, como for, if o set. + +00:04:22.436 --> 00:04:27.166 align:middle +Te mostraré la lista completa de +etiquetas "hacer algo" en un minuto. + +00:04:27.166 --> 00:04:33.986 align:middle +Un bucle for tiene este aspecto: for track in tracks, +donde pistas es la variable sobre la que hacemos el b ucle + +00:04:34.406 --> 00:04:37.556 align:middle +y track será la variable dentro del bucle. + +00:04:37.556 --> 00:04:45.296 align:middle +Después de esto, añade {% endfor %}: la mayoría de +las etiquetas "hacer algo" tienen una etiqueta de fin. + +00:04:46.106 --> 00:04:51.836 align:middle +Dentro del bucle, añade un li y luego utiliza +la sintaxis de decir algo para imprimir track. + +00:04:53.036 --> 00:04:54.236 align:middle +Cuando lo probemos... + +00:04:54.966 --> 00:04:58.056 align:middle +¡qué bien! Pero vamos a +ponernos más complicados. De + +00:04:58.286 --> 00:05:03.936 align:middle +vuelta al controlador, en lugar de utilizar +un simple array, lo reestructuraré + +00:05:03.936 --> 00:05:09.406 align:middle +para que cada pista sea un array +asociativo con las claves song y artist. + +00:05:11.536 --> 00:05:14.066 align:middle +Pondré ese mismo cambio para el resto. + +00:05:15.216 --> 00:05:17.306 align:middle +¿Qué ocurre si lo probamos? + +00:05:18.256 --> 00:05:21.536 align:middle +Ah, volvemos a la conversión +de "matriz a cadena". + +00:05:22.346 --> 00:05:26.106 align:middle +Cuando hacemos un bucle, cada +pista en sí es ahora un array. + +00:05:26.656 --> 00:05:30.076 align:middle +¿Cómo podemos leer las claves song y artist? + +00:05:30.886 --> 00:05:34.656 align:middle +¿Recuerdas cuando dije que Twig +se parece mucho a JavaScript? + +00:05:35.316 --> 00:05:43.306 align:middle +Pues bien, no debería sorprender que la +respuesta sea track.song y track.artist. + +00:05:43.416 --> 00:05:48.956 align:middle +Y... eso hace que nuestra lista funcione. + +00:05:49.946 --> 00:05:55.206 align:middle +Ahora que ya tenemos los fundamentos +de Tw ig, vamos a ver la lista completa + +00:05:55.206 --> 00:05:58.776 align:middle +de etiquetas "hacer algo", a +conocer los "filtros" de Twig + +00:05:59.066 --> 00:06:03.446 align:middle +y abordar el importantísimo +sistema de herencia de plantillas diff --git a/sfcasts/ep1/es/webpack-encore-usage.md b/sfcasts/ep1/es/webpack-encore-usage.md new file mode 100644 index 0000000..c153cfc --- /dev/null +++ b/sfcasts/ep1/es/webpack-encore-usage.md @@ -0,0 +1,64 @@ +# Empaquetar JS y CSS con Encore + +Cuando instalamos Webpack Encore, su receta nos dio este nuevo directorio `assets/`. Mira el archivo `app.js`. Es interesante. Observa cómo importa este archivo `bootstrap`. En realidad es `bootstrap.js`: este archivo de aquí. La extensión `.js` es opcional. + +## Importaciones de JavaScript + +Esta es una de las cosas más importantes que nos da Webpack: la capacidad de importar un archivo JavaScript de otro. Podemos importar funciones, objetos... realmente cualquier cosa desde otro archivo. Vamos a hablar más sobre este archivo`bootstrap.js` dentro de un rato. + +Esto también importa un archivo CSS? Si no has visto esto antes, puede parecer... raro: ¿JavaScript importando CSS? + +Para ver cómo funciona todo esto, en `app.js`, añade un `console.log()`. + +[[[ code('37b4bd7997') ]]] + +Y `app.css` ya tiene un fondo de cuerpo... pero añade un `!important` para que podamos ver definitivamente si se está cargando. + +[[[ code('28ba4bdd4d') ]]] + +Vale... ¿entonces quién lee estos archivos? Porque... no viven en el directorio `public/`... así que no podemos crear etiquetas `script` o `link` que apunten directamente a ellos. + +## webpack.config.js + +Para responder a esto, abre `webpack.config.js`. Webpack Encore es un binario ejecutable: vamos a ejecutarlo en un minuto. Cuando lo hagamos, cargará este archivo para obtener su configuración. + +Y aunque hay un montón de funciones dentro de Webpack, lo único en lo que tenemos que centrarnos ahora es en esta: `addEntry()`. Este `app` puede ser cualquier cosa... como `dinosaur`, no importa. Te mostraré cómo se utiliza en un minuto. Lo importante es que apunta al archivo `assets/app.js`. Por ello,`app.js` será el primer y único archivo que Webpack analizará. + +Esto es bastante bueno: Webpack leerá el archivo `app.js` y luego seguirá todas las declaraciones de`import` recursivamente hasta que finalmente tenga una colección gigante de todo el JavaScript y el CSS que nuestra aplicación necesita. Entonces, lo escribirá en el directorio `public/`. + +## Ejecutando Webpack Encore + +Vamos a verlo en acción. Busca tu terminal y ejecuta: + +```terminal +yarn watch +``` + +Esto es, como dice, un atajo para ejecutar `encore dev --watch`. Si miras tu archivo `package.json`, viene con una sección `script` con algunos atajos. + +En cualquier caso, `yarn watch` hace dos cosas. En primer lugar, crea un nuevo directorio `public/build/`y, dentro, los archivos `app.css` y `app.js` Pero no dejes que los nombres te engañen: `app.js` contiene mucho más que lo que hay dentro de `assets/app.js`: contiene todo el JavaScript de todas las importaciones que encuentra. `app.css` +contiene todo el CSS de todas las importaciones. + +La razón por la que estos archivos se llaman `app.css` y `app.js` es por el nombre de la entrada. + +Así que la conclusión es que, gracias a Encore, de repente tenemos nuevos archivos en el directorio`public/build/` que contienen todo el JavaScript y el CSS que necesita nuestra aplicación + +## Las funciones Twig de Encore + +Y si te diriges a tu página de inicio y la actualizas... ¡woh! Ha funcionado al instante!? El fondo ha cambiado... y en mi inspector... ¡está el registro de la consola! ¿Cómo diablos ha ocurrido eso? + +Abre tu diseño base: `templates/base.html.twig`. El secreto está en las funciones`encore_entry_link_tags()` y `encore_entry_script_tags()`. Apuesto a que puedes adivinar lo que hacen: añadir la etiqueta `link` a `build/app.css` y la etiqueta `script`a `build/app.js`. + +Puedes ver esto en tu navegador. Mira la fuente de la página y... ¡sí! La etiqueta link para `/build/app.css`... y la etiqueta `script` para `/build/app.js`. Ah, pero también ha renderizado otras dos etiquetas `script`. Eso es porque Webpack es muy inteligente. Por motivos de rendimiento, en lugar de volcar un gigantesco archivo `app.js`, a veces Webpack lo divide en varios archivos más pequeños. Afortunadamente, estas funciones Twig de Encore son lo suficientemente inteligentes como para manejar eso: incluirá todas las etiquetas de enlace o de script necesarias. + +Lo más importante es que el código que tenemos en nuestro archivo `assets/app.js`-incluyendo todo lo que importa- ¡ahora funciona y aparece en nuestra página! + +## Vigilancia de los cambios + +Ah, y como hemos ejecutado `yarn watch`, Encore sigue funcionando en segundo plano en busca de cambios. Compruébalo: entra en `app.css`... y cambia el color de fondo. Guarda, pasa y actualiza + +[[[ code('9947a18198') ]]] + +¡Se actualiza instantáneamente! Eso es porque Encore se ha dado cuenta del cambio y ha recompilado el archivo construido muy rápidamente. + +A continuación: vamos a trasladar nuestro CSS existente al nuevo sistema y a aprender cómo podemos instalar e importar bibliotecas de terceros -mira Bootstrap o FontAwesome- directamente en nuestra configuración de Encore. diff --git a/sfcasts/ep1/es/webpack-encore-usage.vtt b/sfcasts/ep1/es/webpack-encore-usage.vtt new file mode 100644 index 0000000..040aa03 --- /dev/null +++ b/sfcasts/ep1/es/webpack-encore-usage.vtt @@ -0,0 +1,282 @@ +WEBVTT + +00:00:01.216 --> 00:00:06.086 align:middle +Cuando instalamos Webpack Encore, su receta +nos dio este nuevo directorio assets/. + +00:00:06.746 --> 00:00:08.836 align:middle +Mira el archivo app.js. + +00:00:09.636 --> 00:00:10.376 align:middle +Es interesante. + +00:00:10.746 --> 00:00:13.786 align:middle +Observa cómo importa este archivo bootstrap. + +00:00:14.286 --> 00:00:18.946 align:middle +En realidad es bootstrap.js: +este archivo de aquí. + +00:00:18.946 --> 00:00:20.906 align:middle +La extensión .js es opcional. + +00:00:21.646 --> 00:00:25.876 align:middle +Esta es una de las cosas más +importantes que nos da Webpack + +00:00:26.156 --> 00:00:30.846 align:middle +la capacidad de importar un +archivo JavaScript de otro. + +00:00:31.476 --> 00:00:34.066 align:middle +Podemos importar funciones, objetos... + +00:00:34.326 --> 00:00:36.376 align:middle +realmente cualquier cosa desde otro archivo. + +00:00:36.946 --> 00:00:41.086 align:middle +Vamos a hablar más sobre este archivo +bootstrap.js dentro de un rato. + +00:00:42.206 --> 00:00:45.156 align:middle +Esto también importa un archivo CSS? + +00:00:45.686 --> 00:00:47.966 align:middle +Si no has visto esto antes, puede parecer... + +00:00:47.966 --> 00:00:50.786 align:middle +extraño: ¿JavaScript importando CSS? + +00:00:51.766 --> 00:00:56.536 align:middle +Para ver cómo funciona todo esto, +en app.js, añade un console.log(). + +00:00:58.106 --> 00:01:01.116 align:middle +Y app.css ya tiene un fondo de cuerpo... + +00:01:01.736 --> 00:01:06.966 align:middle +pero añade un !important para que podamos +ver definitivamente si se está cargando. + +00:01:07.976 --> 00:01:11.066 align:middle +Vale... ¿entonces quién lee estos archivos? + +00:01:11.586 --> 00:01:12.026 align:middle +Porque... + +00:01:12.136 --> 00:01:15.056 align:middle +no viven en el directorio public/... + +00:01:15.336 --> 00:01:20.586 align:middle +por lo que no podemos crear etiquetas script +o link que apunten directamente a ellos. + +00:01:21.616 --> 00:01:25.376 align:middle +Para responder a esto, abre webpack.config.js. + +00:01:26.176 --> 00:01:31.986 align:middle +Webpack Encore es un binario ejecutable: +vamos a ejecutarlo en un minuto. + +00:01:32.636 --> 00:01:36.796 align:middle +Cuando lo hagamos, cargará este +archivo para obtener su configuración. + +00:01:37.836 --> 00:01:43.556 align:middle +Y aunque hay un montón de funciones dentro de +Webpack, lo único en lo que tenemos que centrarnos + +00:01:43.556 --> 00:01:46.536 align:middle +ahora es en esta: addEntry(). + +00:01:47.426 --> 00:01:49.436 align:middle +Este app puede ser cualquier cosa... + +00:01:49.646 --> 00:01:52.136 align:middle +como dinosaur, no importa. + +00:01:52.616 --> 00:01:54.596 align:middle +Te mostraré cómo se utiliza en un minuto. + +00:01:55.436 --> 00:02:00.846 align:middle +Lo importante es que apunta +al archivo assets/app.js. + +00:02:01.736 --> 00:02:08.946 align:middle +Por ello, app.js será el primer y +único archivo que Webpack analizará. + +00:02:09.396 --> 00:02:15.746 align:middle +Esto es bastante bueno: Webpack leerá el +archivo app.js y luego seguirá todas las + +00:02:15.746 --> 00:02:22.006 align:middle +sentencias de import de forma recursiva hasta +que finalmente tenga una colección gigante + +00:02:22.006 --> 00:02:25.926 align:middle +de todo el JavaScript y el CSS +que necesita nuestra aplicación. + +00:02:26.326 --> 00:02:30.076 align:middle +Entonces, lo escribirá +en el directorio public/. + +00:02:30.996 --> 00:02:32.156 align:middle +Veámoslo en acción. + +00:02:32.436 --> 00:02:38.846 align:middle +Busca tu terminal y ejecuta: +yarn watch Esto es, como dice, + +00:02:39.126 --> 00:02:42.766 align:middle +un atajo para ejecutar encore dev --watch. + +00:02:43.846 --> 00:02:49.226 align:middle +Si miras tu archivo package.json, viene +con una sección script con algunos atajos. + +00:02:51.306 --> 00:02:53.916 align:middle +En cualquier caso, yarn watch hace dos cosas. + +00:02:54.296 --> 00:03:04.746 align:middle +En primer lugar, crea un nuevo directorio +public/build/ y, dentro, los archivos app.css y app.js + +00:03:05.376 --> 00:03:10.356 align:middle +Pero no dejes que los nombres te +engañen: app.js contiene mucho más + +00:03:10.356 --> 00:03:14.396 align:middle +que lo que hay dentro de assets/app.js: + +00:03:14.966 --> 00:03:19.746 align:middle +contiene todo el JavaScript de todas +las importaciones que encuentra. + +00:03:20.476 --> 00:03:24.566 align:middle +app.css contiene todo el CSS +de todas las importaciones. + +00:03:25.516 --> 00:03:32.166 align:middle +La razón por la que estos archivos se llaman +app.css y app.js es por el nombre de la entrada. + +00:03:32.776 --> 00:03:38.046 align:middle +Así que la conclusión es que, gracias a +Encore, de repente tenemos nuevos archivos + +00:03:38.046 --> 00:03:45.356 align:middle +en el directorio public/build/ que contienen todo el +JavaScript y el CSS que necesita nuestra aplicación + +00:03:46.326 --> 00:03:50.236 align:middle +Y si vas a tu página de +inicio y la actualizas... + +00:03:50.756 --> 00:03:53.376 align:middle +¡woh! Ha funcionado al instante!? + +00:03:54.146 --> 00:03:55.546 align:middle +El fondo ha cambiado... + +00:03:55.826 --> 00:03:57.336 align:middle +y en mi inspector... + +00:03:57.726 --> 00:03:59.366 align:middle +¡está el registro de la consola! + +00:03:59.796 --> 00:04:01.416 align:middle +¿Cómo diablos ha ocurrido eso? + +00:04:02.306 --> 00:04:07.276 align:middle +Abre tu diseño base: templates/base.html.twig. + +00:04:08.016 --> 00:04:15.436 align:middle +El secreto está en las funciones +encore_entry_link_tags() y encore_entry_script_tags(). + +00:04:16.266 --> 00:04:22.636 align:middle +Apuesto a que puedes adivinar lo que hacen: +añadir la etiqueta link a build/app.css + +00:04:22.846 --> 00:04:25.286 align:middle +y la etiqueta script a build/app.js. + +00:04:25.286 --> 00:04:28.396 align:middle +Puedes ver esto en tu navegador. + +00:04:28.756 --> 00:04:31.166 align:middle +Mira la fuente de la página y... + +00:04:31.416 --> 00:04:35.486 align:middle +¡sí! La etiqueta link para /build/app.css... + +00:04:35.716 --> 00:04:39.136 align:middle +y la etiqueta script para /build/app.js. + +00:04:40.436 --> 00:04:43.726 align:middle +Ah, pero también ha renderizado +otras dos etiquetas script. + +00:04:44.286 --> 00:04:46.926 align:middle +Eso es porque Webpack es muy inteligente. Por + +00:04:47.456 --> 00:04:53.166 align:middle +motivos de rendimiento, en lugar de +volcar un gigantesco archivo app.js, + +00:04:53.546 --> 00:04:58.396 align:middle +a veces Webpack lo divide en +varios archivos más pequeños. + +00:04:59.566 --> 00:05:04.266 align:middle +Afortunadamente, estas funciones Twig de Encore son +lo suficientemente inteligentes como para manejar eso: + +00:05:04.266 --> 00:05:08.476 align:middle +incluirán todas las etiquetas de +enlace o de script necesarias. Lo + +00:05:09.426 --> 00:05:15.606 align:middle +más importante es que el código que +tenemos en nuestro archivo assets/app.js + +00:05:16.076 --> 00:05:22.186 align:middle +-incluyendo todo lo que importa- ¡ahora +funciona y aparece en nuestra página! + +00:05:23.256 --> 00:05:27.206 align:middle +Ah, y como hemos ejecutado yarn +watch, Encore sigue funcionando + +00:05:27.206 --> 00:05:29.196 align:middle +en segundo plano vigilando los cambios. + +00:05:29.196 --> 00:05:32.576 align:middle +Compruébalo: entra en app.css... + +00:05:32.986 --> 00:05:34.896 align:middle +y cambia el color de fondo. + +00:05:36.686 --> 00:05:39.066 align:middle +Guarda, pasa y actualiza. + +00:05:39.246 --> 00:05:42.596 align:middle +¡Se actualiza instantáneamente! + +00:05:43.266 --> 00:05:49.506 align:middle +Eso es porque Encore se ha dado cuenta del cambio y +ha recompilado el archivo construido muy rápidamente. + +00:05:50.906 --> 00:05:57.156 align:middle +A continuación: vamos a trasladar nuestro CSS existente +al nuevo sistema y a aprender cómo podemos instalar + +00:05:57.156 --> 00:06:00.926 align:middle +e importar bibliotecas de +terceros -mira Bootstrap + +00:06:00.926 --> 00:06:04.116 align:middle +o FontAwesome- directamente en +nuestra configuración de Encore diff --git a/sfcasts/ep1/es/webpack-encore.md b/sfcasts/ep1/es/webpack-encore.md new file mode 100644 index 0000000..d726504 --- /dev/null +++ b/sfcasts/ep1/es/webpack-encore.md @@ -0,0 +1,105 @@ +# Configuración de Webpack Encore + +Nuestra configuración de CSS está bien. Ponemos los archivos en el directorio `public/` y luego... apuntamos a ellos desde dentro de nuestras plantillas. Podríamos añadir archivos de JavaScript de la misma manera. + +Pero si queremos tomarnos realmente en serio la escritura de CSS y JavaScript, tenemos que llevar esto al siguiente nivel. E incluso si te consideras un desarrollador principalmente de backend, las herramientas de las que vamos a hablar te permitirán escribir CSS y JavaScript de forma más fácil y menos propensa a errores que a lo que probablemente estés acostumbrado. + +La clave para llevar nuestra configuración al siguiente nivel es aprovechar una biblioteca de nodos llamada Webpack. Webpack es la herramienta estándar de la industria para empaquetar, minificar y analizar tu CSS, JavaScript y otros archivos del frontend. Pero no te preocupes: Node es sólo JavaScript. Y su papel en nuestra aplicación será bastante limitado. + +Configurar Webpack puede ser complicado. Por eso, en el mundo Symfony, utilizamos una herramienta ligera llamada Webpack Encore. Sigue siendo Webpack... ¡sólo lo hace más fácil! Y tenemos un tutorial gratuito sobre ello si quieres profundizar. + +## Instalar Encore + +Pero vamos a hacer un curso intensivo ahora mismo. Primero, en tu línea de comandos, asegúrate de que tienes instalado Node: + +```terminal +node -v +``` + +También necesitarás `npm` -que viene con Node automáticamente- o `yarn`: + +```terminal-silent +yarn --version +``` + +Npm y yarn son gestores de paquetes de Node: son el Compositor para el mundo de Node... y puedes usar cualquiera de los dos. Si decides usar yarn - que es lo que yo usaré - asegúrate de instalar la versión 1. + +Estamos a punto de instalar un nuevo paquete... así que vamos a confirmar todo: + +```terminal +git add . +``` + +Y... se ve bien: + +```terminal-silent +git status +``` + +Así que confirma todo: + +```terminal-silent +git commit -m "Look mom! A real app" +``` + +Para instalar Encore, ejecuta: + +```terminal +composer require encore +``` + +Esto instala WebpackEncoreBundle. Recuerda que un bundle es un plugin de Symfony. Y este paquete tiene una receta: una receta muy importante. Ejecuta: + +```terminal +git status +``` + +## La receta de Encore + +Por primera vez, la receta ha modificado el archivo `.gitignore`. Vamos a comprobarlo. Abre `.gitignore`. Lo de arriba es lo que teníamos originalmente... y lo de abajo es lo nuevo que ha añadido WebpackEncoreBundle. Está ignorando el directorio`node_modules/`, que es básicamente el directorio `vendor/` para Node. No necesitamos confirmarlo porque esas bibliotecas de proveedores se describen en otro archivo nuevo de la receta: `package.json`. Este es el archivo `composer.json`de Node: describe los paquetes de Node que necesita nuestra aplicación. El más importante es el propio Webpack Encore, que es una biblioteca de Node. También tiene algunos otros paquetes que nos ayudarán a realizar nuestro trabajo. + +La receta también ha añadido un directorio `assets/`... y un archivo de configuración para controlar Webpack: `webpack.config.js`. El directorio `assets/` ya contiene un pequeño conjunto de archivos para que podamos empezar. + +## Instalar las dependencias de Node + +Bien, con Composer, si no tuviéramos este directorio `vendor/`, podríamos ejecutar`composer install` que le diría que leyera el archivo `composer.json` y volviera a descargar todos los paquetes en `vendor/`. Lo mismo ocurre con Node: tenemos un archivo `package.json`. Para descargarlo, ejecuta + +```terminal +yarn install +``` + +O: + +```terminal +npm install +``` + +¡Go node go! Esto tardará unos instantes mientras se descarga todo. Probablemente recibirás algunas advertencias como ésta, que puedes ignorar. + +¡Genial! Esto hizo dos cosas. En primer lugar, descargó un montón de archivos en el directorio`node_modules/`: el directorio de "proveedores" de Node. También creó un archivo`yarn.lock`... o `package-lock.json` si estás usando npm. Esto sirve para el mismo propósito de `composer.lock`: almacena las versiones exactas de todos los paquetes para que obtengas las mismas versiones la próxima vez que instales tus dependencias. + +En su mayor parte, no necesitas preocuparte por estos archivos de bloqueo... excepto que debes confirmarlos. Hagámoslo. Ejecuta: + +```terminal +git status +``` + +Entonces: + +```terminal +git add . +``` + +Hermoso: + +```terminal-silent +git status +``` + +Y confirma: + +```terminal-silent +git commit -m "Adding Webpack Encore" +``` + +¡Hey! ¡Ya está instalado Webpack Encore! Pero... ¡todavía no hace nada! Aprovechado. A continuación, vamos a utilizarlo para llevar nuestro JavaScript al siguiente nivel. \ No newline at end of file diff --git a/sfcasts/ep1/es/webpack-encore.vtt b/sfcasts/ep1/es/webpack-encore.vtt new file mode 100644 index 0000000..4e07e74 --- /dev/null +++ b/sfcasts/ep1/es/webpack-encore.vtt @@ -0,0 +1,256 @@ +WEBVTT + +00:00:01.136 --> 00:00:03.626 align:middle +Nuestra configuración de CSS está bien. + +00:00:04.176 --> 00:00:07.286 align:middle +Ponemos los archivos en el +directorio public/ y luego... + +00:00:07.636 --> 00:00:10.076 align:middle +apuntamos a ellos desde +dentro de nuestras plantillas. + +00:00:10.706 --> 00:00:13.236 align:middle +Podríamos añadir archivos de +JavaScript de la misma manera. + +00:00:14.026 --> 00:00:18.486 align:middle +Pero si queremos tomarnos realmente en +serio la escritura de CSS y JavaScript, + +00:00:18.926 --> 00:00:21.366 align:middle +tenemos que llevar esto al siguiente nivel. + +00:00:21.596 --> 00:00:27.146 align:middle +E incluso si te consideras un desarrollador principalmente +de backend, las herramientas de las que vamos a hablar + +00:00:27.146 --> 00:00:33.766 align:middle +te permitirán escribir CSS y JavaScript de +forma más fácil y menos propensa a errores + +00:00:33.766 --> 00:00:35.596 align:middle +que a lo que probablemente estés acostumbrado. + +00:00:36.846 --> 00:00:42.836 align:middle +La clave para llevar nuestra configuración al siguiente nivel +es aprovechar una biblioteca de nodos llamada Webpack. Webpack + +00:00:43.586 --> 00:00:46.576 align:middle +es la herramienta estándar de +la industria para empaquetar, + +00:00:46.636 --> 00:00:52.436 align:middle +minificar y analizar tu CSS, JavaScript +y otros archivos del frontend. + +00:00:53.086 --> 00:00:56.636 align:middle +Pero no te preocupes: Node es sólo JavaScript. + +00:00:56.896 --> 00:01:00.166 align:middle +Y su papel en nuestra aplicación +será bastante limitado. + +00:01:01.156 --> 00:01:03.266 align:middle +Configurar Webpack puede ser complicado. + +00:01:03.516 --> 00:01:09.736 align:middle +Por eso, en el mundo Symfony, utilizamos una +herramienta ligera llamada Webpack Encore. + +00:01:10.436 --> 00:01:11.766 align:middle +Sigue siendo Webpack... + +00:01:11.926 --> 00:01:13.616 align:middle +¡sólo que lo hace más fácil! + +00:01:13.866 --> 00:01:16.986 align:middle +Y tenemos un tutorial gratuito +sobre ello si quieres profundizar. + +00:01:17.676 --> 00:01:19.616 align:middle +Pero vamos a hacer un curso +intensivo ahora mismo. + +00:01:20.736 --> 00:01:24.546 align:middle +Primero, en tu línea de comandos, +asegúrate de que tienes instalado Node: + +00:01:25.046 --> 00:01:32.026 align:middle +node -v También necesitarás npm -que +viene con Node automáticamente- o yarn: + +00:01:32.846 --> 00:01:39.036 align:middle +Npm y yarn son gestores de paquetes de Node: +son el Compositor para el mundo de Node... + +00:01:39.276 --> 00:01:40.786 align:middle +y puedes usar cualquiera de los dos. + +00:01:41.346 --> 00:01:46.786 align:middle +Si decides usar yarn - que es lo que yo +usaré - asegúrate de instalar la versión 1. + +00:01:48.006 --> 00:01:49.866 align:middle +Estamos a punto de instalar un nuevo paquete... + +00:01:50.176 --> 00:01:53.556 align:middle +así que vamos a confirmar todo: git add . + +00:01:56.776 --> 00:02:04.166 align:middle +Y... se ve bien: Así que confirma +todo: Para instalar Encore, ejecuta: + +00:02:04.436 --> 00:02:10.576 align:middle +composer require encore Esto +instala WebpackEncoreBundle. + +00:02:11.476 --> 00:02:14.056 align:middle +Recuerda que un paquete +es un plugin de Symfony. + +00:02:14.456 --> 00:02:19.626 align:middle +Y este paquete tiene una receta: +una receta muy importante. + +00:02:20.576 --> 00:02:23.496 align:middle +Ejecuta: git status ¡Ooh! + +00:02:23.496 --> 00:02:29.656 align:middle +Por primera vez, la receta ha +modificado el archivo .gitignore. + +00:02:30.716 --> 00:02:31.626 align:middle +Vamos a comprobarlo. + +00:02:32.256 --> 00:02:33.466 align:middle +Abre .gitignore. + +00:02:35.206 --> 00:02:37.996 align:middle +Lo de arriba es lo que +teníamos originalmente... + +00:02:38.216 --> 00:02:42.966 align:middle +y aquí abajo está lo nuevo que +ha añadido WebpackEncoreBundle. + +00:02:43.606 --> 00:02:50.236 align:middle +Está ignorando el directorio node_modules/, que +es básicamente el directorio vendor/ de Node. + +00:02:51.136 --> 00:02:56.096 align:middle +No necesitamos confirmar eso porque esas +bibliotecas de proveedores se describen + +00:02:56.096 --> 00:03:00.246 align:middle +en otro archivo nuevo de +la receta: package.json. + +00:03:01.256 --> 00:03:07.756 align:middle +Este es el archivo composer.json de Node: describe +los paquetes de Node que necesita nuestra aplicación. + +00:03:08.416 --> 00:03:13.716 align:middle +El más importante es el propio Webpack +Encore, que es una biblioteca de Node. + +00:03:14.536 --> 00:03:18.376 align:middle +También tiene algunos otros paquetes que +nos ayudarán a realizar nuestro trabajo. + +00:03:19.636 --> 00:03:22.046 align:middle +La receta también ha añadido +un directorio assets/... + +00:03:23.046 --> 00:03:27.706 align:middle +y un archivo de configuración para +controlar Webpack: webpack.config.js. + +00:03:28.746 --> 00:03:33.466 align:middle +El directorio assets/ ya contiene un pequeño +conjunto de archivos para que podamos empezar. + +00:03:34.416 --> 00:03:41.426 align:middle +Vale, con Composer, si no tuviéramos este directorio +vendor/, podríamos ejecutar composer install + +00:03:41.716 --> 00:03:44.466 align:middle +que le diría que leyera +el archivo composer.json + +00:03:44.686 --> 00:03:47.736 align:middle +y volviera a descargar todos +los paquetes en vendor/. + +00:03:48.596 --> 00:03:53.496 align:middle +Lo mismo ocurre con Node: +tenemos un archivo package.json. + +00:03:55.006 --> 00:04:02.326 align:middle +Para descargarlo, ejecuta yarn +install O bien: npm install ¡Vaya + +00:04:02.906 --> 00:04:05.806 align:middle +nodo vaya! Esto tardará +unos instantes mientras se + +00:04:06.286 --> 00:04:09.756 align:middle +descarga todo. Probablemente recibirás +algunas advertencias como ésta, que puedes + +00:04:11.006 --> 00:04:13.426 align:middle +ignorar. ¡Genial! Esto hizo + +00:04:14.196 --> 00:04:16.936 align:middle +dos cosas. En primer +lugar, descargó un montón + +00:04:16.936 --> 00:04:21.226 align:middle +de archivos en el directorio node_modules/: +el directorio de "proveedores" + +00:04:22.386 --> 00:04:24.776 align:middle +de Node. También creó un + +00:04:25.106 --> 00:04:28.356 align:middle +archivoyarn.lock... o +package-lock.json si estás + +00:04:29.156 --> 00:04:35.996 align:middle +usando npm. Esto sirve para el mismo propósito que +composer.lock: almacena las versiones exactas de todos los + +00:04:36.316 --> 00:04:40.946 align:middle +paquetes para que obtengas las mismas +versiones la próxima vez que instales + +00:04:41.606 --> 00:04:45.566 align:middle +tus dependencias. En su mayor parte, +no necesitas preocuparte por estos + +00:04:45.816 --> 00:04:47.566 align:middle +archivos de bloqueo... excepto que debes +confirmarlos. + +00:04:48.146 --> 00:04:48.756 align:middle +Hagámoslo. + +00:04:49.256 --> 00:04:52.846 align:middle +Ejecuta: git status Despues: git add . + +00:04:55.106 --> 00:04:59.326 align:middle +Hermoso: Y confirma: ¡Hey! + +00:04:59.526 --> 00:05:02.106 align:middle +¡Ya está instalado Webpack Encore! + +00:05:02.426 --> 00:05:05.436 align:middle +Pero... ¡todavía no hace nada! + +00:05:05.766 --> 00:05:06.566 align:middle +Aprovechado. + +00:05:07.546 --> 00:05:11.776 align:middle +A continuación, vamos a utilizarlo para +llevar nuestro JavaScript al siguiente nivel. diff --git a/sfcasts/ep1/es/wildcard-route.md b/sfcasts/ep1/es/wildcard-route.md new file mode 100644 index 0000000..80ce65a --- /dev/null +++ b/sfcasts/ep1/es/wildcard-route.md @@ -0,0 +1,81 @@ +# Rutas comodín + +La página de inicio será el lugar donde el usuario podrá diseñar y construir su próxima cinta de mezclas. Pero además de crear nuevas cintas, los usuarios también podrán explorar las creaciones de otras personas. + +## Crear una segunda página + +Vamos a crear una segunda página para eso. ¿Cómo? Añadiendo un segundo controlador: función pública, qué tal `browse`: el nombre no importa realmente. Y para ser responsable, añadiré un tipo de retorno `Response`. + +Por encima de esto, necesitamos nuestra ruta. Ésta será exactamente igual, salvo que pondremos la URL en `/browse`. Dentro del método, ¿qué es lo que siempre devolvemos de un controlador? Así es: ¡un objeto `Response`! Devuelve un nuevo `Response`... con un mensaje corto para empezar. + +[[[ code('00c844ab6c') ]]] + +¡Vamos a probarlo! Si actualizamos la página de inicio, no cambia nada. Pero si vamos a `/browse`... ¡lo machacamos! ¡Una segunda página en menos de un minuto! ¡Caramba! + +En esta página, acabaremos por listar las cintas de mezclas de otros usuarios. Para ayudar a encontrar algo que nos guste, quiero que los usuarios también puedan buscar por género. Por ejemplo, si voy a `/browse/death-metal`, eso me mostraría todas las cintas de vinilo de death metal. Hardcore. + +Por supuesto, si probamos esta URL ahora mismo... no funciona. + +> No se ha encontrado la ruta + +No se han encontrado rutas coincidentes para esta URL, por lo que nos muestra una página 404. Por cierto, lo que estás viendo es la elegante página de excepciones de Symfony, porque estamos desarrollando. Nos da muchos detalles cuando algo va mal. Cuando finalmente despliegues a producción, puedes diseñar una página de error diferente que verían tus usuarios. + +## {Cartel de la muerte} Rutas + +De todos modos, la forma más sencilla de hacer que esta URL funcione es simplemente... cambiar la URL a`/browse/death-metal` + +[[[ code('7cf6861477') ]]] + +Pero... no es súper flexible, ¿verdad? Necesitaríamos una ruta para cada género... ¡que podrían ser cientos! Y además, ¡acabamos de matar la URL `/browse`! Ahora es 404. + +Lo que realmente queremos es una ruta que coincida con `/browse/`. Y podemos hacerlo con un comodín. Sustituye el código duro `death-metal` por `{}` y, dentro,`slug`. Slug es sólo una palabra técnica para designar un "nombre seguro para la URL". En realidad, podríamos haber puesto cualquier cosa dentro de las llaves, como `{genre}` o `{coolMusicCategory}`: no hay ninguna diferencia. Pero sea lo que sea que pongamos dentro de este comodín, se nos permite tener un argumento con ese mismo nombre: `$slug`. + +[[[ code('5a1436e579') ]]] + +Sí, si vamos a `/browse/death-metal`, coincidirá con esta ruta y pasará la cadena`death-metal` a ese argumento. La coincidencia se hace por nombre: `{slug}` conecta con `$slug`. + +Para ver si funciona, devolvamos una respuesta diferente: `Genre` y luego la `$slug`. + +[[[ code('90a1e7b05e') ]]] + +¡Hora de probar! Vuelve a `/browse/death-metal` y... ¡sí! Prueba con `/browse/emo` y ¡sí! ¡Estoy mucho más cerca de mi cinta de mezcla de Dashboard Confessional! + +Ah, y es opcional, pero puedes añadir un tipo `string` al argumento `$slug`. Eso no cambia nada... es sólo una bonita forma de programar: el `$slug` ya iba a ser siempre una cadena. + +[[[ code('dd04d150f1') ]]] + +Un poco más adelante, aprenderemos cómo puedes convertir un comodín numérico -como el número 5- en un número entero si así lo deseas. + +## Usando el componente de cadena de Symfony + +Hagamos esta página un poco más elegante. En lugar de imprimir el slug exactamente, vamos a convertirlo en un título. Digamos `$title = str_replace()` y sustituyamos los guiones por espacios. Luego, aquí abajo, utiliza el título en la respuesta. En un futuro tutorial, vamos a consultar la base de datos para estos géneros, pero, por ahora, al menos podemos hacer que tenga un aspecto más agradable. + +[[[ code('2d1891ae08') ]]] + +Si lo probamos, el Emo no se ve diferente... pero el death metal sí. ¡Pero quiero que sea más elegante! Añade otra línea con `$title =` y luego escribe `u` y autocompleta una función que se llama literalmente... `u`. + +No utilizamos muchas funciones de Symfony, pero éste es un ejemplo raro. Proviene de una biblioteca de Symfony llamada `symfony/string`. Como he mencionado, Symfony tiene muchas bibliotecas diferentes -también llamadas componentes- y vamos a aprovechar esas bibliotecas todo el tiempo. Esta te ayuda a hacer transformaciones de cadenas... y resulta que ya está instalada. + +Mueve el `str_replace()` al primer argumento de `u()`. Esta función devuelve un objeto sobre el que podemos hacer operaciones de cadena. Uno de los métodos se llama `title()`. Digamos `->title(true)` para convertir todas las palabras en mayúsculas y minúsculas. + +[[[ code('7ef6fbf8e0') ]]] + +Ahora, cuando lo probamos... ¡qué bien! ¡Pone las letras en mayúsculas! El componente de la cadena no es especialmente importante, sólo quiero que veas cómo podemos aprovechar partes de Symfony para hacer nuestro trabajo. + +## Hacer que el comodín sea opcional + +Bien: un último reto. Ir a `/browse/emo` o `/browse/death-metal` funciona. Pero ir a `/browse`... no funciona. ¡Está roto! Un comodín puede coincidir con cualquier cosa, pero, por defecto, se requiere un comodín. Tenemos que ir a`/browse/`. + +¿Podemos hacer que el comodín sea opcional? Por supuesto Y es deliciosamente sencillo: haz que el argumento correspondiente sea opcional. + +[[[ code('9ba3296dd1') ]]] + +En cuanto lo hagamos, le dirá a la capa de enrutamiento de Symfony que no es necesario que el `{slug}` esté en la URL. Así que ahora cuando refrescamos... funciona. Aunque no es un buen mensaje para la página. + +Veamos. Si hay un slug, pon el título como estábamos. Si no, pon`$title` a "Todos los géneros". Ah, y mueve el "Género:" aquí arriba... para que abajo en el `Response` podamos pasar simplemente `$title`. + +[[[ code('8cef2cd6cb') ]]] + +Inténtalo. En `/browse`... "Todos los géneros". En `/browse/emo`... "Género: Emo". + +Siguiente: poner un texto como éste en un controlador.... no es muy limpio ni escalable, especialmente si empezamos a incluir HTML. No, tenemos que hacer una plantilla. Para ello, vamos a instalar nuestro primer paquete de terceros y seremos testigos del importantísimo sistema de recetas de Symfony en acción. diff --git a/sfcasts/ep1/es/wildcard-route.vtt b/sfcasts/ep1/es/wildcard-route.vtt new file mode 100644 index 0000000..be5460b --- /dev/null +++ b/sfcasts/ep1/es/wildcard-route.vtt @@ -0,0 +1,395 @@ +WEBVTT + +00:00:01.016 --> 00:00:05.136 align:middle +La página de inicio será el lugar +donde el usuario podrá diseñar + +00:00:05.136 --> 00:00:07.906 align:middle +y crear su próxima cinta de mezclas. + +00:00:08.256 --> 00:00:12.396 align:middle +Pero además de crear nuevas cintas, +los usuarios también podrán + +00:00:12.396 --> 00:00:14.886 align:middle +examinar las creaciones de otras personas. + +00:00:15.616 --> 00:00:17.726 align:middle +Hagamos una segunda página para eso. + +00:00:18.676 --> 00:00:23.006 align:middle +¿Cómo? Añadiendo un segundo +controlador: función pública, + +00:00:23.226 --> 00:00:27.186 align:middle +qué tal browse: el nombre +no importa realmente. + +00:00:28.076 --> 00:00:32.356 align:middle +Y para ser responsable, añadiré +un tipo de retorno Response. + +00:00:33.776 --> 00:00:35.806 align:middle +Por encima de esto, necesitamos nuestra ruta. + +00:00:36.476 --> 00:00:41.486 align:middle +Ésta será exactamente igual, salvo +que pondremos la URL en /browse. + +00:00:42.566 --> 00:00:46.446 align:middle +Dentro del método, ¿qué es lo que +siempre devolvemos de un controlador? + +00:00:46.796 --> 00:00:49.566 align:middle +Así es: ¡un objeto Response! + +00:00:50.506 --> 00:00:51.976 align:middle +Devuelve un nuevo Response... + +00:00:52.426 --> 00:00:54.466 align:middle +con un mensaje corto para empezar. + +00:00:58.256 --> 00:00:59.176 align:middle +¡Vamos a probarlo! + +00:01:00.536 --> 00:01:03.386 align:middle +Si actualizamos la página +de inicio, no cambia nada. + +00:01:03.886 --> 00:01:06.016 align:middle +Pero si vamos a /browse... + +00:01:06.576 --> 00:01:08.086 align:middle +¡lo machacamos! + +00:01:08.406 --> 00:01:10.716 align:middle +¡Una segunda página en menos de un minuto! + +00:01:11.516 --> 00:01:16.136 align:middle +¡Caramba! En esta página, acabaremos listando +las cintas de mezclas de otros usuarios. + +00:01:16.806 --> 00:01:22.836 align:middle +Para ayudar a encontrar algo que nos guste, quiero que +los usuarios también puedan buscar por género. Por + +00:01:23.696 --> 00:01:27.656 align:middle +ejemplo, si voy a /browse/death-metal, + +00:01:27.926 --> 00:01:31.256 align:middle +eso me mostraría todas las +cintas de vinilo de death metal. + +00:01:31.796 --> 00:01:36.456 align:middle +Hardcore. Por supuesto, si +probamos esta URL ahora mismo... + +00:01:36.856 --> 00:01:38.106 align:middle +no funciona. + +00:01:38.296 --> 00:01:45.786 align:middle +No se ha encontrado ninguna ruta que coincida con esta +URL, por lo que nos muestra una página 404. Por cierto + +00:01:46.386 --> 00:01:50.726 align:middle +, lo que estás viendo es la elegante +página de excepciones de Symfony, + +00:01:50.856 --> 00:01:52.996 align:middle +porque estamos desarrollando. + +00:01:53.676 --> 00:01:57.086 align:middle +Nos da muchos detalles cuando algo va mal. + +00:01:57.676 --> 00:01:59.856 align:middle +Cuando finalmente despliegues a producción, + +00:02:00.086 --> 00:02:04.106 align:middle +puedes diseñar una página de error +diferente que verían tus usuarios. + +00:02:04.936 --> 00:02:09.266 align:middle +De todos modos, la forma más sencilla de +hacer que esta URL funcione es simplemente... + +00:02:09.266 --> 00:02:13.276 align:middle +cambiar la URL a /browse/death-metal. + +00:02:13.806 --> 00:02:16.776 align:middle +Pero... no es súper flexible, ¿verdad? + +00:02:17.316 --> 00:02:20.456 align:middle +Necesitaríamos una ruta para cada género... + +00:02:20.616 --> 00:02:22.446 align:middle +¡que podrían ser cientos! + +00:02:22.966 --> 00:02:26.356 align:middle +Y además, ¡acabamos de matar la URL /browse! + +00:02:26.796 --> 00:02:28.046 align:middle +Ahora es 404. + +00:02:29.066 --> 00:02:34.206 align:middle +Lo que realmente queremos es una ruta que +coincida con /browse/. + +00:02:34.606 --> 00:02:37.446 align:middle +Y podemos hacerlo con un comodín. + +00:02:38.036 --> 00:02:45.096 align:middle +Sustituye el código duro +death-metal por {} y, dentro, slug. + +00:02:46.086 --> 00:02:50.526 align:middle +Slug es sólo una palabra técnica para +designar un "nombre seguro para la URL". + +00:02:50.986 --> 00:02:56.106 align:middle +En realidad, podríamos haber puesto cualquier +cosa dentro de las llaves, como {genre} + +00:02:56.396 --> 00:03:01.046 align:middle +o {coolMusicCategory}: no +hay ninguna diferencia. + +00:03:01.676 --> 00:03:06.536 align:middle +Pero sea lo que sea que pongamos +dentro de este comodín, se nos permite + +00:03:06.536 --> 00:03:10.436 align:middle +tener un argumento con ese mismo nombre: $slug. + +00:03:11.386 --> 00:03:16.526 align:middle +Sí, si vamos a /browse/death-metal, +coincidirá con esta ruta + +00:03:16.526 --> 00:03:20.046 align:middle +y pasará la cadena +death-metal a ese argumento. + +00:03:20.546 --> 00:03:26.106 align:middle +La coincidencia se hace por +nombre: {slug} conecta con $slug. + +00:03:27.296 --> 00:03:32.676 align:middle +Para ver si funciona, devolvamos una +respuesta diferente: Genre y luego la $slug. + +00:03:34.306 --> 00:03:35.296 align:middle +¡Hora de probar! + +00:03:35.616 --> 00:03:40.106 align:middle +Vuelve a /browse/death-metal y... + +00:03:40.406 --> 00:03:45.936 align:middle +¡sí! Prueba con /browse/emo y ¡ sí! + +00:03:46.656 --> 00:03:50.656 align:middle +¡Estoy mucho más cerca de mi cinta +de mezcla de Dashboard Confessional! + +00:03:51.296 --> 00:03:55.736 align:middle +Ah, y es opcional, pero puedes añadir +un tipo string al argumento $slug. + +00:03:56.126 --> 00:03:57.816 align:middle +Eso no cambia nada... + +00:03:57.816 --> 00:04:03.616 align:middle +sólo es una forma de programar: el +$slug ya iba a ser siempre una cadena. + +00:04:04.436 --> 00:04:08.446 align:middle +Un poco más adelante, aprenderemos cómo +puedes convertir un comodín numérico + +00:04:08.756 --> 00:04:13.846 align:middle +-como el número 5- en un +número entero si así lo deseas. + +00:04:13.906 --> 00:04:15.766 align:middle +Hagamos esta página un poco más elegante. + +00:04:16.396 --> 00:04:20.146 align:middle +En lugar de imprimir el slug exactamente, +vamos a convertirlo en un título. + +00:04:20.746 --> 00:04:26.376 align:middle +Digamos $title = str_replace() y +sustituyamos los guiones por espacios. + +00:04:29.706 --> 00:04:32.746 align:middle +Luego, aquí abajo, utiliza +el título en la respuesta. + +00:04:33.486 --> 00:04:39.866 align:middle +En un futuro tutorial, vamos a consultar la base +de datos para estos géneros, pero, por ahora, + +00:04:39.956 --> 00:04:42.046 align:middle +al menos podemos hacer que +tenga un aspecto más agradable. + +00:04:42.756 --> 00:04:45.916 align:middle +Si lo probamos, el Emo no se ve diferente... + +00:04:47.306 --> 00:04:49.346 align:middle +pero el death metal sí. + +00:04:49.716 --> 00:04:51.796 align:middle +¡Pero quiero que sea más elegante! + +00:04:52.556 --> 00:04:56.616 align:middle +Añade otra línea con +$title = y luego escribe u + +00:04:56.706 --> 00:05:00.076 align:middle +y autocompleta una función +que se llama literalmente... + +00:05:00.306 --> 00:05:06.476 align:middle +u. No utilizamos muchas funciones de +Symfony, pero éste es un ejemplo raro. + +00:05:07.266 --> 00:05:11.786 align:middle +Proviene de una biblioteca de +Symfony llamada symfony/string. + +00:05:12.566 --> 00:05:18.486 align:middle +Como he mencionado, Symfony tiene muchas bibliotecas +diferentes -también llamadas componentes- + +00:05:18.746 --> 00:05:22.166 align:middle +y vamos a aprovechar esas +bibliotecas todo el tiempo. + +00:05:23.016 --> 00:05:25.766 align:middle +Esta te ayuda a hacer +transformaciones de cadenas... + +00:05:26.236 --> 00:05:29.116 align:middle +y resulta que ya está instalada. + +00:05:29.806 --> 00:05:32.906 align:middle +Mueve el str_replace() al +primer argumento de u(). + +00:05:34.176 --> 00:05:39.876 align:middle +Esta función devuelve un objeto sobre el +que podemos hacer operaciones de cadena. + +00:05:40.746 --> 00:05:42.206 align:middle +Uno de los métodos se llama title(). + +00:05:42.616 --> 00:05:47.146 align:middle +Digamos ->title(true) para convertir todas +las palabras en mayúsculas y minúsculas. + +00:05:48.136 --> 00:05:49.276 align:middle +Ahora, cuando lo probamos... + +00:05:50.456 --> 00:05:53.146 align:middle +¡qué bien! ¡Pone las letras en mayúsculas ! + +00:05:54.106 --> 00:05:57.526 align:middle +El componente de la cadena no es especialmente + +00:05:57.646 --> 00:06:02.826 align:middle +importante, sólo quiero que veas cómo podemos +aprovechar partes de Symfony para hacer nuestro trabajo. + +00:06:03.906 --> 00:06:05.636 align:middle +Vale: un último + +00:06:05.866 --> 00:06:11.436 align:middle +reto. Ir a /browse/emo o /browse/death-metal + +00:06:12.096 --> 00:06:14.256 align:middle +funciona. Pero ir a + +00:06:14.456 --> 00:06:16.096 align:middle +/browse... no + +00:06:16.256 --> 00:06:17.116 align:middle +funciona. ¡ Está + +00:06:17.576 --> 00:06:23.986 align:middle +roto! Un comodín puede coincidir con cualquier +cosa, pero, por defecto, se requiere un comodín. + +00:06:24.376 --> 00:06:28.226 align:middle +Tenemos que ir a + +00:06:28.746 --> 00:06:30.886 align:middle +/browse/. +¿Podemos hacer que el comodín + +00:06:31.496 --> 00:06:32.506 align:middle +sea opcional? + +00:06:32.876 --> 00:06:38.176 align:middle +Por supuesto Y es deliciosamente sencillo: +haz que el argumento correspondiente sea + +00:06:39.076 --> 00:06:42.176 align:middle +opcional. En cuanto lo hagamos, le dirá +a la capa de enrutamiento de Symfony + +00:06:42.246 --> 00:06:45.756 align:middle +que no es necesario que el {slug} esté en la + +00:06:46.076 --> 00:06:47.646 align:middle +URL. Así que ahora cuando + +00:06:48.116 --> 00:06:52.716 align:middle +refrescamos... funciona. Aunque +no es un buen mensaje para la + +00:06:53.676 --> 00:06:54.346 align:middle +página. + +00:06:54.716 --> 00:06:58.366 align:middle +Veamos. Si hay un slug, pon el título como + +00:06:59.276 --> 00:07:02.236 align:middle +estábamos. Si no, pon +$title a "Todos los géneros + +00:07:03.576 --> 00:07:06.626 align:middle +". Ah, y mueve el "Género:" + +00:07:07.156 --> 00:07:10.446 align:middle +aquí arriba... para que abajo en el +Response podamos pasar simplemente + +00:07:13.176 --> 00:07:15.226 align:middle +$title. Intentalo. En /browse ... + +00:07:16.276 --> 00:07:17.236 align:middle +"Todos los géneros". + +00:07:17.916 --> 00:07:19.496 align:middle +En /browse/emo... + +00:07:19.806 --> 00:07:21.076 align:middle +"Género: Emo". + +00:07:22.156 --> 00:07:25.366 align:middle +Siguiente: poner +un texto como éste en un + +00:07:25.806 --> 00:07:31.086 align:middle +controlador.... no es muy limpio ni escalable, +especialmente si empezamos a incluir + +00:07:32.096 --> 00:07:34.686 align:middle +HTML. No, tenemos que representar una + +00:07:35.146 --> 00:07:39.976 align:middle +plantilla. Para ello, vamos a instalar +nuestro primer paquete de terceros + +00:07:40.186 --> 00:07:45.136 align:middle +y seremos testigos del importantísimo +sistema de recetas de Symfony en acción diff --git a/sfcasts/ep1/flex-recipes.md b/sfcasts/ep1/flex-recipes.md new file mode 100644 index 0000000..fd65318 --- /dev/null +++ b/sfcasts/ep1/flex-recipes.md @@ -0,0 +1,167 @@ +# Flex Recipes + +We just installed a new package by running `composer require templates`. *Normally* +when you do that, Composer will update the `composer.json` and `composer.lock` files, +but nothing else. + +But when we run: + +```terminal +git status +``` + +There are *other* changes. This is thanks to Flex's recipe system. Each time we +install a new package, Flex checks a central repository to see if that package has +a recipe. And if it does, it installs it. + +## Where do Recipes Live? + +Where do these recipes live? In the cloud... or more specifically GitHub. Check +it out. Run: + +```terminal +composer recipes +``` + +This is a command *added* to composer by Flex. It lists all of the recipes that +have been installed. And if you want more info about one, just run: + +```terminal +composer recipes symfony/twig-bundle +``` + +This is one of the recipes that was just executed. And... cool! It shows us +a couple of nice things! The first is a tree of the files it *added* +to our project. The second is a URL to the recipe that was installed. +I'll click to open that. + +Yep! Symfony recipes live in a special repository called `symfony/recipes`. This +is a big directory organized by package name. There's a `symfony` directory that +holds recipes for all packages starting with `symfony/`. The one we were just +looking at... is way down here: `twig-bundle`. And then there are different +versions of the recipe based on your version of the package. We're using the latest +5.4 version. + +Every recipe has a `manifest.json` file, which controls what it does. The recipe +system can only do a specific set of operations, including adding new files +to your project and modifying a few *specific* files. For example, this `bundles` +section tells flex to add this line to our `config/bundles.php` file. + +If we run `git status` again... yup! That file *was* modified. If we diff it: + +```terminal-silent +git diff config/bundles.php +``` + +It added *two* lines, probably one for each of the *two* recipes. + +## Symfony Bundles? + +By the way, `config/bundles.php` is not a file that you need to think about +much. A bundle, in Symfony land, is basically a plugin. So if you install a new bundle +into your app, that gives you new Symfony features. In order to *activate* that +bundle, its name needs to live in this file. + +So the first thing that the recipe did for twig-bundle, thanks to this line up here, +was to activate itself inside `bundles.php`... so that we didn't need to do it +manually. Recipes are like automated installation. + +## New, Copied Files + +The *second* section in the manifest is called `copy-from-recipe`. This is simple: +it says to copy the `config/` and `templates/` directories from the recipe into the +project. If we look... the recipe contains a `config/packages/twig.yaml` file... +and also a `templates/base.html.twig` file. + +Back at the terminal, run `git status` again. We see these two files at the bottom: +`config/packages/twig.yaml`... and inside of `templates/`, `base.html.twig`. + +I *love* this. Think about it: if you install a templating tool into your app, +we're going to need some configuration *somewhere* that tells that templating tool +which directory to look inside of to find our templates. Whelp, go check out that +`config/packages/twig.yaml` file. We're going talk about these Yaml files more +in the next tutorial. But on a high level, this file controls how Twig - the +templating engine for Symfony - behaves. And check out the `default_path` key set +to `%kernel.project_dir%/templates`. Don't worry about this percent syntax: +that's a fancy way to refer to the root of our project. + +The point is, this config says: + +> Hey Twig! When you look for templates, look for them in the `templates/` directory. + +And then the recipe even *created* that directory with a layout file inside. We'll +use this in a few minutes. + +## symfony.lock & Committing Files + +The *last* unexplained file that was modified is `symfony.lock`. This is *not* +important: it just keeps track of which recipes have been installed... and you +*should* commit it. + +In fact, we should commit *all* of this stuff. The recipe might give us files, +but then they are *our's* to modify. Run: + +```terminal +git add . +``` + +Then: + +```terminal +git status +``` + +Cool. Let's commit! + +```terminal +git commit -m "Adding Twig and its beautiful recipe" +``` + +## Updating Recipes + +Done! By the way, a few months down the road, there might be *changes* to some +of the recipes that you've installed. And if there *are*, when you run + +```terminal +composer recipes +``` + +you'll see a little "update available" next to them. Run `composer recipes:update` +to upgrade to the latest version. + +Oh, and before I forget, in addition to `symfony/recipes`, there is also a +`symfony/recipes-contrib` repository. So recipes can live in *either* of these +two places. The recipes in `symfony/recipes` are approved by Symfony's core team, +so they're a bit more vetted for quality. Other than that, there's no difference. + +## Our Project Started as One File + +Now, the recipe system is *so* powerful that *every* single file in our project was +*added* via a recipe! I can prove it. Go to https://github.com/symfony/skeleton. + +When we originally ran that `symfony new` command to start our project, what that +*really* did was *clone* this repository... and then ran `composer install` inside +of it, which downloads everything into the `vendor/` directory. + +Yup! Our project - the one that we see right here - was originally *just* a single +file: `composer.json`. But then, when the packages were installed, the *recipes* +for those packages added *everything* else we see. Run: + +```terminal +composer recipes +``` + +again. One recipe is for something called `symfony/console`. Check out its +details: + +```terminal +composer recipes symfony/console +``` + +And... yes! The recipe for `symfony/console` added the `bin/console` file! The recipe +for `symfony/framework-bundle` - one of the other packages that was originally +installed - added almost everything else, including the `public/index.php` +file. How cool is that? + +Okay next: we installed Twig! So let's get back to work and use it to render some +templates! You're going to love Twig. diff --git a/sfcasts/ep1/flex.md b/sfcasts/ep1/flex.md new file mode 100644 index 0000000..0f7ce19 --- /dev/null +++ b/sfcasts/ep1/flex.md @@ -0,0 +1,154 @@ +# Symfony Flex: Aliases, Packs & Recipes + +Symfony is a set of libraries that gives us tons of tools: tools for logging, making +database queries, sending emails, rendering templates and making API calls, just to +name a few. If you counted them, I did, Symfony consists of about 100 separate +libraries. Wow! + +Right now, I want to start turning our pages into true HTML pages... instead of just +returning text. But we are *not* going to jam a bunch of HTML into our PHP classes. +Yuck. Instead, we're going to render a template. + +## Symfony's Start Small & Install Features Philosophy + +But guess what? There is no templating library in our project! What? But I thought +you just said that Symfony has a tool for rendering templates!? Lies! + +Well... Symfony *does* have a tool for that. But our app currently uses very +*few* of the Symfony libraries. The tools we have so far don't amount to much more +than a route-controller-response system. If you need to render a template or make +a database query, we do *not* have *those* tools installed in our app... yet. + +I actually *love* this about Symfony. Instead of starting us with a gigantic project, +with everything we need, plus *tons* of stuff that we *don't* need, Symfony starts +tiny. *Then*, if you need something, you install it! + +But before we install a templating library, at your terminal, run: + +```terminal +git status +``` + +Let's commit everything: + +```terminal +git add . +``` + +I can safely run `git add .` - which adds *everything* in my directory to git - +because one of the files that our project *originally* came with was a `.gitignore` +file, which already ignores stuff like the `vendor/` directory, `var/` directory, +and several other paths. If you're wondering what these weird marker things are, +that's related to the recipe system, which we're about to talk about. + +Anyways, run `git commit` and add a message: + +```terminal-silent +git commit -m "route -> controller -> response -> profit" +``` + +Perfect! And now, we are clean. + +## Installing a Templating Library (Twig) + +Okay. So how can we install a templating library? And what templating libraries are +even *available* for Symfony? And which is recommended? Well, of course, a great +way to answer these questions would be check the Symfony documentation. + +But we can also just... guess! In *any* PHP project, you can add new third-party +libraries to your app by saying "composer require" and then the package name. +We don't *know* the package name we need yet, so I'll just guess: + +```terminal +composer require templates +``` + +Now, if you've used Composer before, you might be *screaming* at your screen right +about now! Why? Because in Composer, package names are *always* `something/something`. +It is literally *not* possible to have a package just named `templates`. + +But watch: when we run this, it works! And up on top, it says using version 1 +for `symfony/twig-pack`. Twig is the name of the templating engine for Symfony. + +## Flex Aliases + +To understand this, let's take a tiny step backwards. Our project started with a +`composer.json` file containing several Symfony libraries. One of these is called +`symfony/flex`. Flex is a composer *plugin*. So it *adds* more features to composer. +Actually, it adds *three* superpowers to composer. + +***TIP +The flex.symfony.com server was shut down in favor of a new system. But you can still +see a list of all of the available recipes at https://bit.ly/flex-recipes! +*** + +The first, which we just saw, is called Flex aliases. Head to https://flex.symfony.com +to see a *giant* page full of packages. Search for "templates". Here it is. +Under `symfony/twig-pack`, it says Aliases: template, templates, twig, and twig-pack. + +The idea between behind Flex aliases is dead simple. We type +`composer require templates`. And then, internally, Flex *changes* that to +`symfony/twig-pack`. Ultimately, *that* is the package that Composer installs. + +This means that, most of the time, you can just "composer require" whatever you want, +like `composer require logger`, `composer require orm`, `composer require icecream`, +whatever. It's just a shortcut system. The important point is that, what *really* +got installed was `symfony/twig-pack`. + +## Flex Packs + +And *that* means that, in our `composer.json` file, we *should* now see +`symfony/twig-pack` under the `require` key. But if you spin over, it's not there! +Gasp! Instead, it added `symfony/twig-bundle`, `twig/extra-bundle`, and `twig/twig`. + +We're witnessing the *second* superpower of Symfony Flex: unpacking packs. +Copy the original package name and... we can actually find that repository on GitHub +by going to https://github.com/symfony/twig-pack. + +And... it contains just *one* file: `composer.json`. And *this* requires three +*other* packages: the three we just saw added to *our* project. + +This is called a Symfony pack. It's... really just a fake package that helps us +install *other* packages. It turns out, if you want a rich template engine to be +added to your app, we recommend installing these *three* packages. But instead of +making you add them manually, you can composer require `symfony/twig-pack` and +get them automatically. When you install a "pack", like this, Flex automatically +"unpacks" it: it finds the three packages that the pack depends on and adds *those* +into your `composer.json` file. + +So, packs are a shortcut so that you can run one `composer require` command and +get *multiple* libraries added to your project. + +Ok, so what is the third and final superpower of Flex? So glad you asked! To find +out, at your terminal, run: + +```terminal +git status +``` + +## Flex Recipes + +Whoa. Normally when you run `composer require`, the only files it should modify - +other than downloading packages into `vendor/` - are `composer.json` and +`composer.lock`. Flex's third superpower is its recipes system. + +Whenever you install a package, that package *may* have a *recipe*. If it does, +in addition to downloading the package into the `vendor/` directory, Flex will +also *execute* its recipe. Recipes can do things like add new files or even modify +a few existing files. + +Watch: if we scroll up a little, ah yes: it says "configuring 2 recipes". So +apparently there was a recipe for `symfony/twig-bundle` and also a recipe for +`twig/extra-bundle`. And these recipes apparently updated the `config/bundles.php` +file and added a new directory and file. + +The recipe system is *sweet*. All *we* need to do is composer require a new library +and its recipe will then add all the configuration files or other setup needed +so that we can start *using* that library immediately! No more following 5 manual +"installation" steps in a README. When you add a library, it works out-of-the-box. + +Next: I want to dive a bit deeper into the recipes. Like, where do they live? +What's their favorite color? And what did this recipe added specifically to our app +and why? I'm also going to let you in on a little secret: *every* file on our +project - all the files in `config/`, the `public/` directory... *all* of this stuff - +was added via a recipe. And I'll prove it. diff --git a/sfcasts/ep1/generate-urls.md b/sfcasts/ep1/generate-urls.md new file mode 100644 index 0000000..fa8f60a --- /dev/null +++ b/sfcasts/ep1/generate-urls.md @@ -0,0 +1,153 @@ +# Generate Urls & bin/console + +There are two different ways that we can interact with our app. The first is +via the web server... and that's what we've been doing! We got to a URL and... +behind the scenes, it executes `public/index.php`, which boots up Symfony, calls +the routing and runs our controller. + +## Hello bin/console + +What's the *second* way we can interact with our app? We haven't seen it yet: it's +via a command line tool called `bin/console`. At your terminal run: + +```terminal +php bin/console +``` + +... to see a *bunch* of commands *within* this script. I *love* this thing. It's +full of stuff to help us debug, eventually it will have code-generation commands, +commands for setting secrets: all kinds of good stuff that we're going to discover +little-by-little. + +But I *do* want to point out that... there's nothing special about this `bin/console` +script! It's just a file: there's literally a `bin/` directory with a `console` +file inside. You'll probably never need to open this file or think about it, but +it *is* useful. Oh, and on most systems, you can just run: + +```terminal +./bin/console +``` + +... which looks cooler. Or sometimes you may see me run: + +```terminal +symfony console +``` + +... which is just *another* way to execute this file. We'll talk more about this +in a future tutorial. + +## bin/console debug:router + +The *first* command I want to check out inside of `bin/console` is `debug:router`: + +```terminal-silent +php bin/console debug:router +``` + +This is awesome. It shows us *every* route in our app, like *our* two routes +for `/` and `/browse/{slug}`. What are these other routes? They come form the +web debug toolbar and profiler system... and they're only here while we're +developing locally. + +Ok, back on our site.... at the top of the page, we have two non-functional links +to the homepage and browse page. Let's hook these up. Open `templates/ +base.html.twig`... and search for `a` tags. Here we go. + +So it would be *really* easy to get this working by just `href="/"`. But instead, +whenever we link to a page in Symfony, we're going to ask the routing system to +*generate* a URL for us. We'll say: + +> Please generate the URL to the homepage's route, or the browse page's route. + +*Then*, if we ever change the URL of a route, all our links will instantly update. +Magic. + +## Naming your Route + +Let's start with the homepage. How do we ask Symfony to generate a URL to this +route? Well first, we need to give the route a *name*. Surprise! *Every* route has +an internal name. You can see it back in `debug:router`. Our route's are +named `app_vinyl_homepage` and `app_vinyl_browse`. Huh, those are the *exact* +names of my pet turtles when I was kid. + +Where did these names come from? By default, Symfony automatically generates a name +*for* us, which is fine. The name isn't used at *all* until we generate a URL to it. +And as soon as we *do* need to generate a URL to a route, I highly recommend +taking control of this name... just to make sure it never accidentally changes. + +To do this, find the route and add an argument: `name` set to, how about, +`app_homepage`. I like using the `app_` prefix: it makes it easier to search +for a route name later. + +[[[ code('25d040ab5a') ]]] + +By the way, PHP 8 attributes - like this `Route` attribute - are represented by +actual, physical PHP classes. If you hold command or ctrl, you can open it and look +inside. This is great: the `__construct()` method shows all of the different +*options* you can pass to the attribute. + +For example, there's a `name` argument... and then we're using PHP's named +argument syntax to pass this into the attribute. Opening up an attribute is a +*great* way to learn about its options. + +## Generating a URL from Twig + +Anyways, now that we've given this a name, go back to our terminal and run +`debug:router` again: + +```terminal-silent +php bin/console debug:router +``` + +This time... yea! The route is named `app_homepage`! Copy that, then head back +to `base.html.twig`. To generate a URL inside of twig, say `{{` - because +we're going to print something - and then use a Twig function called `path()`. +Pass this the route name. + +[[[ code('8a471c3074') ]]] + +Done! Refresh... and the link up here works! + +One more link to go. We know step one: give the route a name. So `name:` and, how +about, `app_browse`. + +[[[ code('c8ff1fc6dd') ]]] + +Copy that, and... scroll down a bit. Here it is: "Browse Mixes". Change that +to `{{ path('app_browse') }}`. + +[[[ code('802a4800ee') ]]] + +And now... that link works too! + +## Generating URLs with Wildcards + +Oh, but on this page, we have some quick links to go to the browse page for a specific +genre. And these do *not* work yet. + +This is interesting. We want to generate a URL like before... but this time we need +to pass something to the `{slug}` wildcard. Open `browse.html.twig`. Here's how +we do that. The first part is the same: `{{ path() }}` and then the name +of the route: `app_browse`. + +If we stopped here, it would generate `/browse`. To pass values to any wildcards +in a route, `path()` has a second argument: an associative array of those value. +And, again, just like JavaScript, to create an "associative array", you use +`{` and `}`. I'm going to hit enter to break this onto multiple lines... just +to keep things readable. Inside add a `slug` key to the array... and since this is +the "Pop" genre, set it to `pop`. + +Cool! Let's repeat this two more times: `{{ path('app_browse') }}`, pass curly +braces for an associative array, with `slug` set to `rock`. And then one more time +down here... which I'll do really quickly. + +[[[ code('761700d7e5') ]]] + +Let's see if it works! Refresh. Ah! Variable `rock` doesn't exist. I bet some of +you saw me do that. I forgot my quotes, so this looks like a *variable*. + +Try it again. There we go. And try the links... yes! They work! + +Next: we've created two HTML pages. Now let's see what it looks like to create +a JSON API endpoint. diff --git a/sfcasts/ep1/js-vendor-libs.md b/sfcasts/ep1/js-vendor-libs.md new file mode 100644 index 0000000..39fe812 --- /dev/null +++ b/sfcasts/ep1/js-vendor-libs.md @@ -0,0 +1,102 @@ +# Installing 3rd Party Code into our JS/CSS + +We now have a nice new JavaScript and CSS system that lives entirely inside of the +`assets/` directory. Let's move our public styles into this. Open +`public/styles/app.css`, copy all of this, delete the *entire* directory... and +then paste into the new `app.css`. Thanks to the `encore_entry_link_tags()` in +`base.html.twig`, the new CSS *is* being included... and we don't need the old +`link` tag anymore. + +Go check it out. Refresh and... it still looks great! + +## Installing 3rd Party JavaScript/CSS Libraries + +Go back to `base.html.twig`. What about these external link tags for bootstrap +and FontAwesome? Well, you can *totally* keeps these CDN links. But we can +*also* process this stuff through Encore. How? By installing Bootstrap and +FontAwesome as *vendor* libraries and *importing* them. + +Remove *all* of these link tags... and then refresh. Yikes! It's back to looking +like *I* designed this site. Let's... *first* re-add bootstrap. Find your terminal. +Since the watch command is running, open a new terminal tab and then run: + +```terminal +yarn add bootstrap --dev +``` + +This does three things. First, it adds `bootstrap` to our `package.json` file. Second +it downloads bootstrap into our `node_modules/` directory... you would find it down +here. And *third*, it updated the `yarn.lock` file with the exact version of bootstrap +that it just downloaded. + +If we stopped now... this wouldn't make any difference! We downloaded bootstrap - +yay - but we're not using it. + +To use it, we need to *import* it. Go into `app.css`. Just like in JavaScript files, +we can import from inside *CSS* files by saying `@import` and then the file. We could +reference a file in the same directory with `./other-file.css`. *Or*, if you want +to import something from the `node_modules/` directory in CSS, there's a trick: +a `~` and then the package name: `bootstrap`. + +[[[ code('43abc8b3a6') ]]] + +That's it! As *soon* as we did that, Encore's watch function rebuilt our `app.css` +file... which now *includes* Bootstrap! Watch: refresh the page and... we're back! +So cool! + +The two *other* things we're missing are `FontAwesome` and a specific Font. To add +those, head back to the terminal and run: + +```terminal +yarn add @fontsource/roboto-condensed --dev +``` + +Full disclosure: I did some searching *before* recording so that I knew the names +of all the packages we need. You can search for packages at https://npmjs.com. + +Let's also add the last one we need: + +```terminal +yarn add @fortawesome/fontawesome-free --dev +``` + +Again, this downloaded the two libraries into our project... but doesn't automatically +*use* them yet. Because those libraries both hold CSS files, go back to our +`app.css` file and import them: `@import '~'` then `@fortawesome/fontawesome-free`. +And `@import '~@fontsource/roboto-condensed'`. + +[[[ code('c72cdebd5f') ]]] + +The first package should fix this icon... and the second should cause the font to +change on the whole page. Watch the font when we refresh... it *did* change! But, +uh... the icons are still kind of broken. + +## Importing Specific Files from node_modules/ + +To be totally honest, I'm not sure why that doesn't work out-of-the box. But the +fix is kind of interesting. Hold command on a Mac - or ctrl otherwise - and +click this `fontawesome-free` string. + +When you use this syntax, it goes into your `node_modules/` directory, into +`@fortawesome/fontawesome-free`... and then if you don't put any filename after this, +there's a mechanism where this library tells Webpack *which* CSS file it should +import. By default, it imports this `fontawesome.css` file. For some reason... that +doesn't work. What we want is this `all.css`. + +And we can import *that* by adding the path: `/css/all.css`. We don't need the +minified file because Encore handles minifying for us. + +[[[ code('0e00a5819a') ]]] + +And now... we're back! + +The *main* reason I love Webpack Encore and this system is that it allows us to +use proper imports. We can even organize *our* JavaScript into small files - putting +classes or functions into each - and then import them when we need them. There's no +more need for global variables. + +Webpack also allows us to use more serious stuff like React or Vue: you can even +see, in `webpack.config.js`, the methods to activate those. + +But usually, I like using a delightful JavaScript library called Stimulus. And I +want to tell you about it next. diff --git a/sfcasts/ep1/json-api.md b/sfcasts/ep1/json-api.md new file mode 100644 index 0000000..b4b72ac --- /dev/null +++ b/sfcasts/ep1/json-api.md @@ -0,0 +1,83 @@ +# JSON API Endpoint + +In a future tutorial, we're going to create a database to manage songs, genres, and +the mixed vinyl records that our users are creating. Right now, we're working +entirely with hardcoded data... but our controllers - and - especially templates +won't feel *that* much different once we make this all dynamic. + +So here's our new goal: I want to create an API endpoint that will return the data +for a single song as JSON. We're going to use this in a few minutes to bring this +play button to life. At the moment, none of these buttons do anything, but they +do look pretty. + +## Creating the JSON Controller + +The two steps to creating an API endpoint are... exactly the same as creating an +HTML page: we need a route and a controller. Since this API endpoint will be returning +*song* data, instead of adding *another* method inside of `VinylController`, let's +create a totally new controller class. How you organize this stuff is entirely up +to you. + +Create a new PHP class called `SongController`... or `SongApiController` would also +be a good name. Inside, this will start like any other controller, by extending +`AbstractController`. Remember: that's optional... but it gives us shortcut methods +with no downside. + +Next, create a `public function` called, how about, `getSong()`. Add the route... +and hit tab to auto-complete this so that PhpStorm adds the use statement on top. +Set the URL to `/api/songs/{id}`, where `id` will eventually be the database id +of the song. + +And because we have a wildcard in the route, we are *allowed* to have an `$id` +argument. *Finally*, even though we don't *need* to do this, because we know that +our controller will return a `Response` object, we can set that as the return type. +Make sure to auto-complete the one from Symfony's `HttpFoundation` component. + +Inside the method, to start, `dd($id)`... *just* to see if everything is +working. + +[[[ code('ad23fa7738') ]]] + +Let's do this! Head over to `/api/songs/5` and... got it! *Another* new page. + +Back in that controller, I'm going to paste in some song data: eventually, this +will come from the database. You can copy this from the code block on this page. +Our job is to return this as JSON. + +So how *do* we return JSON in Symfony? By returning a new `JsonResponse` and passing +it the data. + +[[[ code('32ff0953ad') ]]] + +I know... too easy! Refresh and... hello JSON! Now you *might* be thinking: + +> Ryan! You've been telling us - repeatedly - that a controller must all +> always return a Symfony `Response` object, which is what `render()` returns. +> Now you're returning some *other* type of `Response` object? + +Ok, *fair*... but this works because `JsonResponse` *is* a Response. Let me explain. +Sometimes it's useful to jump into core classes to see how they work. To do that, +in PHPStorm - if you're on a Mac hold command, otherwise hold control - and then +click the class name that you want to jump into. And... surprise! `JsonResponse` +*extends* `Response`. Yea, we're still returning a `Response`. But this sub-class +is nice because it automatically JSON encodes our data *and* sets the +`Content-Type` header to `application/json`. + +## The ->json() Shortcut Method + +Oh, and back in our controller, we can be even *lazier* by saying +`return $this->json($song)`... where `json()` is *another* shortcut method that +comes from `AbstractController`. + +[[[ code('6499fe8656') ]]] + +Doing this makes absolutely *no* difference because this is just a shortcut to return +... a `JsonResponse`! + +If you're building a serious API, Symfony has a +`serializer` component that's really good at turning objects into JSON... and then +JSON back into objects. We talk a lot about it in our API Platform tutorial, which +is a powerful library for creating APIs in Symfony. + +Next, let's learn how to make our routes smarter, like by making a wildcard only +match a *number*, instead of matching anything. diff --git a/sfcasts/ep1/metadata.yml b/sfcasts/ep1/metadata.yml new file mode 100644 index 0000000..f47f5fe --- /dev/null +++ b/sfcasts/ep1/metadata.yml @@ -0,0 +1,22 @@ +chapters: + - setup + - directories + - route-controller + - wildcard-route + - flex + - flex-recipes + - twig + - twig-inheritance + - profiler + - assets + - generate-urls + - json-api + - route-requirements + - services + - twig-service + - webpack-encore + - webpack-encore-usage + - js-vendor-libs + - stimulus + - stimulus-example + - turbo diff --git a/sfcasts/ep1/profiler.md b/sfcasts/ep1/profiler.md new file mode 100644 index 0000000..c9fd4ad --- /dev/null +++ b/sfcasts/ep1/profiler.md @@ -0,0 +1,144 @@ +# Profiler: Your Debugging Best Friend + +Time to install our *second* package. And this one is *fun*. Let's commit our +changes first: it'll makes it easier to check out any changes that the new package's +recipe makes. + +Add everything: + +```terminal-silent +git add . +``` + +That looks fine so... commit: + +```terminal-silent +git commit -m "Added some Tiwggy goodness" +``` + +Beautiful. + +## The debug Pack + +*Now* run: + +```terminal +composer require debug +``` + +So yes, this is *another* Flex alias... and apparently it's an alias for +`symfony/debug-pack`. And *we* know that a pack is a *collection* of packages. So +instead of adding this *one* line to our `composer.json` file, if we check, +it looks like it added one new package up under the `require` section - this +is a logging library - and... *all* the way at the bottom, it added a new +`require-dev` section with three *other* libraries. + +The difference between `require` and `require-dev` isn't too important: all of these +packages were downloaded into our app, But as a best practice, if you install a +library that's only meant for *local* development, you should put it into +`require-dev`. The pack did that for us! Thanks pack! + +## Recipe Changes + +Back at the terminal, this also installed three recipes! Ooh. Let's see what those +did. I'll clear the screen and run: + +```terminal +git status +``` + +So this is familiar: it modified `config/bundles.php` to activate three +new bundles. Again, bundles are Symfony plugins, which add more features to +our app. + +It also added several configuration files to the `config/packages/` directory. +We will talk more about these files in the next tutorial, but, on a high level, +they control the *behavior* of those bundles. + +## The Web Debug Toolbar And Profiler + +So what *did* these new bundles give us? To find out, head over to your browser and +refresh the homepage. Holy cats, Batman! It's the web debug toolbar. This is debugging +*madness*: a toolbar *full* of good info. On the left, you can see the controller +that was called along with the HTTP status code. There's also the amount of time +the page took, the memory it used and also how many templates were rendered +through Twig: this is the cute Twig icon. + +On the right side, we have details about the Symfony local web server that's running +and PHP info. + +But you haven't seen the best part: click any of these icons to jump into the +*profiler*. This is the web debug toolbar... gone crazy. It's *full* of +data about that request, like the request and response, all of the log messages +that happened during that request, information about the routes and the route +that was matched, Twig shows you which templates were rendered and how many *times* +they were rendered... and there's configuration information down here. Phew! + +But my *most* favorite section is Performance. This shows a timeline of everything +that happened during the request. This is great for two reasons. The first is +pretty obvious: you can use this to find which parts of your page are slow. +So, for example, our controller took 20.4 millisecond. And *within* the controller's +execution, the homepage template rendered in 3.9 milliseconds and `base.html.twig` +rendered in 2.8 milliseconds. + +The second reason this is really cool is that it uncovers all the hidden layers +of Symfony. Set this threshold down to zero. Previously, this only displayed things +that took *more* than one millisecond. Now it's showing *everything*. You don't +need to worry about the vast majority of the stuff, but it's *super* cool to see +the layers of Symfony: the things that happen before and after your controller +is executed. We have a deep dive tutorial for Symfony if you want to learn more +about this stuff. + +The web debug toolbar and profiler will also grow with our app. In a future +tutorial, when we install a library to talk to the database, we will suddenly have +a new section that lists all of the database queries that a page made and how long +each took. + +## dump() and dd() Functions + +Ok, so the debug pack installed the web debug toolbar. It also installed a logging +library that we'll use later. *And* it installed a package that gives us two fantastic +debug functions. + +Head over to `VinylController`. Pretend that we're doing some developing and we +need to see what this `$tracks` variable looks like. It's pretty obvious in this +case, but sometimes you'll want to see what's inside a complex object. + +To do that, say `dd($tracks)` where "dd" stands for dump and die. + +[[[ code('841aecc2d1') ]]] + +So if we refresh... yup! That dumps the variable and kills the page. And this is +a *lot* more powerful - and prettier - than using `var_dump()`: we can expand sections +and see deep data really easily. + +Instead of `dd()`, you can also use `dump()`.. to dump and *live*. But this might +not show up where you expect it to. Instead of printing in the middle of the page, +it shows up down in the web debug toolbar, under the target icon. + +[[[ code('13b985bb4b') ]]] + +If that's a bit too small, click to see a bigger version in the profiler. + +## Dumping in Twig + +You can *also* use this `dump()` in Twig. Remove the dump from the controller... +and then in the template, right before the `ul`, `dump(tracks)`. + +[[[ code('ee2dc39650') ]]] + +And this... looks exactly the same. Except that when you dump in Twig, it +*does* dump right into the middle of the page + +And even *more* useful, in Twig *only*, you can use `dump()` with no arguments. + +[[[ code('43c4067dd7') ]]] + +This will dump *all* variables that we have access to. So here's the `title` variable, +`tracks` and, surprise! There's a third variable called `app`. This is a global +variable that we have in *all* templates... and it gives us access to things like +the session and user data. And... we just discovered it by being curious! + +So now that we've got these awesome debugging tools, let's turn to our next +job... which is to make this site less ugly. Time to add CSS and a proper +layout to bring our site to life! diff --git a/sfcasts/ep1/route-controller.md b/sfcasts/ep1/route-controller.md new file mode 100644 index 0000000..88dd46a --- /dev/null +++ b/sfcasts/ep1/route-controller.md @@ -0,0 +1,142 @@ +# Routes, Controllers & Responses + +I gotta say, I miss the 90's. Well, not the beanie babies and... definitely not +the way *I* dressed back then, but... the mix tapes. If you weren't a kid in the +80's or 90's, you may not know how hard it was to share your favorite tunes with +your friends. Oh yea, I'm talking about a Michael Jackson, Phil Collins, Paula +Abdul mashup. Perfection. + +To capitalize off of that nostalgia, but with a hipster twist, we're going to create +a brand new app called Mixed Vinyl: a store where users can create mix tapes - +complete with Boyz || Men, Mariah Carey and Smashing Pumpkins... except pressed +onto a vinyl record. Hmm, I might need to put a record player in my car. + +The page we're looking at, which is super cute and changes colors when we refresh... +is not a real page. It's just a way for Symfony to say "hi" and link us to the +documentation. And by the way, Symfony's documentation is great, so definitely +check it out as you're learning. + +## Routes & Controllers + +Ok: *every* web framework in *any* language has the same job: to help us create pages, +whether those are HTML pages, JSON API responses or ASCII art. And pretty much +every framework does this in the same way: via a route & controller system. The +route defines the URL for the page and points to a controller. The controller is +a PHP function that builds that page. + +So route + controller = page. It's math people. + +## Creating the Controller + +We're going to build these two things... kind of in reverse. So first, let's create +the controller function. In Symfony, the controller function is always a *method* +inside of a PHP class. I'll show you: in the `src/Controller/` directory, create +a new PHP class. Let's call it `VinylController`, but the name could be anything. + +[[[ code('6709e819e9') ]]] + +And, congrats! It's our first PHP class! And guess where it lives? In the `src/` +directory, where *all* PHP classes will live. And for the most part, it doesn't +matter how you organize things inside `src/`: you can usually put things into whatever +directory you want and name the *classes* whatever you want. So flex your +creativity. + +***TIP +Controllers actually *do* need to live in `src/Controller/`, unless you change +some config. Most PHP classes can live anywhere in `src/`. +*** + +But there *are* two important rules. First, notice the namespace that +PhpStorm added on top of the class: `App\Controller`. *However* you decide to organize +your `src/` directory, the namespace of a class *must* match the directory structure... +starting with `App`. You can imagine that the `App\` namespace points to the +`src/` directory. Then, if you put a file in a `Controller/` sub-directory, it +needs a `Controller` part in its namespace. + +If you ever mess this up, like you typo something or forget this, you're gonna have a +bad time. PHP will not be able to find the class: you'll get a "class not found" +error. Oh, and the *other* rule is that the *name* of a file must match the class +name inside of it, plus `.php`. Hence, `VinylController.php`. We'll follow those +two rules for *all* files we create in `src/`. + +## Creating the Controller + +Back to our job of creating a controller function. Inside, add a new +public method called `homepage()`. And no, the name of this method doesn't matter +either: try naming it after your cat: it'll work! + +For now, I'm just going to put a `die()` statement with a message. + +[[[ code('66d737c13c') ]]] + +## Creating the Route + +Good start! Now that we have a controller function, let's create a route, +which defines the *URL* to our new page and *points* to this controller. There are +a few ways to create routes in Symfony, but almost everyone uses attributes. + +Here's how it works. Right above this method, say `#[]`. This is the PHP 8 attribute +syntax, which is a way to add configuration to your code. Start typing `Route`. +But before you finish that, notice that PhpStorm is auto-completing it. Hit tab +to let *it* finish. + +That, *nicely*, completed the word `Route` for me. But *more* importantly, it added +a `use` statement up on top. Whenever you use an attribute, you *must* have a +corresponding `use` statement for it at the top of the file. + +Inside `Route`, pass `/`, which will be the URL to our page. + +[[[ code('ffeff58236') ]]] + +And... done! This route defines the URL and points to this controller... simply +because it's right *above* this controller. + +Let's try it! Refresh and... congratulations! Symfony looked at the URL, saw +that it matched the route - `/` or no slash is the same for the homepage - executed +our controller and hit the `die` statement! + +Oh, and by the way, I keep saying controller function. That's commonly just called +the "controller" or the "action"... just to confuse things. + +## Returning a Response + +Ok, so inside of the controller - or action - we can write *whatever* code we want +to build the page, like make database queries, API calls, render a template, whatever. +We are going to do all of that eventually. + +The *only* thing that Symfony cares about is that your controller returns a +`Response` object. Check it out: type `return` and then start typing `Response`. +Woh: there are quite a few `Response` classes already in our code... and two +are from Symfony! We want the one from HTTP foundation. HTTP foundation is one +of those Symfony libraries... and it gives us nice classes for things like the +Request, Response and Session. Hit tab to auto-complete and finish that. + +Oh, I should have said return new response. That's better. *Now* hit tab. When +I let `Response` auto-complete the first time, *very* importantly, PhpStorm +added this use statement on top. *Every* time we reference a class or interface, +we will need to add a `use` statement to the top of the file we're working in. + +By letting PhpStorm auto-complete that for me, it added the `use` statement +automatically. I'll do that *every* time I reference a class. Oh, and if you're +still a bit new to PHP namespaces and `use` statements, check out our short and +free [PHP namespaces](https://symfonycasts.com/screencast/php-namespaces) tutorial. + +Anyways, inside of `Response`, we can put whatever we want to return to the user: +HTML, JSON or, for now, a simple message, like the title of the Mixed vinyl we're +working on: PB and jams. + +[[[ code('6750c9c02f') ]]] + +Ok team, let's see what happens! Refresh and... PB and Jams! It may not like much, +but we just built our first fully-functional Symfony page! Route + controller = +profit! + +And you've just learned the most *foundational* part of Symfony... and we're +just getting started. Oh, and because our controllers always return a `Response` +object, it's optional, but you can add a return type to this function if you want +to. But that doesn't change anything: it's just a nice way to code. + +[[[ code('32c60928e5') ]]] + +Next I'm feeling pretty confident. So let's create *another* page, but with a much +fancier route that matches a wildcard pattern. diff --git a/sfcasts/ep1/route-requirements.md b/sfcasts/ep1/route-requirements.md new file mode 100644 index 0000000..84d8866 --- /dev/null +++ b/sfcasts/ep1/route-requirements.md @@ -0,0 +1,98 @@ +# Smart Routes: GET-only & Validate {Wildcards} + +Now that we have a new page, at your terminal, run `debug:router` again. + +```terminal-silent +php bin/console debug:router +``` + +Yep, there's our new endpoint! Notice that the table has a column called "Method" +that says "any". This means that you can make a request to this URL using *any* +HTTP method - like GET or POST - and it will match that route. + +## Restricting Routes to GET or POST Only + +But the purpose of our new API endpoint is to allow users to make a GET request to +fetch song data. Technically, right now, you could also make a POST for request +to this... and it would work just fine. We might not care, but often with APIs, +you'll want to restrict an endpoint to only work with one specific method like +GET, POST or PUT. Can we make this route somehow only match GET requests? + +Yep! By adding another option to the `Route`. In this case, it's called `methods`, +it even auto-completes! Set this to an array and, put `GET`. + +[[[ code('cbd8c30440') ]]] + +I'm going to hold Command and click into the `Route` class again... so we can see +that... yup! `methods` is one of the arguments. + +Back over on `debug:router`: + +```terminal-silent +php bin/console debug:router +``` + +Nice. The route will now only match GET requests. It's... kind of hard to test this, +since a browser always makes GET requests if you go directly to a URL... but this is +where *another* `bin/console` command comes in handy: `router:match`. + +If we run this with no arguments: + +```terminal-silent +php bin/console router:match +``` + +It gives us an error but shows how it's used! Try: + +```terminal +php bin/console router:match /api/songs/11 +``` + +And... that matches our new route! But *now* ask what would happen if we made +a POST request to that URL with `--method=POST`: + +```terminal-silent +php bin/console router:match /api/songs/11 --method=POST +``` + +No routes match this path with that method! But it *does* say that it *almost* +matched our route. + +## Restricting Route Wildcards by Regex + +Let's do *one* more thing to tighten up our new endpoint. I'm going to add +an `int` type-hint to the `$id` argument. + +[[[ code('eb386d1bd6') ]]] + +That... doesn't change anything, except that PHP will now take the string `id` from +the URL that Symfony passes into this method and cast it into an `int`, which is... +just nice because then we're dealing with a true integer in our code. + +You can see the subtle difference in the response. Right now, the `id` field is +a string. When we refresh, `id` is now a true number in JSON. + +But... if somebody was being tricky... and went to `/api/songs/apple`... yikes! A +PHP error, which, on production, would be a 500 error page! I do *not* like that. + +But... what can we do? The error comes comes from when Symfony tries to call our +controller and passes in that argument. So it's not like we can put code down +*in* the controller to check if `$id` is a number: it's too late! + +So what if, instead, we could tell Symfony that this route should only *match* if +the `id` wildcard is a *number*. Is that possible? Totally! + +By default, when you have a wildcard, it matches anything. But you *can* change it +match a custom *regular expression*. Inside of the curly braces, right after the +name, add a `<` then `>` and, in between, `\d+`. That's a regular expression +meaning "a digit of anything length". + +[[[ code('b28748cdaa') ]]] + +Try it! Refresh and... yes! A 404. No route found: it simply didn't match +this route. A 404 is great... but a 500 error... that's something we want to +avoid. And if we head back to `/api/songs/5`... that still works. + +Next: if you asked me what *the* most central and important part of Symfony is, +I wouldn't hesitate: it's services. Let's find out what a service is and how it's +the key to unlocking Symfony's potential. diff --git a/sfcasts/ep1/services.md b/sfcasts/ep1/services.md new file mode 100644 index 0000000..f7b7a7d --- /dev/null +++ b/sfcasts/ep1/services.md @@ -0,0 +1,104 @@ +# Service Objects + +I see Symfony as two big parts. The first half is the route, controller, response +system. It's dead simple and well... you're already an expert on it! The second half +of Symfony is *all* about the many useful objects that are floating around... just +waiting for us to use them! + +## Hello Service Objects + +For example, when we render a template, what we are *actually* doing is taking +advantage of a Twig object and asking *it* to render a template. There's also a +logger object, a cache object, a database connection object, an object that helps +make API requests, and many, many more! And when you install a new bundle, that +give you even *more* useful objects. + +The truth is that *everything* that *Symfony* does is... actually done by +one of these useful objects. Heck there's even a router object that's responsible +for finding the matching route for the given page! + +In the Symfony world, and really the object oriented programming world in general, +these "objects that do work" have a special name: *services*. But don't let that +word confuse you. When you hear service, just think: that's an object that does +work! Like a templating object that renders a template or a database connection object +that makes queries. + +And since service objects do work, they're basically... tools that help you get +your job done! The second half of Symfony is *all* about discovering which services +are available and how to use them. + +## The debug:autowiring Command + +Let's try something. In our controller, I want to log a message... maybe some +debugging message. Since logging a message is work, it's done by a service. Does +our app already have a logger service? And if so, how do we get it? + +To find out, move over to your terminal and run another `bin/console` command: + +```terminal +php bin/console debug:autowiring +``` + +Say hello to one of the most *powerful* `bin/console` commands. I *love* this thing! +This lists *all* of the services that exist in our app. Okay, it's actually not the +*full* list, but this shows the services that you're most likely to need. +And even though our app is small, there's a lot of stuff here! There's a filesystem +service... and down here a cache service. There's even a twig service! + +Is there a service for logging? You can look in this list... or you can re-run this +command and search for the word log: + +```terminal-silent +php bin/console debug:autowiring log +``` + +Excellent! For now, ignore everything except for the first line. This line tells +us that there *is* a logger service and that this object implements an interface +called `Psr\Log\LoggerInterface`. + +## Fetching a Service via Autowiring + +Ok, so why does knowing that help us? Because if you want a service, you *ask* for +it by using the type-hint shown in this command. It's called autowiring. + +Let's try it. Head over to our controller and add a second argument. Actually, +the order of these arguments doesn't matter. What matters is that the new argument +is *type-hinted* with `LoggerInterface`. I'll hit tab to autocomplete that... so +that PhpStorm adds the use statement on top. + +In this case, the argument can be *called* anything, like `$logger`. When Symfony +sees this type-hint, it looks inside of the `debug:autowiring` list... and because +there's a match, it will pass us the logger service. + +So we now know *two* different types of arguments that we are allowed to have in +controller: you can have an argument whose *name* matches a wildcard in the route +*or* an argument whose type-hint matches one of the services in our app. + +## Using the Logger + +Ok, so now that we know Symfony will pass us the logger service object, let's use +it! I don't know, yet, what *methods* I can call on it but... if we say +`$logger->`... PhpStorm... tells us! That was easy! + +I'm going to log something at an `info()` priority level. Let's say: + +> Returning API response for song + +And then the `$id`. + +[[[ code('77f869a1d1') ]]] + +Actually, we can do something even cooler with this logger +service. Add `{song}` to the message... and add a second argument, which +is an array of extra information you want to attach to the log message. Pass `song` +set to `$id`. In a minute, you'll see that the logger will print the *actual* id +in place of `{song}`. + +[[[ code('c3b3132647') ]]] + +*Anyways*, this controller is for our API endpoint. So let's go over and refresh. +Um... ok! So no error, that's good! But did it work? Where does the logger service... +actually log to? + +Let's find out next, learn a trick to see the profiler even for API requests and +then leverage our *second* service directly. diff --git a/sfcasts/ep1/setup.md b/sfcasts/ep1/setup.md new file mode 100644 index 0000000..79a183f --- /dev/null +++ b/sfcasts/ep1/setup.md @@ -0,0 +1,144 @@ +# Hello Symfony + +Welcome. Hello. Hi, my name is Ryan and I have *the* absolute pleasure to introduce +you to the beautiful and fascinating and productive world of Symfony 6. Seriously, +I feel like Willie Wonka inviting you into my chocolate factory, except with +hopefully less sugar-related injuries. Anyways, if you're new to Symfony, I'm... +honestly a bit jealous! You're going to *love* the journey... and hopefully become +an even better developer along the way: you're *definitely* going to build some cool +stuff. + +The secret sauce of Symfony is that it starts *tiny*, which makes it easy to learn. +But then, it scales up its features automatically through a unique recipe system. +In Symfony 6, those features include new JavaScript tools and a new security system... +just to name two of the many new things. + +Symfony is also lightning fast with a huge focus on creating a joyful developer +experience, but without sacrificing programming best practices. Yea: you get to love +coding *and* love your code. I know... that sounded cheesy, but it's true. + +So come with me and you'll be in a world of pure elucidation. + +That's my first time singing in these tutorials... and maybe my last. Let's get started. + +## Installing the "symfony" Binary + +Head over to https://symfony.com/download. On this page, you'll find some +instructions - which will differ based on your operating system - on how to download +something called the Symfony binary. + +This is... not actually Symfony. It's just a command line tool that will help us +*start* new Symfony projects and give us some nice local development tools. It's +optional, but I highly recommend it! + +Once you've installed this - I already have - open up your favorite terminal app. +I'm using iTerm for mac, but it doesn't matter. If you got everything set up +correctly, you should be able to run: + +```terminal +symfony +``` + +Or even better: + +```terminal +symfony list +``` + +to see a list of all the "things" that this symfony binary can do. There's a lot +of stuff here: things that help with "local" development... and also some +optional services for deployment. We'll walk through the stuff *you* need to know +along the way. + +## Let's Start a Symfony App! + +Ok, so *we* want to start a brand new shiny Symfony app. To do *that* run: + +```terminal +symfony new mixed_vinyl +``` + +Where "mixed_vinyl" is the directory the new app will be downloaded into. That's +our top-secret project to combine the *best* part of the 90's - no, not dial-up +internet, I'm talking about mix tapes - with the auditory delight of records. More +on that later. + +Behind the scenes, this command uses *composer* - that's PHP's package manager - to +create the new project. More on *that* later. + +The end-result is that we can move into our new `mixed_vinyl` directory. Open +this folder up in your favorite editor. I'm using PhpStorm and I *highly* recommend +it. + +## Meeting our new Project + +So what did that `symfony new` command do? It bootstrapped a new Symfony project! +Ooh. And we already have a git repository. Run: + +```terminal +git status +``` + +Yep: on branch main, nothing to commit. Try: + +```terminal +git log +``` + +Cool. After downloading the new project, the command committed all of the original +files automatically... which was *very* nice of it. Though I do wish that first +commit message was a bit more rock n' roll. + +What I *really* want to show you is that our new project is super small! Try +this command: + +```terminal +git show --name-only +``` + +Yup! Our entire project is... about 17 files. And we'll learn about all of these +along the way. But I want you to feel comfortable: there's not a lot of code here. + +We're going to add features little-by-little. But if you *did* want +to start with a bigger, more fully-featured project, you can do that by running +that `symfony new` command with `--webapp`. + +***TIP +If you want a fully-dockerized new Symfony app, check out +https://github.com/dunglas/symfony-docker +*** + +## Checking System Requirements + +Before we jump into coding, let's make sure our system is ready. Run another +command from the symfony binary: + +```terminal +symfony check:req +``` + +Looks good! If your PHP install is missing any extensions... or there are +any other problems... like your computer is actually a teapot, this will let +you know. + +## Starting the Dev Web Server + +So: we have a new Symfony app over here... and our system is ready! All we need now +is a subwoofer. I mean web server! You *can* set up a real web server like Nginx or +something trendy like Caddy. But for local development, the Symfony binary can help +us. Run: + +```terminal +symfony serve -d +``` + +And... we have a web server running! Come back! + +The first time you run this, it might ask you to run a different command to set +up an SSL certificate, which is nice because then the server supports https. + +Moment of truth! Copy the URL, spin over to your browser, hold your breath and woo! +Hello Symfony 6 welcome page... complete with fancy color changes whenever we reload. + +Next: let's meet - and become friends with - the code inside our app, so we can +demystify what each part does. Then we'll code. diff --git a/sfcasts/ep1/stimulus-example.md b/sfcasts/ep1/stimulus-example.md new file mode 100644 index 0000000..dc88fb9 --- /dev/null +++ b/sfcasts/ep1/stimulus-example.md @@ -0,0 +1,147 @@ +# Real-World Stimulus Example + +Let's put Stimulus to the test. Here's our goal: when we click the play icon, we're +going to make an Ajax request to our API endpoint... the one in `SongController`. +This returns the URL to where this song can be played. We'll then use that in +JavaScript to... play the song! + +Take `hello_controller.js` and rename it to, how about `song-controls_controller.js`. +Inside, just to see if this is working, in `connect()`, log a message. The +`connect()` method is called whenever Stimulus sees a new matching element on +the page. + +[[[ code('e29d1ac07a') ]]] + +Now, over in the template, hello isn't going to work anymore, so remove that. What +I want to do is surround each song *row* with this controller.... so that's this +`song-list` element. After the class, add `{{ stimulus_controller('song-controls') }}`. + +[[[ code('bd43e92632') ]]] + +Let's try that! Refresh, check the console and... yes! It hit our code six times! +Once for *each* of these elements. And each element gets its own, separate controller +*instance*. + +## Adding Stimulus Actions + +Okay, next, when we click play, we want to run some code. To do that, we can add +an *action*. It looks like this: on the `a` tag, add `{{ stimulus_action() }}` - +another shortcut function - and pass this the controller name that you're +attaching the action to - `song-controls` - and then a method inside of that +controller that should be called when someone clicks this element. How about `play`. + +[[[ code('69caff7d11') ]]] + +Cool huh? Back in song controller, we don't need the `connect()` method anymore: +we don't need to *do* anything each time we notice another `song-list` row. But +we *do* need a `play()` method. + +And like with normal event listeners, this will receive an `event` object... and +then we can say `event.preventDefault()` so that our browser doesn't try to follow +the link click. To test, `console.log('Playing!')`. + +[[[ code('80418b94bb') ]]] + +Let's go see what happens! Refresh and... click. It's working. It's that easy to +hook up an event listener in Stimulus. Oh, and if you inspect this element... that +`stimulus_action()` function is just a shortcut to add a special `data-action` +attribute that Stimulus understands. + +## Installing and Importing Axios + +Ok, how can we make an Ajax call from inside of the `play()` method? Well, we +could use the built-in `fetch()` function from JavaScript. But instead, I'm going to +install a third-party library called Axios. At your terminal, install it by saying: + +```terminal +yarn add axios --dev +``` + +We now know what this does: it downloads this package into our `node_modules` +directory, and adds this line to our `package.json` file. + +Oh, and side note: you absolutely *can* use jQuery inside of Stimulus. I won't +do it, but it works great - and you can install - and import - jQuery like any other +package. We talk about that in our Stimulus tutorial. + +Ok, so how do we *use* the `axios` library? By importing it! + +At the top of this file, we're already importing the `Controller` base class from +`stimulus`. Now `import axios from 'axios'`. As soon as we do that, Webpack Encore +will grab the `axios` source code and *include* it in our built JavaScript files. + +[[[ code('f334d5443b') ]]] + +Now, down here, we can say `axios.get()` to make a GET request. But... what should +we pass for the URL? It needs to be something like `/api/songs/5`... but how do we +know what the "id" is for *this* row? + +## Stimulus Values + +One of the coolest things about Stimulus is that it allows you to pass values from +Twig *into* your Stimulus controller. To do that, declare which values you want to +*allow* to passed in via a special static property: `static values = {}`. Inside, +let's allow an `infoUrl` value to be passed. I totally just made up that name: I'm +thinking we'll pass in the *full* URL to the API endpoint. Set this to the *type* +that this will be. So, a `String`. + +We'll learn *how* we pass this value from Twig into our controller in a minute. But +because we have this, below, we can reference the value by saying `this.infoUrlValue`. + +[[[ code('0684edc5d4') ]]] + +So how do we pass that in? Back in `homepage.html.twig`, add a second +argument to `stimulus_controller()`. This is an array of the *values* you want +to pass into the controller. Pass `infoUrl` set to the URL. + +Hmm, but we need to generate that URL. Does that route have a name yet? Nope! +Add `name: 'api_songs_get_one'`. + +[[[ code('e20f187c7b') ]]] + +Perfect. Copy that... and back in the template, set `infoURl` to `path()`, the +name of the route... and then an array with any wildcards. Our route has an +`id` wildcard. + +In a real app, these tracks would probably each have a database id that we could +pass. We don't have that yet... so to, kind of, fake this, I'm going to use +`loop.index`. This is a magic Twig variable: if you're inside of a Twig `for` +loop, you can access the *index* - like 1, 2, 3, 4 - by using `loop.index`. So +we're going to use this as a fake ID. Oh, and don't forget to say `id:` then +`loop.index`. + +[[[ code('f3a0755cb5') ]]] + +Testing time! Refresh. The first thing I want you to see is that, when we pass +`infoUrl` as the second argument to `stimulus_controller`, all that really does is +output a very special `data` attribute that Stimulus knows how to read. That's how +you pass a value into a controller. + +Click one of the play links and... got it. Every controller object is passed its +correct URL! + +## Making the Ajax Call + +Let's celebrate by making the Ajax call! Do it with `axios.get(this.infoUrlValue)` - +yes, I just typo'ed that, `.then()` and a callback using an arrow function that +will receive a `response` argument. This will be called when the Ajax call +finishes. Log the response to start. Oh, and fix to use `this.infoUrlValue`. + +[[[ code('de15183eff') ]]] + +Alrighty, refresh... then click a play link! Yes! It dumped the response... and +one of its keys is `data`... which contains the `url`! + +Time for our victory lap! Back in the function, we can *play* that audio by creating +a new `Audio` object - this is just a normal JavaScript object - passing it +`response.data.url`... and then calling `play()` on this. + +[[[ code('a92c517158') ]]] + +And now... when we hit play... finally! Music to my ears. + +If you want to learn more about Stimulus - this *was* a bit fast - we have an entire +tutorial about it... and it's *great*. + +To finish off *this* tutorial, let's install one more JavaScript library. This one +will instantly make our app feel like a single page app. That's next. diff --git a/sfcasts/ep1/stimulus.md b/sfcasts/ep1/stimulus.md new file mode 100644 index 0000000..d3373f5 --- /dev/null +++ b/sfcasts/ep1/stimulus.md @@ -0,0 +1,82 @@ +# Stimulus: Sensible, Beautiful JavaScript + +I want to talk about Stimulus. Stimulus is a small, but delightful JavaScript library +that I *love*. And Symfony has first-class support for it. It's also heavily used +by the Ruby on Rails community. + +## SPA vs "Traditional" Apps + +So there are kind of two philosophies in web development. The first is where you +return HTML from your site like we've been doing on our homepage and browse page. +And then you *add* JavaScript behavior to that HTML. The second philosophy is to +use a front-end JavaScript framework to build *all* of your HTML and JavaScript. +That's a single page application. + +The right solution depends on your app, but I strongly like the first approach. +And by using Stimulus - as well as another tool we'll talk about in a few minutes +called Turbo - we can create highly-interactive apps that look and feel *as* +responsive as a single page app. + +We have an entire tutorial on Stimulus, but let's get a taste. You can already +see how it works in the example on their docs. You create a small JavaScript class +called a controller... and then *attach* that controller to one or more elements +on the page. And that's it! Stimulus allows you to attach event listeners - +like click events - and has other goodies. + +## Stimulus Controllers in our App + +In our app, when we installed Encore, it gave us a `controllers/` directory. This +is where our Stimulus controllers will live. And in `app.js`, we import +`bootstrap.js`. This is not a file that you'll need to look at much, but it's +*super* useful. This starts up Stimulus - yes, it's already installed - and +registers everything in the `controllers/` directory as a Stimulus controller. +This means that if you want to create a new Stimulus controller, all you need to +do is add a file to this `controllers/` directory! + +And we get one Stimulus controller out-of-the box called `hello_controller.js`. All +Stimulus controllers follow the naming practice of something "underscore" +`controller.js` or something *dash* `controller.js`. The part before `_controller` - +so `hello` - becomes the controller's *name*. + +## Attaching a Controller to an Element + +Let's attach this to an element. Open up `templates/vinyl/homepage.html.twig`. +Let's see... on the main part of the page, I'm going to add a div... and +then to attach the controller to this element, add `data-controller="hello"`. + +[[[ code('a818d762b0') ]]] + +Let's try it! Refresh and... yes! It worked! Stimulus saw this element, instantiated +the controller... and then our code changed the content of the element. The +element that this controller is attached to is available as `this.element`. + +## Stimulus Dynamically sees New Elements! + +So... this is already *really* nice... because we get to work inside of a neat +JavaScript object... which is tied to a specific element. + +But let me show you the *coolest* part of Stimulus: what makes it such a game changer. +Inspect element in your browser tools near the element. I'm going to modify the parent +element's HTML. Right above this - though it doesn't matter where - add *another* +element with `data-controller="hello"`. + +And... boom! We see the message! *This* is the killer feature of Stimulus: you can +add these `data-controller` elements onto the page *whenever* you want. For example, +if you make an Ajax call... which adds fresh HTML to your page, Stimulus will +*notice* that and execute any controllers that the new HTML should be attached to. +If you've ever had problems where you add HTML to your page via Ajax... but that +new HTML's JavaScript is broken because it's missing some event listeners, well, +Stimulus just solved that. + +## The stimulus_controller () Function + +When you use Stimulus inside of Symfony, we get a few helper functions to make +life easier. So instead of writing `data-controller="hello"` by hand, we can say +`{{ stimulus_controller('hello') }}`. + +[[[ code('108098d4bf') ]]] + +But that's just a shortcut to render that attribute *exactly* like it was before. + +Ok, now that we have the basics of Stimulus, let's use it to do something real, +like make an Ajax request when we click this play icon. That's next. diff --git a/sfcasts/ep1/turbo.md b/sfcasts/ep1/turbo.md new file mode 100644 index 0000000..f592b2d --- /dev/null +++ b/sfcasts/ep1/turbo.md @@ -0,0 +1,125 @@ +# Turbo: Supercharge your App + +Welcome to the *final* chapter of our intro to Symfony 6 tutorial. If you're watching +this, you're crushing it! And it's time to celebrate by installing one more package +from Symfony. But before we do, as you know, I like to commit everything first... +in case the new package installs an interesting recipe: + +```terminal-silent +git add . +git commit -m "Never gonna let you go..." +``` + +## Installing symfony/ux-turbo + +Ok, let's install the new package: + +```terminal +composer require symfony/ux-turbo +``` + +See that "ux" in the package name? Symfony UX is a set of libraries that add +*JavaScript* functionality to your app... often with some PHP code to help. For +example, there's a library for rendering charts... and another for using an image +Cropper with the form system. + +## Symfony UX Recipes + +So, as you can see, this *did* install a recipe. OoOOo. Run + +```terminal +git status +``` + +so we can see what that did. Most of this is normal, like `config/bundles.php` +means it enabled the new bundle. The two interesting changes are +`assets/controllers.json` and `package.json`. Let's check out `package.json` first. + +When you install a UX package, what that *usually* means is that you're integrating +with a third-party JavaScript library. And so, that package's recipe *adds* that +library to your code. In this case, the JavaScript library we're integrating with +is called `@hotwired/turbo`. Also, the `symfony/ux-turbo` PHP package *itself* comes +with some extra JavaScript. This special line says: + +> Hey Node! I want to include a package called `@symfony/ux-turbo`... but instead +> of downloading that, you can just find its code in the +> `vendor/symfony/ux-turbo/Resources/assets` directory. + +You can literally look at that path: `vendor/symfony/ux-turbo/Resources/assets` +to find a mini JavaScript package. Now, because this updated our `package.json` +file, we need to re-install our dependencies to download and get this all set +up. + +In fact, find your terminal that's running `yarn watch`. We've got an error! +It says the file `@symfony/ux-turbo/package.json` cannot be found, try running +`yarn install --force`. + +Let's do that! Hit control+C to stop this... and then run + +```terminal +yarn install --force +``` + +or `npm install --force`. Then, restart Encore with: + +```terminal +yarn watch +``` + +The *other* file the recipe modified was `assets/controllers.json`. Let's go take a +look at that: `assets/controllers.json`. This is another thing that's unique to +Symfony UX. Normally, if we want to add a stimulus controller, we put it into the +`controllers/` directory. But sometimes, we might install a PHP package and *that* +may want to add its *own* Stimulus controller into our app. This syntax basically +says: + +> Hey Stimulus! Go load this Stimulus controller from that new +> `@symfony/ux-turbo` package. + +Now this *particular* Stimulus controller is a little weird. It's not one that +we're going to use directly inside of the `stimulus_controller()` Twig function. +This is, kind of a, fake controller. What does it do? *Just* by it being loaded, +it's going to activate the Turbo library. + +## Hello Turbo! By Full-Page Refreshes + +So I keep talking about Turbo. What *is* Turbo? Well, by running that composer +require command... then reinstalling yarn, the Turbo JavaScript *is* now active and +running on our site. What does it *do*? It's simple: it turns every link click +and form submit on our site into an Ajax call. And *that* makes our site feel +lightning fast. + +Check it out. Do one last full refresh. And then watch... if I click Browse, there +was no full page refresh! If I click these icons, no refresh! Turbo intercepts +those clicks, makes an Ajax call to the URL, and then puts that HTML onto our site. +This is *huge* because, for free, our app suddenly looks and feels like a single +page app... without us doing anything! + +## The Web Debug Toolbar & Profiler for Ajax Requests + +Now, one other cool thing you'll notice is that even though full page reloads +are gone, these Ajax calls *do* show up on the web debug toolbar. And you can click +to go see the profiler for that Ajax call really easily. This Ajax part of the web +debug toolbar is even more useful with Ajax calls for an API endpoint. If we hit +the play icon... that 7 just went up to 8... and here's the profiler for that API +request! I'll open that link in a new window. That's a *super* easy way to get +to the profiler for *any* Ajax request. + +So Turbo is amazing... and it can do more. There *are* some things you need +to know about it before shipping it to production, and if you're interested, yup! +We have a full tutorial about Turbo. I wanted to mention it in *this* tutorial +because Turbo is easiest if you add it to your app early on. + +All right, congratulations! The first Symfony 6 tutorial is in the books! Pat yourself +on the back... or better, find a friend and give them a crisp high five. + +And keep going! Join us for the next tutorial in this series, which will take you +from a budding Symfony developer to someone who *really* understands what's going +on. How services work, the point of all of these configuration files, Symfony +environments, environmental variables, and a lot more. Basically everything you'll +need to do *whatever* you want with Symfony. + +And if you have any questions or ideas, we are here for you down in the comments +section below the video. + +Alright friends, see you next time! diff --git a/sfcasts/ep1/twig-inheritance.md b/sfcasts/ep1/twig-inheritance.md new file mode 100644 index 0000000..a1a1004 --- /dev/null +++ b/sfcasts/ep1/twig-inheritance.md @@ -0,0 +1,152 @@ +# Twig Inheritance + +Head to https://twig.symfony.com... and then click to check its documentation. There's +lots of good stuff here. But what I want you to do is scroll down to the Twig +reference. Yea! + +## Tags + +The first things to look at, on the left, are these things called tags. This +list represents *every* possible thing you can use with the do something syntax. Yup, +it will always be `{%` and then *one* of these things, like `for` or `if`. And +honestly, you're only going to use about 5 of these on an everyday basis. If +you want to know the syntax for one of these, just click to see its docs. + +## Filters + +In addition to the 20 tags, Twig also has something called *filters*. These are sweet. +Filters are basically functions, but with a more hipster syntax. Twig *does* also +have functions, but there are fewer: Twig really prefers filters: they're way +cooler! + +For example, there's a filter called `upper`. Using a filter is like using the +`|` key on the command line. You have some value - then you "pipe it into" +the filter you want, like `upper`. + +Let's try this! Print `track.artist|upper`. + +[[[ code('73974377cd') ]]] + +And now... it's uppercase! If you want to confuse your coworkers, you can pipe +*that* to `lower`... which sends things *back* to lowercase. There's no *real* +reason to do this, but filters *can* be chained like this. + +[[[ code('016bee2e4d') ]]] + +Anyways, check out the filters list because there's probably something you'll +find useful. + +And... that's pretty much it! Beyond functions, there's also something called +"tests", which are handy in if statements: you can say things like "if number is +divisibleby 5". + +## Template Inheritance + +Ok, just *one* more thing to learn about Twig, and it's cool. + +View the HTML source of the page. Notice that there is *no* HTML structure: there's +no `html`, `head` or `body` tags. Literally the HTML that we have inside of our +template, is what we get. Nothing more. + +So is there... some sort of layout system in Twig where we can add a base layout +around us? Absolutely. And it's incredible. It's called template inheritance. If +you have a template and you want that to use some base layout, at the very top of +the file, use a "do something" tag called `extends`. Pass this the name of the +layout file: `base.html.twig`. + +[[[ code('48dc6d8fec') ]]] + +That's referring to this template right here. Before we check that out, if we +try this now, yikes! Big error: + +> A template that extends another cannot include content outside Twig blocks. + +To figure out what that means, open `base.html.twig`. This is your base layout file... +and you're *totally* free to customize it however you want. Right now... it's +mostly just boring HTML tags... except for a number of these "blocks". + +Blocks are basically "holes" into which a child template can place content. Let +me explain that in a different way. When we say `extends 'base.html.twig'`, that +basically says: + +> Yo Twig! When you render this template, I want you to *actually* render +> `base.html.twig` ... and then put *my* content *inside* of it. + +Twig then politely replies: + +> Ok cool... I can do that. But *where* in `base.html.twig` do you want me to put +> all of your content? Do you want me to put it at the bottom of the page? At the +> top? Some random place in the middle? + +The way we tell Twig *where* to put our content within `base.html.twig` is by +override a block. Notice that `base.html.twig` already has a block called `body`... +and that's *right* where we want to put our template's HTML. + +To put it there, in our template, surround all of the content with +`{% block body %}`... and then `{% endblock %}`. + +[[[ code('01d2fbf6f5') ]]] + +This is called template inheritance because we are *overriding* that `body` block +with this new content. So now, when Twig renders `base.html.twig`... and it gets to +this `block body` part, it's going to print the `block body` HTML from *our* template + +Watch: refresh and... the error is gone. And if you view the page source, we have a +full HTML page! + +## Block Names + +Oh, and the *names* of these blocks are *not* important. If you want to rename them +after your favorite 90's sitcom character, do it. Just remember to *also* update +its name in any child templates. + +You can also add *more* blocks. Every block you add is just another potential +override point. + +## Default Block Content + +Oh, and you may have noticed that blocks *can* have default content. Look +at the page right now: the title says "Welcome". That's because the `title` block +has default content... and we're not overriding it. Let's change the default title +to "Mixed Vinyl". + +[[[ code('086e2afb43') ]]] + +So now *that* will be the title of every page on our site... *unless* we override +that. In our template, either above block body or below - the order of blocks +doesn't matter - add `{% block title %}`, `{% endblock %}` and, in between +"Create a new Record". + +[[[ code('4ebc00a77a') ]]] + +And now... yes! *This* page has a custom title. + +## Adding to (Instead of Replacing) the Parent Block + +Oh, and you might be wondering: + +> What if I don't want to *replace* a block entirely.... but instead, I want to +> *add* to a block? + +That's totally possible. In `base.html.twig`, the `title` block is set to "Mixed +Vinyl". If we wanted to *prepend* our custom title to that, we could say "Create +a new Record" then use the "say something" tag to print a function called `parent()`. + +[[[ code('9729cce31f') ]]] + +That does exactly what you'd expect: it finds the parent template's content for +this block.. and prints it. Refresh and... that's *so* nice. + +## Template Inheritance is Class Inheritance + +If you're ever confused about how template inheritance works, it's useful, for me +at least, to think about it *exactly* like object-oriented inheritance. Each +template is like a class and each block is like a method. So the homepage "class" +extends the `base.html.twig` "class", but overrides two of its methods. If that +only confused you, don't worry about it. + +So... that's it for Twig. You're basically a Twig expert, which I'm told is a +popular topic at parties. + +Next: one of the *killer* features of Symfony is its debugging tools. Let's get +these installed and check 'em out. diff --git a/sfcasts/ep1/twig-service.md b/sfcasts/ep1/twig-service.md new file mode 100644 index 0000000..710aa0a --- /dev/null +++ b/sfcasts/ep1/twig-service.md @@ -0,0 +1,94 @@ +# The Twig Service & Profiler for API Requests + +Since this page just loaded without an error, we *think* that we just successfully +logged a message via the logger service. But... where do log messages go? How +can we check? + +The logger service is provided by a library that we installed earlier called monolog. +It was part of the debug-pack. And you can control its configuration inside the +`config/packages/monolog.yaml` file, including *where* log messages are logged to, +like which file. We'll focus more on config in the next tutorial. + +## The Profiler for API Requests + +But one way that you can *always* see the log messages for a request is via the +profiler! This is super handy. Go to the homepage, click any link on the web debug +toolbar... and then go to the Logs section. We're now seeing *all* the log messages +that were made only during that *last* request to the homepage. + +Great! Except that... our log message is made on an API endpoint... and API +endpoints don't have a web debug toolbar we can click! Are we stuck? Nope! +Refresh this page one more time... then manually go to `/_profiler`. This is... kind +of a secret door into the profiler system... and this page shows the last *ten* +requests made into our system. The second to the top is the API request we just +made. Click the little token link to see... yea! We're looking at the profiler for +that API request! Over in the Logs section... there it is! + +> Returning API response for song 5 + +... and you can even see the extra info we passed. + +## Rendering a Twig Template Manually + +Ok, services are *so* important that... I want to do one more quick example. +Go back to `VinylController`. The `render()` method is *really* just a shortcut to +fetch the "twig" service, call some method on that object to render the template... +and then put the final HTML string into a `Response` object. It's a *great* shortcut +and you *should* use it. + +But! As a challenge, could we render a template *without* using that method? +Of course! Let's do it. + +Step one: find the service that does the work you need to do. So, we need to find +the Twig service. Let's do our trick again: + +```terminal +php bin/console debug:autowiring twig +``` + +And... yes! Apparently the type-hint we need to use is `Twig\Environment`. + +Ok! Go back to our method, add an argument, type `Environment`, and hit tab to +auto-complete that so PhpStorm adds the `use` statement. Let's call it `$twig`. + +Below, instead of using `render`, let's say `$html =` and then `$twig->`. Like +with the logger, we don't need to know what methods this class has, because, thanks +to the type-hint, PhpStorm can *tell* us all the methods. That `render()` method +looks like it's probably what we want. The first argument is the string name of the +template to render and the `$context` argument holds the variables. So... it has +the same arguments that we were already passing. + +To see if this is working, `dd($html)`. + +[[[ code('9dd37843fc') ]]] + +Testing time! Head to the homepage... and yes! We just rendered a template manually! +Seriously awesome! And we can finish this page by wrapping that in a response: +`return new Response($html)`. + +[[[ code('a862fa6787') ]]] + +And now... the page works! And we understand that the *true* way to render a template +is via the Twig service. Someday, you'll find yourself in a situation where you +need to render a template but you are *not* in a controller... and so you do +*not* have the `$this->render()` shortcut method. Knowing that there's a Twig +service you can fetch will be the *key* to solving that problem. More on that in +the next tutorial. + +But in a real app, in a controller, there's no reason to do all this extra work. +So I'm going to revert this... and go back to using `render()`. And... then we +don't need to autowire that argument anymore... and we can even clean up the +`use` statement. + +Here are the three big, gigantic, important takeaways. First, Symfony is *packed* +full of objects that do work... which we call services. Services are tools. Second, +*all* work in Symfony is done by a service... even things like routing. And third, +*we* can use services to help us get *our* work done by autowiring them. + +In the next tutorial in this series, we'll dive deeper into this very important +concept. + +But before we finish *this* tutorial, I *really* really want to talk about one more +big awesome, amazing thing: Webpack Encore, the *key* to writing professional CSS +and JavaScript. Over these last few chapters, we're going to bring our site to life +and even make it as responsive as a single page application. diff --git a/sfcasts/ep1/twig.md b/sfcasts/ep1/twig.md new file mode 100644 index 0000000..2b217a9 --- /dev/null +++ b/sfcasts/ep1/twig.md @@ -0,0 +1,108 @@ +# Twig ❤️ + +Symfony controller classes do *not* need to extend a base class. As long as your +controller function returns a `Response` object, Symfony doesn't care *what* your +controller looks like. But usually, you *will* extend a class called +`AbstractController`. + +Why? Because it gives us shortcut methods. + +## Rendering a Template + +And the *first* shortcut is `render()`: the method for rendering a +template. So return `$this->render()` and pass it two things. The first +is the name of the template. How about `vinyl/homepage.html.twig`. + +It's not required, but it's common to have a directory with the same +name as your controller class and filename that's the same as your method, +but you can do whatever. The second argument is an array of any variables +that you want to pass *into* the template. Let's pass in a variable called +`title` and set it to our mix tape title: "PB and Jams". + +[[[ code('28e791440a') ]]] + +Done in here. Oh, but pop quiz! What do you think the `render()` method returns? +Yea, it's the thing I *keep* repeating: a controller must always return a `Response` +object. `render()` is just a shortcut to render a template, get that string and put +it into a `Response` object. `render()` returns a `Response`. + +## Creating the Template + +We know from earlier that when you render a template, Twig looks in the `templates/` +directory. So create a new `vinyl/` sub-directory... and inside of that, a file +called `homepage.html.twig`. To start, add an `h1` and then print the `title` variable +with a special Twig syntax: `{{ title }}`. And... I'll add some hardcoded TODO text. + +[[[ code('285607e2a3') ]]] + +Let's... go see if this works! We were working on our homepage, so go there and... +hello Twig! + +## Twigs 3 Syntax + +Twig is one of the *nicest* parts of Symfony, and also one of the easiest. We're +going to go through everything you need to know... in basically the next ten minutes. + +Twig has exactly *three* different syntaxes. If you need to print something, +use `{{`. I call this the "say something" syntax. If I say `{{ saySomething }}` +that would print a variable called `saySomething`. Once you're *inside* Twig, +it looks a *lot* like JavaScript. For example, if I surround this in quotes, now +I'm printing the *string* `saySomething`. Twig has functions... so that +would call the function and print the result. + +So syntax #1 - the "say something" syntax - is `{{` + +The second syntax... doesn't really count. It's `{#` to create a comment... and +that's it. + +[[[ code('285607e2a3') ]]] + +The *third* and final syntax I call the "do something" syntax. This is when you're +not *printing*, your *doing* something in the language. Examples of "doing something" +would be if statements, for loops or setting variables. + +## The for Loop + +Let's try a `for` loop. Go back to the controller. I'm going to paste in +a tracks list... and then pass a `tracks` variable into the template set to that array. + +[[[ code('8123e02600') ]]] + +Now, unlike `title`, tracks is an array... so we can't just print it. But, we +can try! Ha! That gives us an array to string conversion. Nope, we need to *loop* +over tracks. + +Add a header and a `ul`. To loop, we'll use the "do something" syntax, which is +`{%` and then the *thing* that you want to do, like `for`, `if` or `set`. I'll show +you the full list of do something tags in a minute. A for loop looks like this: +`for track in tracks`, where tracks is the variable we're looping over and `track` +will be the variable *inside* the loop. + +After this, add `{% endfor %}`: most "do something" tags have an end tag. *Inside* +the loop, add an `li` and then use the say something syntax to print `track`. + +[[[ code('dc265ad487') ]]] + +## Using Sub.keys + +When we try it... nice! Oh, but let's get *trickier*. Back in the controller, +instead of using a simple array, I'll restructure this to make each track an +associative array with `song` and `artist` keys. I'll paste in that same change +for the rest. + +[[[ code('647cdcfd7e') ]]] + +What happens if we try it? Ah, we're back to the "array to string" conversion. +When we loop, each track *itself* is now an array. How can we read the `song` +and `artist` keys? + +Remember when I said that Twig looks a lot like JavaScript? Well then, it shouldn't +be a surprise that the answer is `track.song` and `track.artist`. + +[[[ code('fc54d0388b') ]]] + +And... *that* gets our list working. + +Now that we have the basics of Twig down, next, let's look at the *full* list +of "do something" tags, learn about Twig "filters" *and* tackle the all-important +template inheritance system. diff --git a/sfcasts/ep1/webpack-encore-usage.md b/sfcasts/ep1/webpack-encore-usage.md new file mode 100644 index 0000000..2d6604f --- /dev/null +++ b/sfcasts/ep1/webpack-encore-usage.md @@ -0,0 +1,107 @@ +# Packaging JS and CSS with Encore + +When we installed Webpack Encore, its recipe gave us this new `assets/` directory. +Check out the `app.js` file. Interesting. Notice how it *imports* this `bootstrap` +file. That's actually `bootstrap.js`: this file right here. The `.js` extension +is optional. + +## JavaScript Imports + +This is one of the most *important* things that Webpack gives us: the ability to +*import* one JavaScript file from another. We can import functions, objects... +really *anything* from another file. We're going to talk more about this +`bootstrap.js` file in a little bit. + +This also imports a CSS file!? If you haven't seen this before, it might look... +weird: JavaScript importing CSS? + +To see how this all works, in `app.js`, add a `console.log()`. + +[[[ code('37b4bd7997') ]]] + +And `app.css` already has a body background... but add an `!important` so that +we can *definitely* see if this is being loaded. + +[[[ code('28ba4bdd4d') ]]] + +Ok... so who *reads* these files? Because... they don't live in the `public/` +directory... so we can't create `script` or `link` tags that point directly to them. + +## webpack.config.js + +To answer that, open `webpack.config.js`. Webpack Encore is an executable binary: +we're going to run it in a minute. When we *do*, it will load this file to get +its config. + +And while there are a *lot* of features inside of Webpack, the only thing we need +to focus on right now is this one: `addEntry()`. This `app` could be anything... +like `dinosaur`, it doesn't matter. I'll show you how that's used in a minute. The +important thing is that it points to the `assets/app.js` file. Because of this, +`app.js` will be the first and *only* file that Webpack will parse. + +It's pretty cool: Webpack will reads the `app.js` file and then follow *all* of the +`import` statements *recursively* until it finally has a giant collection of *all* +the JavaScript and CSS our app needs. Then, it will *write* that into the `public/` +directory. + +## Running Webpack Encore + +Let's see it in action. Find your terminal and run: + +```terminal +yarn watch +``` + +This is, as it says, a shortcut for running `encore dev --watch`. If you look +at your `package.json` file, it came with a `script` section with some +shortcuts. + +Anyways, `yarn watch` does two things. First, it created a new `public/build/` +directory and, inside, `app.css` and `app.js` files! But don't let the names +fool you: `app.js` contains a lot more that *just* what's inside `assets/app.js`: +it contains *all* the JavaScript from *all* the imports it finds. `app.css` +contains all the *CSS* from all the imports. + +The reason these files are called `app.css` and `app.js` is because of the entry +name. + +So the takeaway is that, thanks to Encore, we suddenly have new files in a +`public/build/` directory that contain *all* the JavaScript and CSS our app needs! + +## The Encore Twig Functions + +And if you move over to your homepage and refresh... woh! It instantly worked!? The +background changed... and in my inspector... there's the console log! How the heck +did that happen? + +Open your base layout: `templates/base.html.twig`. The secret is in the +`encore_entry_link_tags()` and `encore_entry_script_tags()` functions. I bet you +can guess what these do: add the `link` tag to `build/app.css` and the `script` +tag to `build/app.js`. + +You can see this in your browser. View the source for the page and... yup! The link +tag for `/build/app.css`... and `script` tag for `/build/app.js`. Oh, but it also +rendered two *other* `script` tags. That's because Webpack is really smart. For +performance purposes, instead of dumping one *gigantic* `app.js` file, sometimes +Webpack will *split* it into multiple, smaller files. Fortunately, these Encore +Twig functions are smart enough to handle that: it will include *all* the link +or script tags needed. + +The *most* important thing is that the code that we have in our `assets/app.js` +file - including anything it imports - is now functioning and showing up on our +page! + +## Watching for Changes + +Oh, and because we ran `yarn watch`, Encore is still running in the background +watching for changes. Check it out: go into `app.css`... and change the background +color. Save, move over and refresh. + +[[[ code('9947a18198') ]]] + +It instantly updated! That's because Encore *noticed* the change and recompiled +the built file really quickly. + +Next: let's move our *existing* CSS into the new system and learn how we can +install and import *third party* libraries - look Bootstrap or FontAwesome - right +into our Encore setup. diff --git a/sfcasts/ep1/webpack-encore.md b/sfcasts/ep1/webpack-encore.md new file mode 100644 index 0000000..489cf00 --- /dev/null +++ b/sfcasts/ep1/webpack-encore.md @@ -0,0 +1,139 @@ +# Setting up Webpack Encore + +Our CSS setup is fine. We put files into the `public/` directory and then... +we point to them from inside our templates. We could add JavaScript files the +same way. + +But if we want get truly serious about writing CSS and JavaScript, we need to take +this to the next level. And even if you consider yourself a mostly backend developer, +the tools we're about to talk about will allow you to write CSS and JavaScript that +feels easier and is less error-prone than what you're probably used to. + +The key to taking our setup to the next level is leveraging a node library called +Webpack. Webpack is the industry standard tool for packaging, minifying and parsing +your frontend CSS, JavaScript, and other files. But don't worry: Node is just +JavaScript. And its role in our app will be pretty limited. + +Setting up Webpack *can* be tricky. And so, in the Symfony world, we use a lightweight +tool called Webpack Encore. It's still Webpack... it just makes it easier! And we +have a free tutorial about it if you want to dive deeper. + +## Installing Encore + +But let's do a crash course right now. First, at your command line, make sure you +have Node installed: + +```terminal +node -v +``` + +You'll also need either `npm` - which comes with Node automatically - or `yarn`: + +```terminal-silent +yarn --version +``` + +Npm and yarn are Node package managers: they're the Composer for the Node world... +and you can use either. If you decide to use yarn - thats what I'll use - make sure +to install version 1. + +We're about to install a new package... so let's commit everything: + +```terminal +git add . +``` + +And... looks good: + +```terminal-silent +git status +``` + +So commit everything: + +```terminal-silent +git commit -m "Look mom! A real app" +``` + +To install Encore, run: + +```terminal +composer require encore +``` + +This installs WebpackEncoreBundle. Remember, a bundle is a Symfony plugin. +And this package has a recipe: a very important recipe. Run: + +```terminal +git status +``` + +## The Encore Recipe + +Ooh! For the first time, the recipe modified the `.gitignore` file. Let's go check +that out. Open `.gitignore`. The stuff on top is what we originally had... and +down here is the new stuff added by WebpackEncoreBundle. It's ignoring the +`node_modules/` directory, which is basically the `vendor/` directory for Node. +We don't need to commit that because those vendor libraries are described in +another new file from the recipe: `package.json`. This is Node's `composer.json` +file: it describes the Node packages that our app needs. The most important one +is Webpack Encore itself, which *is* a Node library. It also has a few other +package that will help us get our job done. + +The recipe also added an `assets/` directory... and a configuration file to control +Webpack: `webpack.config.js`. The `assets/` directory already holds a small set of +files to get us started. + +## Installing Node Dependencies + +Ok, with Composer, if we didn't have this `vendor/` directory, we could run +`composer install` which would tell it to read the `composer.json` file and +re-download all the packages into `vendor/`. The same thing happens with Node: we +have a `package.json` file. To *download* this stuff, run: + +```terminal +yarn install +``` + +Or: + +```terminal skip-ci +npm install +``` + +Go node go! This will take a few moments as it downloads everything. You'll probably +get a few warnings like this, which are safe to ignore. + +Great! This did two things. First, it downloaded a *bunch* of files into the +`node_modules/` directory: the "vendor" directory for Node. It also created a +`yarn.lock` file... or `package-lock.json` if you're using npm. This serves the +same purpose of `composer.lock`: it stores the *exact* versions of all the packages +so that you get the *same* versions next time you install your dependencies. + +For the most part, you don't need to worry about these lock files... except that +you *should* commit them. Let's do that. Run: + +```terminal +git status +``` + +Then: + +```terminal +git add . +``` + +Beautiful: + +```terminal-silent +git status +``` + +And commit: + +```terminal-silent +git commit -m "Adding Webpack Encore" +``` + +Hey! Webpack Encore is now installed! But... it's not doing anything yet! Freeloader. +Next, let's use it to take our JavaScript up to the next level. diff --git a/sfcasts/ep1/wildcard-route.md b/sfcasts/ep1/wildcard-route.md new file mode 100644 index 0000000..836409b --- /dev/null +++ b/sfcasts/ep1/wildcard-route.md @@ -0,0 +1,135 @@ +# Wildcard Routes + +The homepage will eventually be the place where a user can design and build their +next sweet mix tape. But in addition to *creating* new tapes, users will also be able +to browse *other* people's creations. + +## Creating a Second Page + +Let's make a second page for that. How? By adding a second controller: public +function, how about `browse`: the name doesn't really matter. And to be responsible, +I'll add a `Response` return type. + +Above this, we need our route. This will look exactly the same, except set +the URL to `/browse`. Inside the method, what do we *always* return from a +controller? That's right: a `Response` object! Return a new `Response`... with a +short message to start. + +[[[ code('00c844ab6c') ]]] + +Let's try it! If we refresh the homepage, nothing changes. But if we go +to `/browse`... we're crushing it! A second page in under a minute! Dang! + +On this page, we'll eventually list mix tapes from other users. To help find +something we like, I want users to *also* be able to browse by *genre*. For example, +if I go to `/browse/death-metal`, that would show me all the death metal vinyl +mix tapes. Hardcore. + +Of course, if we try this URL right now... it doesn't work. + +> Not Route found + +No matching routes were found for this URL, so it shows us a 404 page. By the way, +what you're seeing is Symfony's fancy exception page, because we're currently +*developing*. It gives us *tons* of details whenever something goes wrong. When +you eventually deploy to production, you can design a *different* error page that +your users would see. + +## {Wildcard} Routes + +Anyways, the simplest way to make this URL work is just... to change the URL to +`/browse/death-metal`. + +[[[ code('7cf6861477') ]]] + +But... not super flexible, right? We would need one route +for *every* genre... which could be hundreds! And also, we just killed the `/browse` +URL! *It* now 404's. + +What we *really* want is a route that match `/browse/`. And we can do that +with a *wildcard*. Replace the hard-coded `death-metal` with `{}` and, inside, +`slug`. Slug is just a technical word for a "URL-safe name". Really, we could have +put anything inside the curly-braces, like `{genre}` or `{coolMusicCategory}`: it +makes no difference. But *whatever* we put inside this wildcard, we are then +*allowed* to have an argument with that same name: `$slug`. + +[[[ code('5a1436e579') ]]] + +Yup, if we go to `/browse/death-metal`, it will match this route and pass the string +`death-metal` to that argument. The matching is done by name: `{slug}` connects +to `$slug`. + +To see if it's working, let's return a different response: `Genre` then the `$slug`. + +[[[ code('90a1e7b05e') ]]] + +Testing time! Head back to `/browse/death-metal` and... yes! Try `/browse/emo` and +yea! I'm *that* much closer to my Dashboard Confessional mix tape! + +Oh, and it's optional, but you can add a `string` type to the `$slug` argument. +That doesn't change anything... it's just a nice way to program: the `$slug` was +*already* always going to be a string. + +[[[ code('dd04d150f1') ]]] + +A bit later, we'll learn how you could turn a *number* wildcard - like the +number 5 - into an integer if you want to. + +## Using Symfony's String Component + +Let's make this page a bit fancier. Instead of printing out the slug exactly, +let's convert it to a title. Say `$title = str_replace()` and replace any dashes +with spaces. Then, down here, use title in the response. In a future tutorial, +we're going to query the database for these genres, but, for right now, we can +at least make it look nicer. + +[[[ code('2d1891ae08') ]]] + +If we try it, Emo doesn't look any different... but death metal *does*. +But I want *more* fancy! Add another line with `$title =` then +type `u` and auto-complete a function that's literally called... `u`. + +We don't use many functions from Symfony, but this is a rare example. This comes +from a Symfony library called `symfony/string`. As I mentioned, Symfony is many +different libraries - also called components - and we're going to leverage those +libraries all the time. This one helps you make string transformations... and it +happens to already be installed. + +Move the `str_replace()` to the first argument of `u()`. This function +returns an object that we can then do string operations on. One method is +called `title()`. Say `->title(true)` to convert all words to title case. + +[[[ code('6cd92168f6') ]]] + +Now whe n we try it... sweet! It uppercases the letters! The string component isn't +particularly important, I just want you to see how we can already leverage +parts of Symfony to get our job done. + +## Making the Wildcard Optional + +Ok: one last challenge. Going to `/browse/emo` or `/browse/death-metal` works. +But just going to `/browse`... does *not* work. It's broken! A wild card can +match anything, but, by default, a wild card is *required*. We *have* to go to +`/browse/`. + +Can we make a wildcard optional? Absolutely! And it's delightfully simple: make the +corresponding argument optional. + +[[[ code('9ba3296dd1') ]]] + +As soon as we do that, it tells Symfony's routing layer that the `{slug}` does not +need to be in the URL. So now when we refresh... it works. Though, that's not a great +message for the page. + +Let's see. *If* there's a slug, then set the title the way we were. Else, set +`$title` to "All genres". Oh, and move the "Genre:" up here... so that down in +the `Response` we can just pass `$title`. + +[[[ code('8cef2cd6cb') ]]] + +Try that. On `/browse`... "All Genres". On `/browse/emo`... "Genre: Emo". + +Next: putting text like this into a controller.... isn't very clean or scalable, +especially if we start including HTML. Nope, we need to render a template. +To do *that*, we're going to install our first third-party package and witness the +massively important Symfony recipe system in action. diff --git a/sfcasts/ep2-fundamentals/bundle-config.md b/sfcasts/ep2-fundamentals/bundle-config.md new file mode 100644 index 0000000..a4fc3eb --- /dev/null +++ b/sfcasts/ep2-fundamentals/bundle-config.md @@ -0,0 +1,111 @@ +# Bundle Config (to Control Bundle Services) + +We're now using the `HttpClientInterface` and `CacheInterface` services. Yay! But +*we* aren't actually responsible for *instantiating* these service objects. +Nope, they're created by something else (we'll talk about that in a few minutes), +and then just passed to us. + +That's *great* because all of these services - the "tools" of our app - come ready +to use, out-of-the-box. But... if something *else* is responsible for instantiating +these service objects, how can we control them? + +*Introducing*: bundle configuration! + +## Bundle Configuration + +Go check out the `config/packages/` directory. This has a number of different YAML +files, all of which are loaded *automatically* by Symfony when it first boots +up. These files all have exactly *one* purpose: to configure the *services* that +each bundle gives us. + +Open up `twig.yaml`: + +[[[ code('0100059e45') ]]] + +For now, ignore this `when@test`: we're going to talk +about that in a few minutes. This file has a root key called `twig`. And so, the +entire purpose of this file is to control the services provide by the "Twig" bundle. +And, it's not the filename - `twig.yaml` - that's important. I could rename this to +`pineapple_pizza.yaml` and it would work *exactly* the same *and* be delicious. +I don't care what you think. + +When Symfony loads this file, it sees this root key - `twig` - and says: + +> Oh, okay. I'm going to pass whatever configuration is below to TwigBundle. + +And remember! Bundles give us *services*. Thanks to this config, when TwigBundle is +preparing its services, Symfony passes it this configuration and TwigBundle +*uses* it to decide *how* its services should be instantiated... like what class +names to use for each service... or what first second or third constructor arguments +to pass. + +For example, if we changed the `default_path` to something like +`%kernel.project_dir%/views`, the result is that the Twig service that renders +templates would now be pre-configured to look in that directory. + +The point is: the config in these files give us the power to control the services +that each bundle provides. + +Let's check out another one: `framework.yaml`: + +[[[ code('c5bd3da5ab') ]]] + +Because the root key is `framework`, all of this config is passed to FrameworkBundle... +which uses it to configure the services it provides. + +And, as I mentioned, the filename doesn't matter... though the name *often* matches +the root key... just for sanity reasons: like `framework` and `framework.yaml`. +But that's not *always* the case. Open up `cache.yaml`: + +[[[ code('ba42bd4e0b') ]]] + +Woh! This is... just *more* config for FrameworkBundle! It lives in its own file... +just because it's nice to have a separate file to control the cache. + +## Debugging the Available Bundle Config + +At this point, you might be asking yourself: + +> Ok, cool... but what config keys are we *allowed* to put here? Where can I +> find which options are available? + +Great question! Because... you can't just "invent" whatever keys you want: that would +throw an error. First, yes, you can, *of course*, read the documentation. But +there's *another* way: and it's one of my *favorite* things about Symfony's +config system. + +If you want to know what configuration you can pass to "Twig" bundle, there are two +`bin/console` commands to help you. The first is: + +```terminal +php bin/console debug:config twig +``` + +This will print out all of the *current* configuration under the `twig` key, +*including* any *default* values that the bundle is adding. You can see our +`default_path` set to the `templates/` directory, which comes from our config +file. This `%kernel.project_dir%` is just a fancy way to point to the root of our +project. More on that later. + +Try this: change the value to `views`, re-run that command and... yup! We see +"views" in the output. Let me go ahead and change that back. + +So `debug:config` shows us all of the *actual*, current config for a specific bundle, +like `twig`... which is especially handy since it also shows you *defaults* added +by the bundle. It's a great way to see *what* you can configure. For example, +apparently we can add a global variable to Twig via this `globals` key! + +The second command is similar: Instead of `debug:config`, it's `config:dump`: + +```terminal +php bin/console config:dump twig +``` + +`debug:config` shows you the *current* configuration... but `config:dump` +shows you a giant tree of *example* configuration, which includes *everything* that's +possible. Here you can see `globals` with some examples of how you could use that +key. This is a great way to see *every* potential option that you can pass to a +bundle... to help it configure its services. + +Let's use this new knowledge to see if we can "teach" the cache service to store +its files somewhere else. That's next. diff --git a/sfcasts/ep2-fundamentals/bundle-services.md b/sfcasts/ep2-fundamentals/bundle-services.md new file mode 100644 index 0000000..1c76aef --- /dev/null +++ b/sfcasts/ep2-fundamentals/bundle-services.md @@ -0,0 +1,129 @@ +# Finding & Using the Services from a Bundle + +We just installed KnpTimeBundle. Hooray! Um... but... uh... what does that mean? +What did doing that *give* us? + +The *number* one thing that a bundle gives us is... services! What services does +*this* bundle give us? Well, we could, of course, read the documentation, blah, +blah. Well, ok, you *should* do that... but, come on! Let's venture ahead recklessly +and learn by exploring! + +In the last tutorial, we learned about a command that shows us all of the services +in our app: `debug:autowiring`: + +```terminal-silent +php bin/console debug:autowiring +``` + +For example, if we search for "logger", there's apparently a service called +`LoggerInterface`. We *also* learned that we can autowire any service in this list +into our controller by using its *type*. By using this `LoggerInterface` type - +which is actually `Psr\Log\LoggerInterface` - Symfony knows to pass us this service. +Then, down here, we call methods on it like `$logger->info()`. + +We installed `KnpTimeBundle` a moment ago, so let's search for "time": + +```terminal-silent +php bin/console debug:autowiring time +``` + +And... hey! Look at this! We have a new `DateTimeFormatter` service! That's from +the new bundle and I bet that's what we're looking for. Let's go use it in +our controller. + +## Using the New DateTimeFormatter Service + +The type-hint we need is `Knp\Bundle\TimeBundle\DateTimeFormatter`. Ok! In +`VinylController`, find `browse()`, then add the new argument. + +By the way, the *order* of the arguments does *not* matter... except when it comes +to *optional* arguments. I made the `$slug` argument optional and you typically +need your optional arguments at the *end* of the list. So I'll add `DateTimeFormatter` +right here and hit "tab" to add the `use` statement on top. + +We can *name* the argument anything we want, like `$sherlockHolmes` or +`$timeFormatter`: + +[[[ code('616397799e') ]]] + +To use this, loop over the mixes - `foreach ($mixes as $key => $mix)`: + +[[[ code('7da483ab67') ]]] + +then, on each, add a new `ago` key: `$mixes[$key]['ago'] =`... and this is where we +need the new service. How do we *use* the `DateTimeFormatter`? I have no idea! But +we used its type, so PhpStorm should tell us what methods it has. Type +`$timeFormatter->`... and ok! It has 4 public methods. + +The one *we* want is `formatDiff()`. Pass it the "from" time... which is +`$mix['createdAt']`: + +[[[ code('85dac5f608') ]]] + +That's *all* we need! We're looping over these `$mixes`, taking the `createdAt` +key, which is a `DateTime` object, passing it to the `formatDiff()` method, which +should return a string in the "ago" format. To see if this is working, below, +`dd($mixes)`: + +[[[ code('ec67d50981') ]]] + +Let's try it! Spin over, refresh... and let's open it up. Yes! Look at that: `"ago" +=> "7 months ago"`... `"ago" => "18 days ago"`... It *works*. So remove that dump: + +[[[ code('07e24f0499') ]]] + +And now that each mix has a new `ago` field, in `browse.html.twig`, replace the +`mix.createdAt|date` code with `mix.ago`: + +[[[ code('47f88f8537') ]]] + +And now... *much* better. + +So: we had a problem... and *knew* it needed to be solved by a service... because +services do work. We didn't *have* a service that did what we needed *yet*, so we +went out, found one, and installed it. Problem *solved*! Symfony itself has a *ton* +of different packages, and each of them gives us several services. But sometimes +you'll need a third party bundle like this one to get the job done. Typically, +you can just search online for the problem you're trying to solve, plus "Symfony +bundle", to find it. + +## Using the ago Twig Filter + +In addition to the nice `DateTimeFormatter` service that we just used, this bundle +*also* gave us *another* service. But, this isn't a service that we're meant to +use directly, like in the controller. Nope! This service is meant to be +used by Twig *itself*... to power a brand new Twig filter! That's right! You can +add custom functions, filters... or *anything* to Twig. + +To see the new filter, let's try another useful debugging command: + +```terminal +php bin/console debug:twig +``` + +This prints a list of *all* of the functions, filters, and tests in Twig, along +with the *one* global Twig variable we have. If you go up to Filters, there's a new +one called "ago"! That was *not* there before we installed `KnpTimeBundle`. + +So, all of the work we did in our controller is perfectly fine ... but it turns +out that there's an easier way to do all of this. Delete the `foreach`... +remove the `DateTimeFormatter` service... and, though it's optional, clean up +the extra `use` statement on top: + +[[[ code('c1f3df9471') ]]] + +In `browse.html.twig`, we don't have an `ago` field anymore... but we still have a +`createdAt` field. Instead of piping this into the `date` filter, pipe it to `ago`: + +[[[ code('754b969c0e') ]]] + +That's all we need! Back over on the site refresh and... we get the *exact* +same result. + +By the way, we won't do it in this tutorial, but by the end, you'll be able to +easily follow the documentation to create your own custom Twig functions +and filters. + +Ok, so our app does *not* have a database yet... and it won't until the *next* +episode. But to make things more interesting, let's get our mixes data by making +an HTTP call to a special GitHub repository. That's next. diff --git a/sfcasts/ep2-fundamentals/bundles.md b/sfcasts/ep2-fundamentals/bundles.md new file mode 100644 index 0000000..f990e39 --- /dev/null +++ b/sfcasts/ep2-fundamentals/bundles.md @@ -0,0 +1,78 @@ +# Bundles! + +Hey friends! Welcome back to Episode 2 of our Symfony 6 tutorial series. This is +the one where we *seriously* level-up and unlock our potential to do *anything* +we want. That's because, in this course, we're diving into the fundamentals behind +*everything* in Symfony. We're talking about services, bundles, configuration, +environments, environment variables - the stuff that *truly* makes Symfony *tick*. +We're gonna throw open Symfony's hood and find out what's inside. + +## Site Setup! + +To get the *most* fundamentals out of this fundamentals tutorial, I warmly invite +you to cozy up to a fire, download the course code from this page and code along +with me. It'll be fun! After you unzip the file, you'll find a `start/` directory +with the same code that you see here. Follow our hand-crafted, locally-sourced +`README.md` file for all of the setup instructions. The *last* step will be to open +a terminal, move into the project and run + +```terminal +symfony serve -d +``` + +to start a local web server at `https://127.0.0.1:8000`. I'll cheat and click that +link to see our site. It is... Mixed Vinyl! Our new startup where users can build +their own custom "mixtape" - I'm thinking MMMBop followed by some Spice Girls - +except that we deliver it straight to your door on a freshly pressed vinyl record. +We even throw in that musty old record collection smell for free! + +## Services: Services Everywhere + +In the previous tutorial, we talked briefly about how *everything* in Symfony is +actually done by a *service*. And that the word "service" is a fancy term for a +simple concept: a service is an object that does work. + +For example, in `src/Controller/SongController.php`, we leveraged Symfony's Logger +service to log a message: + +[[[ code('0d6d809ebb') ]]] + +And, though we don't have the code in `VinylController` anymore, we briefly used +the Twig service to directly render a Twig template: + +[[[ code('f65f8310e4') ]]] + +So a service is just an object that does work... and *every* bit of work that's done +in Symfony is done by a service. Heck, even the core code that figures which route +matches the current URL is a service, called the "router" service. + +## Hello Bundles + +So the next question is: where do these services come from? The answer to that is +mordor. I mean *bundles*... services come from bundles. + +Open up `config/bundles.php`: + +[[[ code('796e719451') ]]] + +This isn't a file that you need to look at or worry about much, but this is where +your bundles are *activated*. + +Very simply: bundles are Symfony plugins. They're just PHP code... but they hook +*into* Symfony. And thanks to the recipe system, when we install a new bundle, that +bundle is *automatically* added to this file, which is how we already have 8 bundles +here. When we started our project, we only had 1! + +So a bundle is a Symfony plugin. And bundles can give us several things... though +they largely exist for one reason: to give us *services*. For example, this +TwigBundle up here gives us the Twig *service*. If we removed this line, the Twig +service would no longer exist and our application would *explode*... since we *are* +rendering templates. This `render()` line would no longer work. And MonologBundle +is what gives us the Logger service that we're using in `SongController`. + +So by adding more bundles into our application, we're getting more *services*, and +services are tools! Need more services? Install more bundles! It's like Neo in +the best, I mean first Matrix movie. + +Next... let's teach our app some Kung fu by installing a *new* bundle that gives +us a *new* service to solve a *new* problem. diff --git a/sfcasts/ep2-fundamentals/cache-config.md b/sfcasts/ep2-fundamentals/cache-config.md new file mode 100644 index 0000000..661b5eb --- /dev/null +++ b/sfcasts/ep2-fundamentals/cache-config.md @@ -0,0 +1,103 @@ +# Configuring the Cache Service + +So... I want to know how I can configure the cache service... like to store the +cache somewhere else. In the real world, we can just search for "How do I configure +Symfony's cache service". But... we can *also* figure this out on our own, by +using the commands we just learned. + +We already noticed there's a `cache.yaml` file: + +[[[ code('669c75d7a9') ]]] + +It looks like FrameworkBundle is responsible for creating the cache service... +and it has a sub `cache` key where we can pass *some* values to control it. +All of this is commented-out at the moment. + +To get more information about FrameworkBundle, run: + +```terminal +php bin/console config:dump framework +``` + +FrameworkBundle is the main bundle inside of Symfony. So you can see that this +dumps... wow... a *ton*. FrameworkBundle provides a *lot* of services... so there's +a lot of config. + +## Debugging the Cache Config + +To... zoom in a bit, re-run the command again, passing `framework` *and* then +`cache` to filter for that sub-key: + +```terminal-silent +php bin/console config:dump framework cache +``` + +And... cool! This may not always be *super* understandable, but it's a great starting +point. This definitely just helped us answer the question: + +> Why does the cache system store stuff in the var/cache directory? + +Because... there's a `directory` key that defaults to `%kernel.cache_dir%`... which +is a fancy way of pointing at the `/var/cache/dev` directory. And then we see +`/pools/app`, which is the actual directory that holds our cache. + +## Using dump() and the Profiler + +So here's the goal: instead of caching things to the filesystem, I want to change +the cache system to store somewhere else. Before we do that, go into +`VinylController` and, so we can see the *result* of the change we're about to make, +`dump($cache)`. We've been using `dd()` so far, which stands for "dump and die". +But in this case I want `dump()`... but let the page load. + +[[[ code('8e64b535c2') ]]] + +Refresh now. Wait, where *is* my dump? This is a... feature! When you use `dump()`, +you won't actually see it on the page: it hides down here on the web debug toolbar. +If you look there, the cache is some sort of `TraceableAdapter`. But inside of *that*, +there's an object called `FilesystemAdapter`. That's proof that the cache system +is *saving* to the filesystem. + +## Configuring the Cache Adapter + +To make this store somewhere else, go into `cache.yaml` and change this `app` key. +You can set this to a number of different special strings, called adapters. If we +wanted to store our cache in Redis, we would use `cache.adapter.redis`. + +To make things really easy, use `cache.adapter.array`: + +[[[ code('cc8987c4ae') ]]] + +The `array` adapter is a *fake* cache where it *does* store things... but it only lives +for the duration of the request. So, at the end of each request, it forgets +everything. It's a fake cache, but it's enough to see how changing this key will affect +the cache service itself. + +Watch what happens. Currently, we have a `FilesystemAdapter`. When we refresh... +the cache is an `ArrayAdapter`! And since the `ArrayAdapter` forgets its cache at +the end of the request, you can see that every single request *does* now makes an +HTTP request. + +## Takeaway: It's all about Controlling how Services are Instantiated + +If you're a little confused by this, let me try to clear things up. The point of +this chapter is *not* to teach you how to change this *specific* key in the cache +file. Ultimately, if you need to configure something in Symfony, you'll just search +the docs... which will tell you exactly what to do and which key to change. + +Nope, the big takeaway is that the *sole* purpose of these config files is to +*configure* the *services* in our app. Each time you change a key in *any* +of these files, the end result is that you just changed how some service is +*instantiated*. Tweaking a key may change the entire class name of a service object, +like in this case, or it may change the 2nd or 3rd constructor argument that +will be passed when the service is instantiated. It doesn't really matter +*what* changes, as long as you realize that this config is *all* about services +and how they're instantiated. + +In fact, *none* of this config can be read directly from your app. You couldn't, +for example, ask for the "cache" configuration from inside of a controller. Nope, +Symfony reads this config, uses it to configure how each service object will be +instantiated, then throws it away. Services are supreme. + +Next, sometimes you'll need certain configuration to be *different* based on whether +you're developing locally or running on production. Symfony has a system for +this called "environments". Let's learn *all* about that. diff --git a/sfcasts/ep2-fundamentals/cache-service.md b/sfcasts/ep2-fundamentals/cache-service.md new file mode 100644 index 0000000..f51ca37 --- /dev/null +++ b/sfcasts/ep2-fundamentals/cache-service.md @@ -0,0 +1,133 @@ +# The Cache Service + +*Now* when we refresh the browse page, the mixes are coming from a repository +on GitHub! We make an HTTP request to the GitHub API, that fetches this file right +here, we call `$response->toArray()` to *decode* that JSON into a `$mixes` array... +and then we render *that* in the template. Yup, this file on GitHub is our +temporary fake database! + +One practical problem is that *every single* page load is now making an HTTP +request... and HTTP requests are *slow*. If we deployed this to production, +our site would be *so* popular, of course, that we'd pretty quickly hit our GitHub +API limit. And *then* this page would *explode*. + +So... I'm thinking: what if we cache the result? We could make this HTTP request, +then cache the data for 10 minutes, or even an hour. That just *might* work! +How do we *cache* things in Symfony? You guessed it: with a service! Which service? +I dunno! So let's go find out. + +## Finding the Cache Service + +Run: + +```terminal +php bin/console debug:autowiring cache +``` + +to search for services with "cache" in their name. And... yes! There are, in fact, +*several*! There's one called `CacheItemPoolInterface`, and another called +`StoreInterface`. Some of these aren't *exactly* what we're looking for, but +`CacheItemPoolInterface`, `CacheInterface`, and `TagAwareCacheInterface` *are* all +different services that you can use for caching. They all *effectively* do the same +thing... but the easiest to use is `CacheInterface`. + +So let's grab that.... by doing our fancy autowiring trick! Add another argument +to our method typed with `CacheInterface` (make sure you get the one from +`Symfony\Contracts\Cache`) and call it, how about, `$cache`: + +[[[ code('6b4b596a2d') ]]] + +To *use* the `$cache` service, copy these two lines from before, delete them, +and replace them with `$mixes = $cache->get()`, as if you're going to fetch +some key out of the cache. We can invent whatever cache key we want: +how about `mixes_data`. + +Symfony's cache object works in a unique way. We call `$cache->get()` and pass +it this key. If that result already exists in the cache, it will be returned +*immediately*. If it does *not* exist in the cache yet, then it will call our *second* +argument, which is a function. In here, our job is to return the data that *should* +be cached. Paste in the two lines of code that we copied earlier. This `$httpClient` +is undefined, so we need to add `use ($httpClient)` to bring it into scope. + +There we go! And instead of setting the `$mixes` variable, just `return` this +`$response->toArray()` line: + +[[[ code('c6c78aea17') ]]] + +If you haven't used Symfony's caching service before, this might look strange! +But I love it! The first time we refresh the page, there won't be any `mixes_data` +in the cache yet. So it will call our function, return the result, and then the +cache system will store *that* in the cache. The *next* time we refresh the page, +the key *will* be in the cache, and it will return the result *immediately*. So +we don't need any "if" statements to see if something is already in the cache... +just this! + +## Debugging with the Cache Profiler + +But... will it blend? Let's go find out. Refresh and... beautiful! The first refresh +*still* made the HTTP request like normal. Down on the web debug toolbar, we can +see that there were *three* cache calls and *one* cache write. Open this in a new +tab to jump into the cache section of the profiler. + +So cool: this shows us that there was one call to the cache for `mixes_data`, one +cache *write*, and one cache *miss*. A cache "miss" means that it called our function +and wrote that to the cache. + +On the next refresh, watch this icon here. It disappears! That's because there +was *no* HTTP request. If you open the Cache profiler again, this time there was +one read and one hit. That hit means that the result was loaded from the cache and +it did *not* make an HTTP request. That's exactly what we wanted! + +## Setting the Cache Lifetime + +Now, you *might* be wondering: how long will this info *stay* in the cache? +Right now... *forever*. Ooooh. That's the default. + +To make it expire *sooner* than forever, give the function a `CacheItemInterface` +argument - make sure to hit "tab" to add that use statement - and call it +`$cacheItem`. Now we can say `$cacheItem->expiresAfter()` and, to make it easy, +say `5`: + +[[[ code('205124d7cf') ]]] + +The item will expire after 5 seconds. + +## Clearing the Cache + +Unfortunately, if we try this, the item that's *already* in the cache is set to +*never* expire. So... this won't actually work until we clear the cache. But... +where *is* the cache being stored? Another great question! We'll talk more about +that in a second... but, by default, it's stored in `var/cache/dev/`... +along with a bunch of other cache files that help Symfony do its job. + +We *could* delete this directory manually to clear the cache... but Symfony has a +better way! It is, of course, another `bin/console` command. + +Symfony has a bunch of different "categories" of cache called "cache pools". If +you run: + +```terminal +php bin/console cache:pool:list +``` + +you'll see all of them. Most of these are meant for *Symfony* to use internally. +The cache pool that *we're* using is called `cache.app`. To clear that, run: + +```terminal +php bin/console cache:pool:clear cache.app +``` + +Thats it! This isn't something you'll need to do very often, but it's good to know, +just in case. + +Okay, check this out. When we refresh... we get a cache *miss* and you can see that +it *did* make an HTTP call. But if we refresh again really quickly... it's gone! +Refresh again and... it's back! That's because the five seconds just expired. + +Ok team: we're now leveraging an HTTP client service *and* cache service... both +of which were prepared *for* us by one of our bundles so that we can just... use them! + +But, I do have a question. What if we need to *control* these services? For example, +how could we tell the cache service that, instead of saving things onto the filesystem +in this directory, we want to store things in Redis... or memcache? Let's explore +the idea of *controlling* our services through configuration next. diff --git a/sfcasts/ep2-fundamentals/command-extra.md b/sfcasts/ep2-fundamentals/command-extra.md new file mode 100644 index 0000000..56052ba --- /dev/null +++ b/sfcasts/ep2-fundamentals/command-extra.md @@ -0,0 +1,96 @@ +# Command: Autowiring & Interactive Questions + +Last chapter team! Let's do this! + +Ok, what if we need a *service* from inside our command? For example, let's say +that we want to use `MixRepository` to print out a vinyl mix recommendation. +How can we do that? + +Well, we're inside of a service and we need access to *another* service, which +means we need... the dreaded *dependency injection*. Kidding - not dreaded, easy +with autowiring! + +Add `public function __construct()` with `private MixRepository $mixRepository` +to create and set that property all at once. + +[[[ code('628bac0c7d') ]]] + +Though, if you hover over `__construct()`, it says: + +> Missing parent constructor call. + +To fix this, call `parent::__construct()`: + +[[[ code('f2801b5b65') ]]] + +This is a *super* rare situation where the base class has a constructor that we +need to call. In fact, this is the *only* situation I can think of in Symfony +like this... so not *normally* something you need to worry about. + +## Interactive Questions + +Down here, let's output a mix recommendation... but make it even *cooler* by +first asking the user *if* they want this recommendation. + +We can ask interactive questions by leveraging the `$io` object. I'll say +`if ($io->confirm('Do you want a mix recommendation?'))`: + +[[[ code('cdef6583b7') ]]] + +This will ask that question, and if the user answers "yes", return true. +The `$io` object is *full* of cool stuff like this, including asking multiple +choice questions, and auto-completing answers. Heck, we can even build a progress bar! + +Inside the if, get all of the mixes with +`$mixes = $this->mixRepository->findAll()`. Then... we need just a bit of ugly +code - `$mix = $mixes[array_rand($mixes)]` - to get a random mix. + +Print the mix with one more `$io` method `$io->note()` passing +`I recommend the mix` and then pop in `$mix['title']`: + +[[[ code('bb2cf7650b') ]]] + +And... done! By the way, notice this `return Command::SUCCESS`? That controls +the exit code of your command, so you'll always want to have `Command::SUCCESS` at +the bottom of your command. If there was an error, you could `return Command::ERROR`. + +***TIP +Whoops, the correct constant name if the command fails is `Command::FAILURE`! +*** + +Okay, let's try this! Head over to your terminal and run: + +```terminal +php bin/console app:talk-to-me --yell +``` + +We get the output... and then we get: + +> Do you want a mix recommendation? + +Why, yes we *do*! And what an *excellent* recommendation! + +All right, team! We did it! We finished - what I think is - the most important +Symfony tutorial of all time! No matter what you need to build in Symfony, the +concepts we've just learned will be the *foundation* of doing it. + +For example, if you need to add a custom function or filter to Twig, no problem! +You do this by creating a Twig *extension* class... and you can use MakerBundle +to generate this for you or build it by hand. It's very similar to creating a +custom console command: in both cases, you're building something to "hook into" +part of Symfony. + +So, to create a Twig *extension*, you would create a new PHP class, make it +implement whatever interface or base class that Twig extensions need (the +documentation will tell you that)... and then you just fill in the logic... which +I won't show here. + +That's it! Behind the scenes, your Twig extension would *automatically* be seen as +a service, and autoconfiguration would make sure it's integrated into Twig... +*exactly* like the console command. + +In the next course, we'll put our new superpowers to work by adding a database to +our app so that we can load real, dynamic data. And if you have any *real*, +*dynamic* questions, we are here for you, as always, down in the comment section. + +All right, friends. Thanks so much for coding with me and we'll see you next time. diff --git a/sfcasts/ep2-fundamentals/controllers-services.md b/sfcasts/ep2-fundamentals/controllers-services.md new file mode 100644 index 0000000..989c2b3 --- /dev/null +++ b/sfcasts/ep2-fundamentals/controllers-services.md @@ -0,0 +1,53 @@ +# Controllers are Services Too! + +Open up `src/Controller/VinylController.php`. It *may* or may not be obvious, but our +controller classes are *also* services in the container! Yep! They *feel* special +because they're *controllers*... but they're really just good old, boring services +like everything else. Well, except that they have one *superpower* that *nothing* else +has: the ability to autowire arguments into their action *methods*. Normally, +autowiring only works with the constructor. + +## Binding Action Arguments + +And, the action methods really *do* work *just* like the constructors when it comes +to autowiring. For example, add a `bool $isDebug` argument to the `browse()` action... +then `dump($isDebug)` below: + +[[[ code('b6b1e19418') ]]] + +And that... doesn't work! So far, the only two things that we know we are allowed +to have as arguments to our "actions" are (A), any wildcards in the route like +`$slug` and (B) autowireable services, like `MixRepository`. + +But now, go back to `config/services.yaml` and *uncomment* that global `bind` from +earlier: + +[[[ code('78858f9ac6') ]]] + +This time... it works! + +## Adding a Constructor + +Going in the *other* direction, because controllers are services, you can *absolutely* +have a constructor if you want. Let's move `MixRepository` and `$isDebug` up to a +new constructor. Copy those, remove them... add `public function __construct()`, +paste... then I'll put them on their own lines. To turn them into properties, add +`private` in front of each: + +[[[ code('4c824ee41c') ]]] + +Back down below, we just need to make sure we change to `dump($this->isDebug)` and +add `$this->` in front of `mixRepository`: + +[[[ code('672b0fa2a8') ]]] + +Nice! If we try this now... that works just fine! + +I don't *normally* follow this approach... mainly because adding arguments to the +action *method* is just so darn easy. But if you need a service or other value in +*every* action method of your class, you can definitely clean up your argument list +by injecting it through the constructor. I'll go remove that `dump()`. + +Next, let's talk about environment variables and the purpose of the `.env` file +that we looked at earlier. This stuff will become increasingly important as we +make our app more and more realistic. diff --git a/sfcasts/ep2-fundamentals/create-service.md b/sfcasts/ep2-fundamentals/create-service.md new file mode 100644 index 0000000..9f3b8fa --- /dev/null +++ b/sfcasts/ep2-fundamentals/create-service.md @@ -0,0 +1,98 @@ +# Creating a Service + +We know that bundles give us services and services do work. Ok. But what if we need +to write our *own* custom code that does work? Should we... put that into our *own* +service class? Absolutely! And it's a *great* way to organize your code. + +We *are* already doing some work in our app. In the `browse()` action: + +[[[ code('51d407c154') ]]] + +we make an HTTP request and cache the result. Putting this logic in our controller is *fine*. +But by moving it into its own service class, it'll make the *purpose* of the code +more clear, allow us to reuse it from multiple places... and even enable us to +unit test that code if we want to. + +## Creating the Service Class + +That sounds *amazing*, so let's do it! How do we create a service? In the `src/` +directory, create a new PHP class *wherever* you want. It seriously doesn't matter +what directories or subdirectories you create in `src/`: do whatever feels good +for you. + +For this example, I'll create a `Service/` directory - though again, you could +call that `PizzaParty` or `Repository` - and inside of *that*, a new +PHP class. Let's call it... how about `MixRepository`. "Repository" is a pretty common +name for a service that returns data. Notice that when I create this, PhpStorm +*automatically* adds the correct namespace. It doesn't matter *how* we organize our +classes inside of `src/`... as long as our namespace matches the directory: + +[[[ code('dab4aa0947') ]]] + +One important thing about service classes: they have *nothing* to do with Symfony. +Our controller class is a Symfony concept. But `MixRepository` is a class *we're* +creating to organize our *own* code. That means... there are no rules! We don't +need to extend a base class or implement an interface. We can make this class look +and feel however we want. The power! + +With that in mind, let's create a new `public function` called, how about, +`findAll()` that will `return` an `array` of all of the mixes in our system. Back +in `VinylController`, copy all of the logic that fetches the mixes and paste that +here: + +[[[ code('3085dac9b6') ]]] + +PhpStorm will ask if we want to add a `use` statement for the `CacheItemInterface`. +We *totally* do! Then, instead of creating a `$mixes` variable, just `return`: + +[[[ code('eebdfc0b0f') ]]] + +There *are* some undefined variables in this class... and those *will* be a problem. +But ignore them for a minute: I *first* want to see if we can *use* our shiny new +`MixRepository`. + +## Is our Service already in the Container? + +Head into `VinylController`. Let's think: we somehow need to tell Symfony's service +container about our new service so that we can then *autowire* it in the same way +we're autowiring core services like `HtttpClientInterface` and `CacheInterface`. + +Whelp, I have a surprise! Spin over to your terminal and run: + +```terminal +php bin/console debug:autowiring --all +``` + +Scroll up to the top and... amaze! `MixRepository` is *already* a service in +the container! Let me explain two things here. First, the `--all` flag is not *that* +important. It *basically* tells this command to show you the core services like +`$httpClient` and `$cache`, *plus* our own services like `MixRepository`. + +Second, the container... *somehow* already saw our repository class and recognized it +as a service. We'll learn *how* that happened in a few minutes... but for now, it's +enough to know that our new `MixRepository` *is* already inside the container *and* +its service *id* is the full class name. *That* means we can autowire it! + +## Autowiring the new Service + +Back over in our controller, add a third argument type-hinted with `MixRepository` - +hit tab to add the `use` statement - and call it... how about `$mixRepository`: + +[[[ code('a05b30f0d4') ]]] + +Then, down here, we don't need any of this `$mixes` code anymore. Replace it with +`$mixes = $mixRepository->findAll()`: + +[[[ code('c5fdb1c634') ]]] + +How nice is that? Will it work? Let's find out! Refresh and... it *does*! Ok, +working in this case means that we get an `Undefined variable $cache` message +coming from `MixRepository`. But the fact that our code *got* here means +that autowiring `MixRepository` *worked*: the container saw this, *instantiated* +`MixRepository` and passed it to us so that we could use it. + +So, we created a service and made it available for autowiring! We are *so* cool! +But our new service needs the `$httpClient` and `$cache` services in order to do +its job. How do we get those? The answer is one of the *most* important concepts +in Symfony and object-oriented coding in general: dependency injection. Let's talk +about that next. diff --git a/sfcasts/ep2-fundamentals/custom-command.md b/sfcasts/ep2-fundamentals/custom-command.md new file mode 100644 index 0000000..f30aa02 --- /dev/null +++ b/sfcasts/ep2-fundamentals/custom-command.md @@ -0,0 +1,98 @@ +# Customizing a Command + +We have a new console command! *But*... it doesn't do much yet, aside from printing +out a message. Let's make it *fancier*. + +Scroll to the top. This is where we have the name of our command, and there's also +a description... which shows up next to the command. Let me change ours to + +> A self-aware command that can do... only one thing. + +[[[ code('0665b762f7') ]]] + +## Configuring Arguments and Options + +Our command is called `app:talk-to-me` because, when we run this, I want to make +it possible to pass a name to the command - like Ryan - and then it'll reply with +"Hey Ryan!". So, literally, we'll type `bin/console app:talk-to-me ryan` and it'll +reply back. + +When you want to pass a value to a command, that's known as an *argument*... and +those are configured down in... the `configure()` method. There's already an +argument called `arg1`... so let's change that to `name`. + +This key is completely *internal*: you'll never see the word `name` when you're +*using* this command. But we *will* use this key to *read* the argument value in +a minute. We can also give the argument a description and, if you want, you can +make it *required*. I'll keep it as optional. + +The next thing we have are *options*. These are like arguments... except that +they start with a `--` when you use them. I want to have an optional flag where +we can say `--yell` to make the command *yell* our name back. + +In this case, the name of the option, `yell`, *is* important: we *will* use +this name when passing the option at the command line to use it. The +`InputOption::VALUE_NONE` means that our flag will just be `--yell` and not +`--yell=` some value. If your option accepts a value, you would change this to +`VALUE_REQUIRED`. Finally, give this a description. + +[[[ code('8001eef9d6') ]]] + +Beautiful! We're not *using* this argument and option yet... but we can already +re-run our command with a `--help` option: + +```terminal +php bin/console app:talk-to-me --help +``` + +And... awesome! We see the description up here... along with some details about +how to use the argument and the `--yell` option. + +## Filling in execute() + +When we call our command, very simply, Symfony will call `execute()`... which +is where the fun starts. Inside, we can do *whatever* we want. It passes us +two arguments: `$input` and `$output`. If you want to read some input - like the +`name` argument or the `yell` option, use `$input`. And if you want to *output* +something, use `$output`. + +But in Symfony, we normally pop these two things into *another* object +called `SymfonyStyle`. This is helper class makes reading and outputing +easier... and fancier. + +Ok: let's start by saying `$name = $input->getArgument('name')`. If we don't +have a name, I'll default this to `whoever you are`. Below, read the +option: `$shouldYell = $input->getOption('yell')`: + +[[[ code('c25283b263') ]]] + +Cool. Let's clear out this stuff down here and start our message: +`$message = sprintf('Hey %s!', $name)`. Then if we want to yell, you know what +to do: `$message = strtoupper($message)`. Below, use `$io->success()` and +put the message there. + +[[[ code('e396b4e104') ]]] + +This is one of the many helper methods on the `SymfonyStyle` class that help +format your output. There's also `$io->warning()`, `$io->note()`, and several others. + +Let's try it. Spin over and run: + +```terminal +php bin/console app:talk-to-me ryan +``` + +And... oh hello there! If we yell: + +```terminal-silent +php bin/console app:talk-to-me ryan --yell +``` + +THAT WORKS TOO! We can even yell at 'whoever I am': + +```terminal-silent +php bin/console app:talk-to-me --yell +``` + +Awesome! But let's get crazier... by autowiring a service and asking a question +*interactively* on the command line. That's next... and it's the last chapter! diff --git a/sfcasts/ep2-fundamentals/debug-container.md b/sfcasts/ep2-fundamentals/debug-container.md new file mode 100644 index 0000000..64abb5e --- /dev/null +++ b/sfcasts/ep2-fundamentals/debug-container.md @@ -0,0 +1,125 @@ +# debug:container & How Autowiring Works + +Ok, I lied. *Before* we talk about environments, I need to come clean about +something: I have *not* been showing you all of the services in Symfony. Not +even close. + +Head over your terminal and run our *favorite* command: + +```terminal +php bin/console debug:autowiring +``` + +We know that all of these services are floating around in Symfony, waiting for +us to ask for them. *And* we know that bundles give us services. The Twig service +down here comes from TwigBundle. + +And since each service is an *object*, something *somewhere* must be responsible +for *instantiating* these objects. The question is: "Who?" And the answer is... +the service container! + +## Hello Service Container + +It turns out that all of the services aren't really... "floating around": they +all live inside something called the "container". And there are *way* more services +in the container than `debug:autowiring` has been telling us about. Ooh... secrets! +This time, run: + +```terminal +php bin/console debug:container +``` + +And... whoa! This prints out a *huge* list. It's so big, it's hard to see everything. +Let me make my font smaller. Much better! + +*This* is the full list of all of the services in our app... or in the +"container". The container is basically a giant "array" where each service +has a unique name that points to its service object. For example, down here... there +we go... we can see that there's a service whose unique name - or "id" is `twig`. + +Knowing that the id of the Twig service is `twig` is not *usually* important, but +it *is* useful to understand that each service has a unique id... and that you can +see all of them inside the `debug:container` command. + +## The Container Creates Objects + +And really, the container might be better-described as a big array of *instructions* +on how to instantiate services, *if* and *when* something asks for them. For instance, +the container knows *exactly* how to instantiate this Twig service. It knows that +its class is `Twig\Environment`. And even though you can't see it on this list, it +knows the *exact* arguments to pass to its constructor. The moment someone +*needs* the Twig service, the container instantiates it and returns it. + +Yup, when we autowire a service, we're basically saying: + +> Hey container, can you please give me the HTTP Client service? + +If nothing in our code has asked for that service yet during this request, the +container will create it. But if something *has* already asked for it, then the +container will simply return the one it *already* created. This means that if we +ask for the HTTP Client service in *ten* different places, the container will only +create and return the same *one* instance. Pretty cool! + +## How Autowiring Works + +Anyway, `debug:container` shows us *all* of the services that the container knows +how to instantiate. *But* `debug:autowiring` only shows us a *fraction* of those +services. Why? + +Well, it turns out that not *all* services are autowireable. Many of the items in +this list are low-level services that just exist to help *other* services do their +job. You'll probably never need to use these low-level services directly... and +you actually *cannot* fetch them via autowiring. + +But, let's back up a minute. Now that we know a bit more, we can now learn exactly +*how* Symfony's autowiring system works. It's beautifully simple. + +As we've seen, the container is really an array where every service has an +id that points to that service object. When Symfony sees this `HttpClientInterface` +type - this is the full type that it sees, thanks to our `use` statement - in order +to figure out *which* service in the container it needs to pass us, it simply looks +for a service whose *ID* matches this string *exactly*. Let me show you! + +Scroll towards the top of this list to find... a service whose ID is +`Symfony\Contracts\HttpClient\HttpClientInterface`! The *vast* majority of the +services in the container use the "snake case" naming strategy. But if a service +is intended for *us* to use in our code, Symfony will add an *additional* service +inside that matches its class or interface name. + +Thanks to that, when we type-hint `HttpClientInterface`, Symfony looks in the +container for a service whose id is +`Symfony\Contracts\HttpClient\HttpClientInterface`, it finds it and passes it +to us. + +## Service Aliases + +But look over on the right side: it says that this is an alias for +a different service ID. An "alias" is like a symbolic link. It means that +when someone asks for the `HttpClientInterface` service, Symfony will *actually* +pass us this *other* service. + +We can use the same logic down here for the `CacheInterface` type. If we check +the list, here's the service whose id matches that type. But, in reality, it's +just an alias for a service called `cache.app`. So when we autowire `CacheInterface`, +the `cache.app` service is what's *actually* being passed to us. + +If you're feeling unsure, here are the three big takeaways. One: there are a *ton* +of service objects floating around and they all live inside something called the +"container". Each service has a unique id. + +Two, only a *small* percentage of these are useful to us... and those are set up +so that we can autowire them. Autowiring works by looking in the container for a +service whose id exactly matches the type. When we run `debug:autowiring`, it's +basically just showing us the services from this list whose id is a class or +interface name. Those are the "autowireable services". + +The third and final takeaway is that services also have an alias system... which +just means that when we ask for the `CacheInterface` service, what it will *really* +give us is the service whose id is `cache.app`. + +If you're wondering how we could ever use a *non-autowireable* service in our code, +that's a great question! It's somewhat rare, but we *will* learn how to do that +later. + +Next, let's talk about using different configuration locally versus production. +Let's talk about *environments*. diff --git a/sfcasts/ep2-fundamentals/dependency-injection.md b/sfcasts/ep2-fundamentals/dependency-injection.md new file mode 100644 index 0000000..7e36ffc --- /dev/null +++ b/sfcasts/ep2-fundamentals/dependency-injection.md @@ -0,0 +1,169 @@ +# Dependency Injection + +Our `MixRepository` service is *sort of* working. We can autowire it into our +controller and the container is *instantiating* the object and passing it to us. +We *prove* that over here because, when we run the code, it successfully calls +the `findAll()` method. + +But.... then it explodes. That's because, inside `MixRepository` we have two +undefined variables. In order for our class to do its job, it needs two services: the +`$cache` service and the `$httpClient` service. + +## Autowiring to Methods is a Controller-Only Superpower + +I keep saying that there are many services floating around inside of Symfony, waiting +for us to use them. That's *true*. *But*, you can't just grab them out of thin air +from anywhere in your code. For example, there's no `Cache::get()` static method +that you can call whenever you want that will return the `$cache` service object. +Nothing like that exists in Symfony. And that's good! Allowing us to grab objects +out of thin air is a recipe for writing bad code. + +So how *can* we get access to these services? Currently, we only know one way: by +autowiring them into our controller. *But* that *won't* work here. Autowiring services +into a *method* is a superpower that *only* works for controllers. + +Watch: if we added a `CacheInterface` argument... then went over and refreshed, +we'd see: + +> Too few arguments to function [...]findAll(), 0 passed [...] and exactly 1 expected. + +That's because *we* are calling `findAll()`. So if `findAll()` needs an argument, +it is *our* responsibility to pass them: there's no Symfony magic. My point is: +autowiring works in controller methods, but don't expect it to work for any +*other* methods. + +## Manually Passing Services to a Method? + +*But* one way we *might* get this to work is by adding both services to the +`findAll()` method and then *manually* passing them in from the controller. This +won't be the final solution, but let's try it. + +I already have a `CacheInterface` argument... so now add the +`HttpClientInterface` argument and call it `$httpClient`: + +[[[ code('bbbc6e904f') ]]] + +Perfect! The code in this method is now happy. + +Back over in our controller, for `findAll()`, pass `$httpClient` and `$cache`: + +[[[ code('4592179cbd') ]]] + +And now... it works! + +## "Dependencies" Versus "Arguments" + +So, on a high level, this solution makes sense. We know that we can autowire services +into our controller... and then we just pass them into `MixRepository`. But if you +think a bit deeper, the `$httpClient` and `$cache` services aren't really *input* +to the `findAll()` function. They don't really make sense as arguments. + +Let's look at an example. Pretend that we decide to change the `findAll()` method +to accept a `string $genre` argument so the method will *only* return mixes for +*that* genre. This argument makes perfect sense: passing different genres changes +what it returns. The argument *controls* how the method *behaves*. + +*But* the `$httpClient` and `$cache` arguments *don't* control how the function +behaves. In reality, we would pass these *same* two values *every* time we call the +method... *just* so things *work*. + +Instead of arguments, these are really *dependencies* that the service *needs*. +They're just stuff that *must* be available so that `findAll()` can do its job! + +## Dependency Injection & The Constructor + +For "dependencies" like this, whether they're service objects or static +configuration that your service needs, instead of passing them to the methods, +we pass them into the *constructor*. Delete that pretend `$genre` argument... then +add a `public function __construct()`. Copy the two arguments, delete them, and +move them up here: + +[[[ code('88ae2b61fb') ]]] + +Before we finish this, I need to tell you that autowiring works in *two* places. We +already know that we can autowire arguments into our controller methods. But we +can *also* autowire arguments into the `__construct()` method of any service. In +fact, that's the *main* place that autowiring is meant to work! The fact that +autowiring also works for controller methods is... kind of an "extra" just to make +life nicer. + +Anyways, autowiring works in the `__construct()` method of our services. So as long +as we type-hint the arguments (and we have), when Symfony instantiates our service, +it will pass us these two services. Yay! + +And what do we *do* with these two arguments? We set them onto properties. + +Create a `private $httpClient` property and a `private $cache` property. Then, down +in the constructor, assign them: `$this->httpClient = $httpClient`, and +`$this->cache = $cache`: + +[[[ code('811c0bad31') ]]] + +So when Symfony instantiates our `MixRepository`, it passes us these two arguments +and we store them on properties so we can use them later. + +Watch! Down here, instead of `$cache`, use `$this->cache`. And then we don't need +this `use ($httpClient)` over here... because we can say `$this->httpClient`: + +[[[ code('f8d4d7c4cd') ]]] + +This service is now in *perfect* shape. + +Back over in `VinylController`, now we can simplify! The `findAll()` +method doesn't need any arguments... and so we don't even need to autowire +`$httpClient` or `$cache` at all. I'm going to celebrate by removing those `use` +statements on top: + +[[[ code('6d017b765c') ]]] + +Look how much easier that is! We autowire the *one* service we need, call the method +on it, and... it even *works*! *This* is how we write services. We add any +dependencies to the constructor, set them onto properties, and then *use* them. + +## Hello Dependency Injection! + +By the way, what we just did has a fancy schmmancy name: "Dependency injection". +But don't run away! That may be a scary... or at least "boring sounding" term, but +it's a very simple concept. + +When you're inside of a service like `MixRepository` and you realize you need +*another* service (or maybe some config like an API key), to get it, create a +constructor, add an argument for the thing you need, set it onto a property, and +then use it down in your code. Yep! *That's* dependency injection. + +Put simply, dependency injection says: + +> If you need something, instead of grabbing it out of thin air, force Symfony to +> pass it to you via the constructor. + +This is one of *the* most important concepts in Symfony... and we'll do this over +and over again. + +## PHP 8 Property Promotion + +Okay, *unrelated* to dependency injection and autowiring, there are two minor +improvements that we can make to our service. The first is that we can add *types* +to our properties: `HttpClientInterface` and `CacheInterface`: + +[[[ code('157fc6f3fd') ]]] + +That doesn't change how our code works... it's just a nice, responsible way to do things. + +But we can go further! In PHP 8, there's a new, shorter syntax for creating +a property and setting it in the constructor like we're doing. It looks like +this. First, I'll move my arguments onto multiple lines... just to keep things +organized. Now add the word `private` in front of each argument. Finish by +deleting the properties... as well as the inside of the method. + +That might look weird at first, but as soon as you add `private`, `protected`, or +`public` in front of a `__construct()` argument, that creates a property with this +name and sets the argument *onto* that property: + +[[[ code('a7cff8b189') ]]] + +So it looks different, but it's the *exact* same as what we had before. + +When we try it... yup! It still works. + +Next: I keep saying that the container holds *services*. That's *true*! But it also +holds one other thing - simple configuration called "parameters". diff --git a/sfcasts/ep2-fundamentals/environment-variables.md b/sfcasts/ep2-fundamentals/environment-variables.md new file mode 100644 index 0000000..39f0cc6 --- /dev/null +++ b/sfcasts/ep2-fundamentals/environment-variables.md @@ -0,0 +1,146 @@ +# Environment Variables + +Open `config/packages/framework.yaml`. We don't need to be authenticated to use this +raw user content part of GitHub's API: + +[[[ code('d0e2649747') ]]] + +But if we hit this endpoint a lot, we *might* hit their rate-limiting, which is pretty low +for anonymous users. So let's *authenticate* our request. + +## Adding an Authorization Header to the HTTP Request + +First, if you're coding along with me, head to "github.com" and create your own +personal access token. Once you've done that, open up `MixRepository` and go down +to where we make the HTTP request. To attach the access token to the request pass +a *third* argument, which is an array. Inside, add a `headers` key set to another +array, with an `Authorization` header assigned to the word `Token` and then the +access token. Start by using a fake token: + +[[[ code('9cb7084ddc') ]]] + +You can tell this is working because, when we go back over to the page and +refresh... it explodes! Our API call now *fails* with a 404 because it recognizes +that we're *trying* to authenticate with a token... but the one we passed is +*bogus*. + +Now add your *real* token. Try it again and... it works! + +## Moving Authorization Header to framework.yaml + +So this is cool! *But* it would be nicer if the service came preconfigured to +*automatically* set this authorization header... especially if we want to use this +HTTP Client service in multiple places. Can we do that? You bet! + +Copy the `Token` line, head into `framework.yaml`, and after `base_uri`, pass +a `headers` key with `Authorization` set to our long string. Actually, let me put a +*fake* token in there temporarily: + +[[[ code('63d8ced9e1') ]]] + +Back in `MixRepository`, remove that third argument: + +[[[ code('614d5f78d4') ]]] + +And now, when we try this... great! Things are broken, which proves we're sending +that header... just with the wrong value. If we change to our *real* token... once +again... it works! Awesome! + +## Hello Environment Variables + +So far, this is just a nice feature of the HttpClient. But this also helps highlight +a common problem. It's... not super great to have our sensitive GitHub API token +hardcoded in this file. I mean, this file is going to be committed to our +repository. I love my teammates... but I don't love them *so* much that I want to +share my access token to with them... or the access token for our company. + +This is where *environment variables* come in handy. If you're not familiar with +environment variables, they're variables that you can set on any system (Windows, +Linux, whatever.)... and then you can read them from inside of PHP. Many hosting +platforms make it super easy to set these. How does that help us? Because, in theory, +we could set our access token as an *environment* variable then simply *read* it +in PHP. That would let us *avoid* putting that sensitive value *inside* our code. + +## Reading Environment Variables + +But, before we talk about *setting* environment variables, how do we +*read* environment variables in Symfony? Copy your access token so you don't lose +it, put single quotes around `Token`, and then we're going to use a very special +syntax to *read* an environment variable. It's actually going to look like a +parameter. Start and end with `%`, and inside, say `env()` with the name of the +environment variable. How about `GITHUB_TOKEN`. I just made that name up: + +[[[ code('812eee7285') ]]] + +If we head back and refresh... we are now *reading* that `GITHUB_TOKEN` environment +variable... but we haven't *set* it yet, so we get this "Environment variable not +found" error. + +## Setting Environment Variables & .env + +In the real world, setting environment variables is... actually kind of tricky. It's +different on Windows versus Linux. And while many *hosting* platforms *do* make it +super easy to set environment variables, it's not very simple to do *locally* on +your computer. + +*That* is why this `.env` file exists. Very simply, when Symfony boots up, it reads +the `.env` file and turns all of these into environment variables. This means we +can say `GITHUB_TOKEN=` and paste our token... and now... it works! + +[[[ code('6de74e6cd7') ]]] + +By the way, if there were a *real* `GITHUB_TOKEN` environment variable set on my +system that real environment variable would win over what we have in this file. + +## The .env.local File + +Okay... this is cool... but we *still* have the same problem! We have a sensitive +value that's inside of a file... which *is* committed to our repository. + +Ok, then, let's try something else. Copy the GitHub token, delete the value from +this file, and then create a new file called `.env.local`. Set the environment +variable *here*. + +And now... things *still* work! + +Here's the deal. When Symfony boots up, it first reads the `.env` file and turns +all of these into environment variables. *Then* it reads `.env.local` and turns +anything in *here* into environment variables... which *override* any values set +in `.env`. + +The result is that your `.env` file is meant to hold safe, default values that are +ok to be committed to your repository. Then locally, (and maybe also on production, +depending on how you deploy), you create a `.env.local` file and put the sensitive +values there. The *key* thing is that `.env.local` is *ignored* by Git. You can +see it's already in our `.gitignore` file. So while this file *will* contain +sensitive values, it will *not* be committed to the repository. + +There *are* a few other `.env` files that you can create... and you can see them +mentioned here. They're not as important, but if you want to read about them, you +can check out the documentation. + +## Visualizing Env Vars with debug:dotenv + +Another cool thing about environment variables is that you can visualize them by +running: + +```terminal +php bin/console debug:dotenv +``` + +Sweet! You can see the current value of `GITHUB_TOKEN`... and that this value +is also set in `.env.local`. In contrast, `APP_ENV` and `APP_SECRET` have `n/a` +here, meaning their values are *not* being overridden in `.env.local`. It also +tells us which `.env` files it detected. + +## Env Var Processors + +There are a few tricks you can use with environment variables. For example, +there's something called a "processor system" where you could use `trim` to "trim" +the white space on `GITHUB_TOKEN`. *Or* you could use `file` where the `GITHUB_TOKEN` +variable is actually a path to a file that contains the true value. Anyways, these +are called "env var processors" if you want to read more about them. + +Next, let's talk quickly about deployment... but even more about how we can safely +store these sensitive values when you deploy to production. One option is Symfony's +secrets vault. diff --git a/sfcasts/ep2-fundamentals/environments.md b/sfcasts/ep2-fundamentals/environments.md new file mode 100644 index 0000000..4364e60 --- /dev/null +++ b/sfcasts/ep2-fundamentals/environments.md @@ -0,0 +1,147 @@ +# Environments + +Our application is like a machine: it's a set of services and PHP classes that do +work... and ultimately render some pages. But we can make our machine work +*differently* by feeding it different *configuration*. + +For example, in `SongController`, we're using the `$logger` service to log some +information: + +[[[ code('ec32177521') ]]] + +If we feed the logger some configuration that says "log everything", +it will log *everything*, including low level debug messages. But if we change the +config to say "only log errors", then this will *only* log errors. In other words, +the same machine can behave *differently* based on our configuration. And sometimes, +like with logging, we might need that configuration to be different while we're +developing locally versus on production. + +To handle this, Symfony has an important concept called "environments". I don't mean +environments like local vs staging vs beta vs production. A Symfony environment +is a *set* of configuration. + +For example, you can run your code in the `dev` environment with a set of +config that's designed for development. Or you can run your app in the `prod` +environment with a set of config that's optimized for production. Let me show you! + +## The APP_ENV Variable + +In the root of our project, we have a `.env` file: + +[[[ code('40c34e875b') ]]] + +We're going to talk more about this file later. But see this `APP_ENV=dev`? +This tells Symfony that the current environment is `dev`, which is *perfect* +for local development. When we deploy to production, we'll change this to `prod`. +More on that in a few minutes. + +But... what *difference* does that make? What happens in our app when we change this +from `dev` to `prod`? To answer, let me close some folders... and open +`public/index.php`: + +[[[ code('7e152d62fa') ]]] + +Remember: this is our front controller. It's the first file +that's executed on every request. We don't really care much about this file, but +its job is important: it boots up Symfony. + +What's interesting is that it *reads* the `APP_ENV` value and passes it as the +first argument to this `Kernel` class. And... this `Kernel` class is actually *in* +our code! It lives at `src/Kernel.php`. + +Cool. So what I want to know *now* is: What does the first argument to `Kernel` +control? + +If we open the class we find... absolutely *nothing*. It's empty. That's +because the majority of the logic lives in this trait. Hold "cmd" or "control" and +click `MicroKernelTrait` to open that up. + +## The config/packages/{ENV} Directory + +The job of the `Kernel` is to load all of the services and routes in our app. +If you scroll down, it has a method called `configureContainer()`. Ooh! We know +what the container is now! And check out what it does! It takes this `$container` +object and imports `$configDir.'/{packages}/*.{php,yaml}'`. This line says: + +> Yo container! I want to load all of the files from the `config/packages/` directory. + +It loads all of those files, and then it passes the configuration from each to +whatever bundle is defined as the root key. But what's *really* interesting for +environments is this next line: `import` +`$configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'`. If you +dug a little, you'd learn that `$this->environment` is equal to the first argument +that's passed to `Kernel`! + +In other words, in the `dev` environment, this will be `dev`. So, in addition +to the *main* config files, this will *also* load anything in the +`config/packages/dev/` directory. Yup, we can add *extra* config there +that *overrides* the main configuration in the `dev` environment. For example, we +could add logging config that tells the logger to log *everything*! + +Below this, we also load a file called `services.yaml` and, if we have it, +`services_dev.yaml`. We'll talk more about `services.yaml` real soon. + +## The when@{ENV} Config + +So, if you want to add environment-specific configuration, you can put it in the +correct environment directory. But there's one *other* way. It's a pretty new feature +and we saw it at the bottom of `twig.yaml`. It's the `when@` syntax: + +[[[ code('c2bc8660e5') ]]] + +In Symfony, by default, there are *three* environments: `dev`, `prod`, +and then if you run automated tests, there's an environment called `test`. Inside +of `twig.yaml`, by saying, `when@test`, it means that this configuration will +only be loaded if the environment is equal to `test`. + +The best example of this might be in `monolog.yaml`. `monolog` is the bundle +that controls the logger service. It *does* have some configuration that's used in +*all* environments. But, below this, it has `when@dev`. We won't talk too much +about the specific `monolog` configuration, but this controls how log messages are +handled. In the `dev` environment, this says that it should log *everything* and it +should log to a file, using this fancy `%kernel.logs_dir%` syntax that we'll learn +about soon. + +Anyways, this points to a `var/logs/dev.log` file and the `level: debug` part means +that it will log *every* single message to `dev.log`... regardless of how important +or unimportant that message is. + +Below this, for the `prod` environment, it's quite different. The most important +line is `action_level: error`. That says: + +> Hi Ms Logger! This app probably logs a *ton* of messages, but I only want you +> to actually *save* messages that are an `error` importance level or higher. + +That makes sense! In production, we don't want our log files filling up with tons +and tons of debug messages. With this, we only log *error* messages. + +The big point is this: by using these tricks, we can configure our services +differently based on the environment. + +## Environment-Specific Routing + +And, we can even do the same thing with routes! Sometimes you have entire routes +that you only want to load in a certain environment. Back in `MicroKernelTrait`, +if you go down, there's a method called `configureRoutes()`. *This* is what's +responsible for loading *all* of our routes... and it's very similar to the other +code. It loads `$configDir.'/{routes}/*.{php,yaml}'` as well as this `dev` +environment directory, if you have one. We don't. + +You can also use the `when@dev` trick. This file is responsible for registering +the routes used by the web debug toolbar. We *don't want* the web debug toolbar +in production... so these routes are *only* imported in the `dev` environment. + +[[[ code('c32be9baf6') ]]] + +Heck, certain *bundles* are only enabled in some environments! If you open +`config/bundles.php`, we have the name of the bundle... and then on the right, +the environments in which that bundle should be enabled. This `all` means *all* +environments.... and *most* are enabled in *all* environments. + +The `WebProfilerBundle` however - the bundle that gives us the web debug toolbar +and profiler - is *only* loaded in the `dev` and `test` environments. Yup, the entire +bundle - and the services it provides - are *never* loaded in the `prod` environment. + +So, now that we understand the basics of environments, let's see if we can switch our +application to the `prod` environment. And then, as a challenge, we'll configure our +cache service *differently* in `dev`. That's next. diff --git a/sfcasts/ep2-fundamentals/es/bundle-config.md b/sfcasts/ep2-fundamentals/es/bundle-config.md new file mode 100644 index 0000000..ddbc7a4 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundle-config.md @@ -0,0 +1,58 @@ +# Bundle Config (para controlar los servicios de bundle) + +Ahora utilizamos los servicios `HttpClientInterface` y `CacheInterface`. ¡Sí! Pero en realidad no somos responsables de instanciar estos objetos de servicio. No, los crea otra cosa (hablaremos de ello en unos minutos), y luego nos los pasa. + +Eso está muy bien, porque todos estos servicios -las "herramientas" de nuestra aplicación- vienen listos para usar, listos para usar. Pero... si otra cosa se encarga de instanciar estos objetos de servicio, ¿cómo podemos controlarlos? + +Presentamos: ¡la configuración del bundle! + +## Configuración del bundle + +Ve a ver el directorio `config/packages/`. Este tiene un número de diferentes archivos YAML, todos los cuales son cargados automáticamente por Symfony cuando se inicia por primera vez. Todos estos archivos tienen exactamente un propósito: configurar los servicios que nos proporciona cada bundle. + +Abre `twig.yaml`. Por ahora, ignora este `when@test`: hablaremos de ello en unos minutos. Este archivo tiene una clave raíz llamada `twig`. Y, por tanto, todo el propósito de este archivo es controlar los servicios que proporciona el bundle "Twig". Y, no es el nombre del archivo - `twig.yaml` - lo que es importante. Podría renombrar esto como`pineapple_pizza.yaml` y funcionaría exactamente igual y sería delicioso, no me importa lo que pienses. + +Cuando Symfony carga este archivo, ve esta clave raíz - `twig` - y dice: + +> Oh, vale. Voy a pasar la configuración que haya debajo a TwigBundle. + +¡Y recuerda! Los bundles nos dan servicios. Gracias a esta configuración, cuando TwigBundle está preparando sus servicios, Symfony le pasa esta configuración y TwigBundle la utiliza para decidir cómo deben instanciarse sus servicios... como qué nombres de clase usar para cada servicio... o qué primer segundo o tercer argumento del constructor pasar. + +Por ejemplo, si cambiáramos el `default_path` por algo como`%kernel.project_dir%/views`, el resultado es que el servicio Twig que genera plantillas estaría ahora preconfigurado para buscar en ese directorio. + +La cuestión es: la configuración de estos archivos nos da el poder de controlar los servicios que proporciona cada bundle. + +Veamos otro: `framework.yaml`. Como la clave raíz es`framework`, toda esta configuración se pasa a FrameworkBundle... que la utiliza para configurar los servicios que proporciona. + +Y, como he mencionado, el nombre del archivo no importa... aunque el nombre suele coincidir con la clave raíz... sólo por razones de cordura: como `framework` y `framework.yaml`. Pero no siempre es así. Abre `cache.yaml`. ¡Woh! Esto es... ¡más configuración para FrameworkBundle! Vive en su propio archivo... sólo porque es bueno tener un archivo separado para controlar la caché. + +## Depuración de la configuración del bundle disponible + +Llegados a este punto, puede que te preguntes: + +> Vale, genial... pero ¿qué claves de configuración podemos poner aquí? ¿Dónde puedo +> encontrar qué opciones están disponibles? + +¡Gran pregunta! Porque... no puedes "inventar" las claves que quieras: eso daría un error. En primer lugar, sí, puedes, por supuesto, leer la documentación. Pero hay otra manera: y es una de mis cosas favoritas del sistema de configuración de Symfony. + +Si quieres saber qué configuración puedes pasar al bundle "Twig", hay dos comandos de`bin/console` que te ayudarán. El primero es: + +```terminal +php bin/console debug:config twig +``` + +Esto imprimirá toda la configuración actual bajo la clave `twig`, incluyendo cualquier valor por defecto que el bundle esté añadiendo. Puedes ver que nuestro`default_path` está configurado en el directorio `templates/`, que proviene de nuestro archivo de configuración. Este `%kernel.project_dir%` es sólo una forma elegante de apuntar a la raíz de nuestro proyecto. Más adelante hablaremos de ello. + +Prueba esto: cambia el valor a `views`, vuelve a ejecutar ese comando y... ¡sí! Vemos "views" en la salida. Déjame que vuelva a cambiarlo. + +Así que `debug:config` nos muestra toda la configuración actual de un bundle específico, como `twig`... lo que es especialmente útil porque también te muestra los valores predeterminados añadidos por el bundle. Es una buena manera de ver lo que puedes configurar. Por ejemplo, ¡aparentemente podemos añadir una variable global a Twig mediante esta clave `globals`! + +El segundo comando es similar: en lugar de `debug:config`, es `config:dump`: + +```terminal +php bin/console config:dump twig +``` + +`debug:config` te muestra la configuración actual... pero `config:dump`te muestra un árbol gigante de configuración de ejemplo, que incluye todo lo que es posible. Aquí puedes ver `globals` con algunos ejemplos de cómo podrías utilizar esa tecla. Esta es una gran manera de ver todas las opciones potenciales que puedes pasar a un bundle... para ayudarle a configurar sus servicios. + +Utilicemos este nuevo conocimiento para ver si podemos "enseñar" al servicio de caché a almacenar sus archivos en otro lugar. Eso a continuación. diff --git a/sfcasts/ep2-fundamentals/es/bundle-config.vtt b/sfcasts/ep2-fundamentals/es/bundle-config.vtt new file mode 100644 index 0000000..33f3520 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundle-config.vtt @@ -0,0 +1,304 @@ +WEBVTT + +00:00:01.016 --> 00:00:06.066 align:middle +Ahora utilizamos los servicios +HttpClientInterface y CacheInterface. + +00:00:06.436 --> 00:00:14.476 align:middle +¡Sí! Pero en realidad no somos responsables +de instanciar estos objetos de servicio. + +00:00:15.176 --> 00:00:19.386 align:middle +No, son creados por otra +cosa (hablaremos de ello + +00:00:19.386 --> 00:00:22.836 align:middle +en unos minutos), y luego se nos pasan. + +00:00:23.546 --> 00:00:28.566 align:middle +Eso está muy bien, porque todos estos servicios +-las "herramientas" de nuestra aplicación- + +00:00:28.886 --> 00:00:31.556 align:middle +vienen listos para usar, listos para usar. + +00:00:32.306 --> 00:00:35.026 align:middle +Pero... si otra cosa se encarga + +00:00:35.026 --> 00:00:39.996 align:middle +de instanciar estos objetos de +servicio, ¿cómo podemos controlarlos? + +00:00:40.706 --> 00:00:43.566 align:middle +Presentamos: ¡la configuración del bundle! + +00:00:44.316 --> 00:00:46.886 align:middle +Echa un vistazo al directorio config/packages/. + +00:00:47.446 --> 00:00:52.916 align:middle +Este contiene varios archivos YAML diferentes, +todos los cuales son cargados automáticamente + +00:00:52.916 --> 00:00:55.396 align:middle +por Symfony cuando arranca por primera vez. + +00:00:56.206 --> 00:00:59.746 align:middle +Todos estos archivos tienen +exactamente un propósito + +00:01:00.286 --> 00:01:04.756 align:middle +configurar los servicios que +nos proporciona cada bundle. + +00:01:05.546 --> 00:01:07.256 align:middle +Abre twig.yaml. + +00:01:07.916 --> 00:01:12.656 align:middle +Por ahora, ignora este when@test: +hablaremos de ello en unos minutos. + +00:01:13.516 --> 00:01:16.446 align:middle +Este archivo tiene una clave +raíz llamada twig . Y así, + +00:01:17.016 --> 00:01:20.756 align:middle +todo el propósito de este archivo es + +00:01:20.756 --> 00:01:25.146 align:middle +controlar los servicios que +proporciona el bundle " Twig". + +00:01:26.076 --> 00:01:30.366 align:middle +Y lo importante no es el nombre +del archivo - twig.yaml -. + +00:01:30.876 --> 00:01:38.986 align:middle +Podría cambiar el nombre a pineapple_pizza.yaml y +funcionaría exactamente igual y sería delicioso. + +00:01:39.406 --> 00:01:40.486 align:middle +No me importa lo que pienses. + +00:01:41.246 --> 00:01:47.566 align:middle +Cuando Symfony carga este archivo, ve esta +clave raíz - twig - y dice: Oh, vale. + +00:01:47.566 --> 00:01:53.016 align:middle +Voy a pasar la configuración +que haya debajo a TwigBundle. + +00:01:53.666 --> 00:01:54.556 align:middle +¡Y recuerda! + +00:01:54.886 --> 00:01:57.026 align:middle +Los bundles nos dan servicios. + +00:01:57.696 --> 00:02:02.276 align:middle +Gracias a esta configuración, cuando +TwigBundle está preparando sus servicios, + +00:02:02.676 --> 00:02:07.206 align:middle +Symfony le pasa esta configuración +y TwigBundle la utiliza + +00:02:07.356 --> 00:02:11.636 align:middle +para decidir cómo deben +instanciarse sus servicios... + +00:02:12.246 --> 00:02:15.056 align:middle +como qué nombres de clase +utilizar para cada servicio... + +00:02:15.386 --> 00:02:19.806 align:middle +o qué primer segundo o tercer +argumento del constructor pasar. + +00:02:20.446 --> 00:02:28.066 align:middle +Por ejemplo, si cambiáramos el default_path +por algo como %kernel.project_dir%/views, el + +00:02:28.556 --> 00:02:34.466 align:middle +resultado es que el servicio Twig que genera +plantillas estaría ahora preconfigurado + +00:02:34.556 --> 00:02:36.496 align:middle +para buscar en ese directorio. La cuestión + +00:02:37.276 --> 00:02:41.566 align:middle +es: la configuración de +estos archivos nos da el poder + +00:02:41.566 --> 00:02:45.296 align:middle +de controlar los servicios +que proporciona cada b undle. + +00:02:46.176 --> 00:02:48.756 align:middle +Veamos otro: framework.yaml. + +00:02:49.446 --> 00:02:55.086 align:middle +Como la clave raíz es framework, toda esta +configuración se pasa a FrameworkBundle... + +00:02:55.636 --> 00:02:58.926 align:middle +que lo utiliza para configurar +los servicios que proporciona. + +00:02:59.576 --> 00:03:03.086 align:middle +Y, como he mencionado, el +nombre del archivo no importa... + +00:03:03.476 --> 00:03:06.726 align:middle +aunque el nombre suele +coincidir con la clave raíz... + +00:03:06.996 --> 00:03:11.486 align:middle +sólo por razones de cordura: +como framework y framework.yaml. + +00:03:12.346 --> 00:03:14.646 align:middle +Pero no siempre es así. + +00:03:15.196 --> 00:03:16.686 align:middle +Abre cache.yaml. + +00:03:17.406 --> 00:03:18.766 align:middle +¡Woh! Esto es... + +00:03:19.096 --> 00:03:21.956 align:middle +¡más configuración para FrameworkBundle! + +00:03:22.596 --> 00:03:24.216 align:middle +Vive en su propio archivo... + +00:03:24.486 --> 00:03:28.786 align:middle +sólo porque es bueno tener un archivo +separado para controlar la caché. + +00:03:29.546 --> 00:03:34.616 align:middle +Llegados a este punto, puede +que te preguntes Vale, genial... + +00:03:35.086 --> 00:03:38.926 align:middle +¿pero qué claves de +configuración podemos poner aquí? + +00:03:39.406 --> 00:03:42.386 align:middle +¿Dónde puedo encontrar +las opciones disponibles? + +00:03:42.996 --> 00:03:43.816 align:middle +¡Gran pregunta! + +00:03:44.176 --> 00:03:44.646 align:middle +Porque... + +00:03:44.846 --> 00:03:50.326 align:middle +no puedes "inventarte" las claves +que quieras: eso daría un error. + +00:03:51.076 --> 00:03:55.456 align:middle +En primer lugar, sí, puedes, por +supuesto, leer la documentación. + +00:03:56.046 --> 00:04:01.876 align:middle +Pero hay otra manera: y es una de mis cosas +favoritas del sistema de configuración de Symfony. + +00:04:02.556 --> 00:04:06.046 align:middle +Si quieres saber qué configuración +puedes pasar al bundle " Twig", + +00:04:06.446 --> 00:04:09.346 align:middle +hay dos comandos de +bin/console que te ayudarán. + +00:04:09.906 --> 00:04:15.906 align:middle +El primero es: php bin/console +debug:config twig Esto imprimirá toda + +00:04:15.906 --> 00:04:19.376 align:middle +la configuración actual bajo la clave twig, + +00:04:19.786 --> 00:04:24.186 align:middle +incluyendo cualquier valor por defecto +que el bundle esté añadiendo. + +00:04:24.846 --> 00:04:28.546 align:middle +Puedes ver que nuestro default_path está +configurado en el directorio templates/, + +00:04:28.976 --> 00:04:30.816 align:middle +que proviene de nuestro +archivo de configuración. + +00:04:31.646 --> 00:04:38.886 align:middle +Este %kernel.project_dir% es sólo una forma +elegante de apuntar a la raíz de nuestro proyecto. + +00:04:39.246 --> 00:04:40.776 align:middle +Más adelante hablaremos de ello. + +00:04:41.676 --> 00:04:47.356 align:middle +Prueba esto: cambia el valor a views, +vuelve a ejecutar ese comando y... + +00:04:47.806 --> 00:04:50.496 align:middle +¡sí! Vemos "views" en la salida. + +00:04:51.246 --> 00:04:53.226 align:middle +Déjame que vuelva a cambiarlo. + +00:04:54.066 --> 00:04:57.496 align:middle +Así, debug:config nos muestra toda la + +00:04:57.606 --> 00:05:01.116 align:middle +configuración actual de un +bundle específico, como twig... + +00:05:01.556 --> 00:05:07.726 align:middle +lo cual es especialmente útil porque también te muestra +los valores predeterminados añadidos por el bundle. + +00:05:08.316 --> 00:05:11.556 align:middle +Es una buena manera de ver +lo que puedes configurar. + +00:05:12.376 --> 00:05:18.956 align:middle +Por ejemplo, ¡aparentemente podemos añadir una +variable global a Twig mediante esta clave globals! + +00:05:19.846 --> 00:05:25.276 align:middle +El segundo comando es similar: en +lugar de debug:config, es config:dump: + +00:05:25.276 --> 00:05:30.646 align:middle +php bin/console config:dump twig debug:config +te muestra la configuración actual... + +00:05:30.976 --> 00:05:36.516 align:middle +pero config:dump te muestra un árbol +gigante de configuración de ejemplo, + +00:05:37.016 --> 00:05:39.866 align:middle +que incluye todo lo que es posible. + +00:05:40.706 --> 00:05:45.656 align:middle +Aquí puedes ver globals con algunos ejemplos +de cómo podrías utilizar esa tecla. + +00:05:46.406 --> 00:05:51.156 align:middle +Esta es una gran manera de ver todas las opciones +potenciales que puedes pasar a un bundle... + +00:05:51.576 --> 00:05:53.616 align:middle +para ayudarle a configurar sus servicios. + +00:05:54.316 --> 00:05:58.856 align:middle +Utilicemos este nuevo conocimiento para ver +si podemos "enseñar" al servicio de caché + +00:05:58.916 --> 00:06:00.846 align:middle +a almacenar sus archivos en otro lugar. + +00:06:01.216 --> 00:06:02.096 align:middle +Eso a continuación diff --git a/sfcasts/ep2-fundamentals/es/bundle-services.md b/sfcasts/ep2-fundamentals/es/bundle-services.md new file mode 100644 index 0000000..41bf098 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundle-services.md @@ -0,0 +1,84 @@ +# Encontrar y Utilizar los Servicios de un Bundle + +Acabamos de instalar KnpTimeBundle. ¡Hurra! Pero... eh... ¿qué significa eso? ¿Qué nos da eso? + +Lo primero que nos da un bundle son... ¡servicios! ¿Qué servicios nos da este bundle? Bueno, podríamos, por supuesto, leer la documentación, bla, bla. Bueno, vale, deberías hacerlo... pero, ¡vamos! ¡Aventurémonos con temeridad y aprendamos explorando! + +En el último tutorial, conocimos un comando que nos muestra todos los servicios de nuestra aplicación: `debug:autowiring`: + +```terminal-silent +php bin/console debug:autowiring +``` + +Por ejemplo, si buscamos "logger", parece que hay un servicio llamado`LoggerInterface`. También aprendimos que podemos autoconectar cualquier servicio de esta lista en nuestro controlador utilizando su tipo. Usando este tipo `LoggerInterface` -que en realidad es `Psr\Log\LoggerInterface` - Symfony sabe que debe pasarnos este servicio. Luego, aquí abajo, llamamos a métodos sobre él como `$logger->info()`. + +Hemos instalado `KnpTimeBundle` hace un momento, así que busquemos "tiempo": + +```terminal-silent +php bin/console debug:autowiring time +``` + +Y... ¡eh! ¡Mira esto! ¡Tenemos un nuevo servicio `DateTimeFormatter`! Es del nuevo bundle y seguro que es lo que buscamos. Vamos a utilizarlo en nuestro controlador. + +## Utilizar el nuevo servicio DateTimeFormatter + +La pista de tipo que necesitamos es `Knp\Bundle\TimeBundle\DateTimeFormatter`. De acuerdo En`VinylController`, busca `browse()`, y añade el nuevo argumento. + +Por cierto, el orden de los argumentos no importa... excepto cuando se trata de argumentos opcionales. He hecho que el argumento `$slug` sea opcional y normalmente necesitas tus argumentos opcionales al final de la lista. Así que añadiré `DateTimeFormatter`justo aquí y pulsaré "tab" para añadir la declaración `use` en la parte superior. + +Podemos nombrar el argumento como queramos, como `$sherlockHolmes` o`$timeFormatter`: + +[[[ code('616397799e') ]]] + +Para usar esto, haz un bucle sobre las mezclas - `foreach ($mixes as $key => $mix)` + +[[[ code('7da483ab67') ]]] + +luego, en cada una, añade una nueva clave `ago`: `$mixes[$key]['ago'] =`... y aquí es donde necesitamos el nuevo servicio. ¿Cómo utilizamos el `DateTimeFormatter`? ¡No tengo ni idea! Pero hemos utilizado su tipo, así que PhpStorm debería decirnos qué métodos tiene. Escribe`$timeFormatter->`... y ¡bien! Tiene 4 métodos públicos. + +El que queremos es `formatDiff()`. Pásale el tiempo "desde"... que es`$mix['createdAt']`: + +[[[ code('85dac5f608') ]]] + +¡Eso es todo lo que necesitamos! Estamos haciendo un bucle sobre estos `$mixes`, tomando la clave `createdAt`, que es un objeto `DateTime`, pasándolo al método `formatDiff()`, que debería devolver una cadena con el formato "ago". Para ver si esto funciona, a continuación,`dd($mixes)`: + +[[[ code('ec67d50981') ]]] + +¡Vamos a probarlo! Gira, refresca... y abramos. ¡Sí! Mira esto: `"ago" +=> "7 months ago"`... `"ago" => "18 days ago"`... Funciona. Así que elimina ese volcado: + +[[[ code('07e24f0499') ]]] + +Y ahora que cada mezcla tiene un nuevo campo `ago`, en `browse.html.twig`, sustituye el código`mix.createdAt|date` por `mix.ago`: + +[[[ code('47f88f8537') ]]] + +Y ahora... mucho mejor. + +Así que: teníamos un problema... y sabíamos que había que resolverlo con un servicio... porque los servicios sí funcionan. Todavía no teníamos un servicio que hiciera lo que necesitábamos, así que salimos, encontramos uno y lo instalamos. ¡Problema resuelto! El propio Symfony tiene un montón de paquetes diferentes, y cada uno de ellos nos proporciona varios servicios. Pero a veces necesitarás un bundle de terceros como éste para hacer el trabajo. Normalmente, puedes buscar en Internet el problema que intentas resolver, más "Symfony bundle", para encontrarlo. + +## Utilizar el filtro Twig de hace años + +Además del bonito servicio `DateTimeFormatter` que acabamos de utilizar, este bundle también nos proporcionó otro servicio. Pero no es un servicio que debamos utilizar directamente, como en el controlador. No Este servicio está destinado a ser utilizado por el propio Twig... ¡para alimentar un nuevo filtro Twig! ¡Así es! Puedes añadir funciones personalizadas, filtros... o cualquier cosa a Twig. + +Para ver el nuevo filtro, vamos a probar otro útil comando de depuración: + +```terminal +php bin/console debug:twig +``` + +Esto imprime una lista de todas las funciones, filtros y pruebas de Twig, junto con la única variable global de Twig que tenemos. Si subes a Filtros, ¡hay uno nuevo llamado "hace"! Eso no estaba allí antes de que instaláramos `KnpTimeBundle`. + +Así que todo el trabajo que hicimos en nuestro controlador está perfectamente bien ... pero resulta que hay una manera más fácil de hacer todo esto. Elimina el `foreach`... elimina el servicio `DateTimeFormatter`... y, aunque es opcional, limpia la declaración extra `use` de la parte superior: + +[[[ code('c1f3df9471') ]]] + +En `browse.html.twig`, ya no tenemos un campo `ago`... pero seguimos teniendo un campo`createdAt`. En lugar de canalizarlo en el filtro `date`, canalízalo en `ago`: + +[[[ code('754b969c0e') ]]] + +¡Eso es todo lo que necesitamos! Volvemos a la actualización del sitio y... obtenemos exactamente el mismo resultado. + +Por cierto, no lo haremos en este tutorial, pero al final, podrás seguir fácilmente la documentación para crear tus propias funciones y filtros Twig personalizados. + +Vale, nuestra aplicación aún no tiene una base de datos... y no la tendrá hasta el próximo episodio. Pero para hacer las cosas más interesantes, vamos a obtener los datos de nuestras mezclas haciendo una llamada HTTP a un repositorio especial de GitHub. Eso a continuación. \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/bundle-services.vtt b/sfcasts/ep2-fundamentals/es/bundle-services.vtt new file mode 100644 index 0000000..209f2ff --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundle-services.vtt @@ -0,0 +1,334 @@ +WEBVTT + +00:00:01.026 --> 00:00:03.876 align:middle +Acabamos de instalar KnpTimeBundle. + +00:00:04.106 --> 00:00:06.386 align:middle +¡Hurra! Um... + +00:00:06.546 --> 00:00:07.326 align:middle +pero... eh... + +00:00:07.606 --> 00:00:09.686 align:middle +¿qué significa eso? + +00:00:10.176 --> 00:00:12.616 align:middle +¿Qué nos da eso? + +00:00:13.176 --> 00:00:16.476 align:middle +Lo primero que nos da un bundle es... + +00:00:16.746 --> 00:00:20.856 align:middle +¡servicios! ¿Qué +servicios nos da este bundle? + +00:00:21.386 --> 00:00:26.486 align:middle +Bueno, podríamos, por supuesto, +leer la documentación, bla, bla. + +00:00:27.086 --> 00:00:28.896 align:middle +Bueno, vale, deberías hacerlo... + +00:00:29.136 --> 00:00:29.986 align:middle +pero, ¡vamos! + +00:00:30.246 --> 00:00:34.776 align:middle +¡Aventurémonos con temeridad +y aprendamos explorando! + +00:00:35.576 --> 00:00:41.356 align:middle +En el último tutorial, conocimos un comando que nos +muestra todos los servicios de nuestra aplicación: + +00:00:41.686 --> 00:00:46.686 align:middle +debug:autowiring: Por +ejemplo, si buscamos "logger", + +00:00:47.246 --> 00:00:50.736 align:middle +parece que hay un servicio +llamado LoggerInterface. + +00:00:51.346 --> 00:00:55.736 align:middle +También aprendimos que podemos autoconectar +cualquier servicio de esta lista + +00:00:55.836 --> 00:00:58.606 align:middle +en nuestro controlador utilizando su tipo. + +00:00:59.856 --> 00:01:06.486 align:middle +Utilizando este tipo LoggerInterface -que +en realidad es Psr\Log\LoggerInterface - + +00:01:06.986 --> 00:01:09.766 align:middle +Symfony sabe que debe pasarnos este servicio. + +00:01:10.346 --> 00:01:15.116 align:middle +Entonces, aquí abajo, llamamos a métodos +sobre él como $logger->info(). + +00:01:15.936 --> 00:01:22.296 align:middle +Hemos instalado KnpTimeBundle hace un +momento, así que busquemos "tiempo": Y... + +00:01:22.676 --> 00:01:24.076 align:middle +¡eh! ¡Mira esto! + +00:01:24.436 --> 00:01:27.586 align:middle +¡Tenemos un nuevo servicio DateTimeFormatter! + +00:01:28.036 --> 00:01:32.126 align:middle +Es del nuevo bundle y seguro +que es lo que buscamos. + +00:01:32.816 --> 00:01:34.756 align:middle +Vamos a utilizarlo en nuestro controlador. + +00:01:35.376 --> 00:01:41.186 align:middle +La pista de tipo que necesitamos es +Knp\Bundle\TimeBundle\DateTimeFormatter. + +00:01:41.776 --> 00:01:46.216 align:middle +Vale, en VinylController, busca +browse(), y añade el nuevo argumento. + +00:01:46.886 --> 00:01:51.026 align:middle +Por cierto, el orden de los +argumentos no importa... + +00:01:51.446 --> 00:01:54.496 align:middle +excepto cuando se trata +de argumentos opcionales. + +00:01:55.246 --> 00:01:57.306 align:middle +He hecho que el argumento $slug sea opcional + +00:01:57.716 --> 00:02:02.466 align:middle +y normalmente necesitas tus argumentos +opcionales al final de la lista. + +00:02:02.926 --> 00:02:08.676 align:middle +Así que añadiré DateTimeFormatter justo aquí y pulsaré +"tab" para añadir la declaración use en la parte superior. + +00:02:09.546 --> 00:02:17.316 align:middle +Podemos nombrar el argumento como queramos, como +$sherlockHolmes o $timeFormatter: Para usar esto, + +00:02:17.586 --> 00:02:25.266 align:middle +haz un bucle sobre las mezclas - foreach ($mixes +as $key => $mix): luego, en cada una, añade + +00:02:25.736 --> 00:02:32.346 align:middle +una nueva clave ago: $mixes[$key]['ago'] =... + +00:02:32.826 --> 00:02:35.316 align:middle +y aquí es donde necesitamos el nuevo servicio. + +00:02:36.016 --> 00:02:38.516 align:middle +¿Cómo utilizamos el DateTimeFormatter? + +00:02:39.046 --> 00:02:40.356 align:middle +¡No tengo ni idea! + +00:02:40.986 --> 00:02:46.516 align:middle +Pero hemos utilizado su tipo, así que +PhpStorm debería decirnos qué métodos tiene. + +00:02:47.306 --> 00:02:48.806 align:middle +Escribe $timeFormatter->... + +00:02:49.186 --> 00:02:52.486 align:middle +y ¡bien! Tiene 4 métodos públicos. + +00:02:52.986 --> 00:02:55.976 align:middle +El que queremos es formatDiff(). + +00:02:56.606 --> 00:02:57.916 align:middle +Pásale el tiempo "desde"... + +00:02:58.246 --> 00:03:03.356 align:middle +que es $mix['createdAt']: ¡Eso +es todo lo que necesitamos! + +00:03:04.086 --> 00:03:10.456 align:middle +Estamos haciendo un bucle sobre estos $mixes, tomando la +clave createdAt, que es un objeto DateTime, pasándolo + +00:03:10.886 --> 00:03:17.256 align:middle +al método formatDiff(), que debería +devolver una cadena con el formato "ago ". + +00:03:18.076 --> 00:03:22.636 align:middle +Para ver si esto funciona, a +continuación, dd($mixes): ¡Probemos! + +00:03:23.016 --> 00:03:24.586 align:middle +Gira, refresca... + +00:03:25.046 --> 00:03:26.696 align:middle +y abramos. + +00:03:27.286 --> 00:03:30.906 align:middle +¡Sí! Mira esto: "ago" => "7 months ago"... + +00:03:31.126 --> 00:03:33.056 align:middle +"ago" => "18 days ago"... + +00:03:33.306 --> 00:03:43.686 align:middle +Funciona. Así que elimina ese volcado: Y ahora que cada +mezcla tiene un nuevo campo ago, en browse.html.twig, + +00:03:44.416 --> 00:03:51.776 align:middle +sustituye el código mix.createdAt|date +por mix.ago: Y ahora... + +00:03:52.946 --> 00:03:54.156 align:middle +mucho mejor. + +00:03:54.816 --> 00:03:56.586 align:middle +Así que: teníamos un problema... + +00:03:56.936 --> 00:04:00.086 align:middle +y sabíamos que debía ser +resuelto por un servicio... + +00:04:00.336 --> 00:04:02.256 align:middle +porque los servicios sí funcionan. + +00:04:02.776 --> 00:04:05.876 align:middle +Todavía no teníamos un servicio +que hiciera lo que necesitábamos, + +00:04:06.386 --> 00:04:09.156 align:middle +así que salimos, encontramos +uno y lo instalamos. + +00:04:09.756 --> 00:04:11.166 align:middle +¡Problema resuelto! + +00:04:11.926 --> 00:04:17.746 align:middle +El propio Symfony tiene un montón de paquetes diferentes, +y cada uno de ellos nos proporciona varios servicios. + +00:04:18.286 --> 00:04:22.726 align:middle +Pero a veces necesitarás un bundle de +terceros como éste para hacer el trabajo. + +00:04:23.416 --> 00:04:27.076 align:middle +Normalmente, basta con buscar en Internet +el problema que intentas resolver, + +00:04:27.216 --> 00:04:29.586 align:middle +más "Symfony bundle", para encontrarlo. + +00:04:30.466 --> 00:04:34.386 align:middle +Además del bonito servicio +DateTimeFormatter que acabamos de utilizar, + +00:04:34.876 --> 00:04:38.096 align:middle +este bundle también nos +proporcionó otro servicio. + +00:04:38.816 --> 00:04:44.676 align:middle +Pero no es un servicio que debamos utilizar +directamente, como en el controlador. + +00:04:45.306 --> 00:04:50.316 align:middle +No Este servicio está destinado a +ser utilizado por el propio Twig... + +00:04:50.686 --> 00:04:54.086 align:middle +¡para alimentar un nuevo filtro Twig! + +00:04:54.676 --> 00:04:55.206 align:middle +¡Así es! + +00:04:55.476 --> 00:04:57.946 align:middle +Puedes añadir funciones +personalizadas, filtros... + +00:04:58.076 --> 00:04:59.616 align:middle +o cualquier cosa a Twig. + +00:05:00.386 --> 00:05:04.246 align:middle +Para ver el nuevo filtro, vamos a probar +otro útil comando de depuración: + +00:05:04.246 --> 00:05:11.496 align:middle +php bin/console debug:twig Esto imprime +una lista de todas las funciones, filtros + +00:05:11.616 --> 00:05:16.966 align:middle +y pruebas de Tw ig , junto con la única +variable global de Twig que tenemos. + +00:05:17.746 --> 00:05:22.326 align:middle +Si subes a Filtros, ¡hay +uno nuevo llamado "hace"! + +00:05:22.706 --> 00:05:26.316 align:middle +Eso no estaba allí antes de +que instaláramos KnpTimeBundle. + +00:05:26.916 --> 00:05:31.316 align:middle +Así que todo el trabajo que hicimos en +nuestro controlador está perfectamente bien... + +00:05:31.676 --> 00:05:35.876 align:middle +pero resulta que hay una forma +más fácil de hacer todo esto. + +00:05:36.616 --> 00:05:37.506 align:middle +Elimina el foreach... + +00:05:37.876 --> 00:05:40.016 align:middle +elimina el servicio DateTimeFormatter... + +00:05:40.656 --> 00:05:48.086 align:middle +y, aunque es opcional, limpia la declaración extra +use de la parte superior: En browse.html.twig, + +00:05:48.746 --> 00:05:50.886 align:middle +ya no tenemos un campo ago... + +00:05:51.386 --> 00:05:53.746 align:middle +pero seguimos teniendo un campo createdAt. + +00:05:54.616 --> 00:06:00.796 align:middle +En lugar de canalizarlo en el filtro date, +canalízalo en ago: ¡Eso es todo lo que necesitamos! + +00:06:01.406 --> 00:06:04.286 align:middle +Volvemos a la actualización del sitio y... + +00:06:04.646 --> 00:06:07.246 align:middle +obtenemos exactamente el mismo resultado. Por + +00:06:08.146 --> 00:06:12.776 align:middle +cierto, no lo haremos en este +tutorial, pero al final, podrás + +00:06:13.206 --> 00:06:16.276 align:middle +seguir fácilmente la documentación + +00:06:16.646 --> 00:06:19.606 align:middle +para crear tus propias funciones +y filtros Twig personalizados. + +00:06:20.416 --> 00:06:23.416 align:middle +Bien, nuestra aplicación aún +no tiene una base de datos... + +00:06:23.846 --> 00:06:26.266 align:middle +y no la tendrá hasta el próximo episodio. + +00:06:26.876 --> 00:06:30.526 align:middle +Pero para hacer las cosas más interesantes, +vamos a obtener los datos de nuestras mezclas + +00:06:30.916 --> 00:06:35.196 align:middle +haciendo una llamada HTTP a un +repositorio especial de GitHub. + +00:06:35.806 --> 00:06:36.786 align:middle +Eso a continuación diff --git a/sfcasts/ep2-fundamentals/es/bundles.md b/sfcasts/ep2-fundamentals/es/bundles.md new file mode 100644 index 0000000..c03b4c2 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundles.md @@ -0,0 +1,45 @@ +# ¡Bundles! + +¡Hola amigos! Bienvenidos de nuevo al Episodio 2 de nuestra serie de tutoriales sobre Symfony 6. Este es el episodio en el que subimos de nivel en serio y desbloqueamos nuestro potencial para hacer todo lo que queramos. Eso es porque, en este curso, nos sumergimos en los fundamentos detrás de todo en Symfony. Hablamos de servicios, bundles, configuración, entornos, variables de entorno - lo que realmente hace que Symfony funcione. Vamos a abrir el capó de Symfony y descubrir lo que hay dentro. + +## ¡Configuración del Sitio! + +Para sacar el máximo provecho de este tutorial de fundamentos, te invito a que te acerques al fuego, descargues el código del curso desde esta página y codifiques conmigo. ¡Será divertido! Después de descomprimir el archivo, encontrarás un directorio `start/` con el mismo código que ves aquí. Sigue nuestro archivo`README.md` hecho a mano y de origen local para obtener todas las instrucciones de configuración. El último paso será abrir un terminal, entrar en el proyecto y ejecutar + +```terminal +symfony serve -d +``` + +para iniciar un servidor web local en `https://127.0.0.1:8000`. Haré trampa y haré clic en ese enlace para ver nuestro sitio. Es... ¡Mixed Vinyl! Nuestra nueva startup en la que los usuarios pueden crear su propia "mixtape" personalizada -estoy pensando en MMMBop seguido de algo de las Spice Girls-, salvo que te la entregamos directamente en tu puerta en un disco de vinilo recién prensado. Incluso añadimos ese olor a vieja colección de discos de forma gratuita! + +## Servicios: Servicios en todas partes + +En el tutorial anterior, hablamos brevemente de que todo en Symfony se hace en realidad mediante un servicio. Y que la palabra "servicio" es un término elegante para un concepto sencillo: un servicio es un objeto que hace un trabajo. + +Por ejemplo, en `src/Controller/SongController.php`, aprovechamos el servicio Logger de Symfony para registrar un mensaje: + +[[[ code('0d6d809ebb') ]]] + +Y, aunque ya no tenemos el código en `VinylController`, utilizamos brevemente el servicio Twig para representar directamente una plantilla Twig: + +[[[ code('f65f8310e4') ]]] + +Así que un servicio no es más que un objeto que hace trabajo... y todo el trabajo que se hace en Symfony lo hace un servicio. Incluso el código central que calcula qué ruta coincide con la URL actual es un servicio, llamado servicio "router". + +## Hola Bundles + +Así que la siguiente pregunta es: ¿de dónde vienen estos servicios? La respuesta es Mordor. Me refiero a los bundles... los servicios vienen de los bundles. + +Abre `config/bundles.php`: + +[[[ code('796e719451') ]]] + +No es un archivo que tengas que mirar o preocuparte mucho, pero aquí es donde se activan tus bundles. + +Muy sencillo: los bundles son plugins de Symfony. No son más que código PHP... pero se enganchan a Symfony. Y gracias al sistema de recetas, cuando instalamos un nuevo bundle, ese bundle se añade automáticamente a este archivo, por lo que ya tenemos 8 bundles aquí. Cuando empezamos nuestro proyecto, ¡sólo teníamos 1! + +Así que un bundle es un plugin de Symfony. Y los bundles pueden darnos varias cosas... aunque en gran medida existen por una razón: darnos servicios. Por ejemplo, este TwigBundle de aquí arriba nos da el servicio Twig. Si elimináramos esta línea, el servicio Twig dejaría de existir y nuestra aplicación explotaría... ya que estamos renderizando plantillas. Esta línea `render()` dejaría de funcionar. Y MonologBundle es lo que nos da el servicio Logger que estamos utilizando en `SongController`. + +Así que al añadir más bundles a nuestra aplicación, estamos obteniendo más servicios, ¡y los servicios son herramientas! ¿Necesitas más servicios? ¡Instala más bundles! Es como Neo en la mejor, digo, primera película de Matrix. + +A continuación... vamos a enseñar a nuestra aplicación algo de Kung fu instalando un nuevo bundle que nos proporcione un nuevo servicio para resolver un nuevo problema. \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/bundles.vtt b/sfcasts/ep2-fundamentals/es/bundles.vtt new file mode 100644 index 0000000..56f1515 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/bundles.vtt @@ -0,0 +1,231 @@ +WEBVTT + +00:00:00.056 --> 00:00:03.546 align:middle +¡Hola amigos! + +00:00:03.846 --> 00:00:07.896 align:middle +Bienvenidos de nuevo al Episodio 2 de +nuestra serie de tutoriales sobre Symfony 6. + +00:00:08.436 --> 00:00:16.036 align:middle +Aquí es donde subimos de nivel en serio y desbloqueamos +nuestro potencial para hacer todo lo que queramos. + +00:00:16.746 --> 00:00:19.786 align:middle +Eso es porque, en este curso, nos sumergimos + +00:00:19.786 --> 00:00:23.446 align:middle +en los fundamentos detrás de todo en Symfony. + +00:00:23.776 --> 00:00:28.026 align:middle +Hablaremos de servicios, bundles +, configuración, entornos, + +00:00:28.186 --> 00:00:33.356 align:middle +variables de entorno: las cosas que +realmente hacen funcionar a Symfony. + +00:00:34.016 --> 00:00:38.436 align:middle +Vamos a abrir el capó de Symfony +y descubrir lo que hay dentro. + +00:00:39.246 --> 00:00:45.276 align:middle +Para sacar el máximo provecho de este +tutorial de fundamentos, te invito + +00:00:45.276 --> 00:00:51.186 align:middle +a que te acerques al fuego, descargues el código +del curso desde esta página y codifiques conmigo. + +00:00:51.676 --> 00:00:52.416 align:middle +¡Será divertido! + +00:00:53.136 --> 00:00:58.236 align:middle +Después de descomprimir el archivo, encontrarás un +directorio start/ con el mismo código que ves aquí. + +00:00:58.946 --> 00:01:03.456 align:middle +Sigue nuestro archivo README.md +hecho a mano y de origen local + +00:01:03.816 --> 00:01:05.796 align:middle +para obtener todas las +instrucciones de configuración. + +00:01:06.476 --> 00:01:13.056 align:middle +El último paso será abrir un terminal, entrar +en el proyecto y ejecutar symfony serve -d + +00:01:13.586 --> 00:01:19.156 align:middle +para iniciar un servidor web +local en https://127.0.0.1:8000. + +00:01:19.746 --> 00:01:23.316 align:middle +Haré trampa y haré clic en ese +enlace para ver nuestro sitio. + +00:01:23.876 --> 00:01:26.226 align:middle +Es... ¡Mixed Vinyl! + +00:01:26.676 --> 00:01:33.216 align:middle +Nuestra nueva startup en la que los usuarios pueden crear su +propia "mixtape" personalizada -estoy pensando en MMMBop seguido + +00:01:33.216 --> 00:01:37.606 align:middle +de algo de las Spice Girls-, salvo que te la +entregamos directamente en tu puerta en un + +00:01:37.606 --> 00:01:39.526 align:middle +disco de vinilo recién prensado. + +00:01:40.006 --> 00:01:43.806 align:middle +Incluso añadimos ese olor a vieja +colección de discos de forma gratuita + +00:01:44.846 --> 00:01:48.206 align:middle +En el tutorial anterior, +hablamos brevemente de cómo todo + +00:01:48.206 --> 00:01:51.026 align:middle +en Symfony se hace en +realidad mediante un servicio. + +00:01:51.526 --> 00:01:55.696 align:middle +Y que la palabra "servicio" es un +término elegante para un concepto simple: + +00:01:56.076 --> 00:01:59.196 align:middle +un servicio es un objeto +que hace un trabajo. Por + +00:02:00.116 --> 00:02:04.016 align:middle +ejemplo, en src/Controller/SongController.php, + +00:02:04.016 --> 00:02:08.106 align:middle +aprovechamos el servicio Logger de +Symfony para registrar un mensaje: + +00:02:08.806 --> 00:02:12.746 align:middle +Y, aunque ya no tenemos el +código en VinylController, + +00:02:13.146 --> 00:02:18.396 align:middle +utilizamos brevemente el servicio Twig para +representar directamente una plantilla Twig: + +00:02:19.176 --> 00:02:22.916 align:middle +Así que un servicio no es más +que un objeto que hace trabajo... + +00:02:23.336 --> 00:02:28.886 align:middle +y todo el trabajo que se hace +en Symfony lo hace un servicio. + +00:02:29.546 --> 00:02:32.036 align:middle +Incluso el código central que calcula + +00:02:32.166 --> 00:02:38.326 align:middle +qué ruta coincide con la URL actual es +un servicio, llamado servicio "router". + +00:02:39.076 --> 00:02:43.476 align:middle +Así que la siguiente pregunta es: +¿de dónde vienen estos servicios? + +00:02:44.476 --> 00:02:46.916 align:middle +La respuesta es: de Mordor. + +00:02:47.576 --> 00:02:48.666 align:middle +Me refiero a los bundles... + +00:02:48.836 --> 00:02:50.866 align:middle +los servicios provienen de bundles. + +00:02:51.506 --> 00:02:53.676 align:middle +Abre config/bundles.php: No es + +00:02:54.616 --> 00:02:58.616 align:middle +un archivo que tengas que +mirar o preocuparte mucho, + +00:02:59.106 --> 00:03:02.456 align:middle +pero aquí es donde se activan tus bundles. + +00:03:03.346 --> 00:03:06.886 align:middle +Muy sencillo: los bundles +son plugins de Symfony. + +00:03:07.316 --> 00:03:08.636 align:middle +No son más que código PHP... + +00:03:08.846 --> 00:03:10.496 align:middle +pero se enganchan a Symfony. + +00:03:11.186 --> 00:03:18.176 align:middle +Y gracias al sistema de recetas, cuando instalamos un +nuevo bundle, ese bundle se añade automáticamente + +00:03:18.176 --> 00:03:22.386 align:middle +a este archivo, por lo que +ya tenemos 8 bundles aquí. + +00:03:23.016 --> 00:03:25.726 align:middle +Cuando empezamos nuestro +proyecto, ¡sólo teníamos 1! + +00:03:26.586 --> 00:03:28.656 align:middle +Así que un bundle es un plugin de Symfony. + +00:03:29.166 --> 00:03:31.426 align:middle +Y los bundles pueden darnos varias cosas... + +00:03:31.776 --> 00:03:37.686 align:middle +aunque en gran medida existen +por una razón: darnos servicios. + +00:03:38.506 --> 00:03:43.186 align:middle +Por ejemplo, este TwigBundle de +aquí arriba nos da el servicio Twig. + +00:03:43.736 --> 00:03:47.846 align:middle +Si elimináramos esta línea, el +servicio Twig dejaría de existir + +00:03:48.296 --> 00:03:50.816 align:middle +y nuestra aplicación explotaría... + +00:03:51.276 --> 00:03:53.316 align:middle +ya que estamos renderizando plantillas. + +00:03:53.916 --> 00:03:56.516 align:middle +Esta línea render() dejaría de funcionar. + +00:03:58.106 --> 00:04:03.856 align:middle +Y MonologBundle es lo que nos da el servicio +Logger que estamos utilizando en SongController. + +00:04:04.586 --> 00:04:07.886 align:middle +Así que al añadir más bundles +a nuestra aplicación, estamos + +00:04:08.116 --> 00:04:12.906 align:middle +obteniendo más servicios, ¡y +los servicios son herramientas! + +00:04:13.516 --> 00:04:14.686 align:middle +¿Necesitas más servicios? + +00:04:14.936 --> 00:04:16.086 align:middle +¡Instala más bundles! + +00:04:16.586 --> 00:04:20.486 align:middle +Es como Neo en la mejor, digo, +primera película de Matrix. + +00:04:21.346 --> 00:04:25.946 align:middle +A continuación... vamos a enseñar a nuestra +aplicación algo de Kung fu instalando un nuevo bundle + +00:04:26.146 --> 00:04:30.336 align:middle +que nos proporcione un nuevo servicio +para resolver un nuevo problema diff --git a/sfcasts/ep2-fundamentals/es/cache-config.md b/sfcasts/ep2-fundamentals/es/cache-config.md new file mode 100644 index 0000000..79525b6 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/cache-config.md @@ -0,0 +1,51 @@ +# Configurar el servicio de caché + +Así que... quiero saber cómo puedo configurar el servicio de caché... como para almacenar la caché en otro lugar. En el mundo real, podemos simplemente buscar "Cómo configuro el servicio de caché de Symfony". Pero... también podemos averiguarlo por nuestra cuenta, utilizando los comandos que acabamos de aprender. + +Ya nos hemos dado cuenta de que hay un archivo `cache.yaml`. Parece que FrameworkBundle se encarga de crear el servicio de caché... y tiene una clave sub `cache` donde podemos pasar algunos valores para controlarlo. Todo esto está comentado por el momento. + +Para obtener más información sobre FrameworkBundle, ejecuta: + +```terminal +php bin/console config:dump framework +``` + +FrameworkBundle es el bundle principal dentro de Symfony. Así que puedes ver que esto vuelca... wow... una tonelada. FrameworkBundle proporciona muchos servicios... así que hay mucha configuración. + +## Depurando la configuración de la caché + +Para... ampliar un poco, vuelve a ejecutar el comando, pasando `framework` y luego`cache` para filtrar por esa subclave: + +```terminal-silent +php bin/console config:dump framework cache +``` + +Y... ¡genial! Puede que esto no sea siempre súper comprensible, pero es un gran punto de partida. Definitivamente, esto acaba de ayudarnos a responder a la pregunta + +> ¿Por qué el sistema de caché almacena cosas en el directorio var/cache? + +Porque... hay una clave `directory` que por defecto es `%kernel.cache_dir%`... que es una forma elegante de apuntar al directorio `/var/cache/dev`. Y luego vemos`/pools/app`, que es el directorio real que contiene nuestra caché. + +## Utilizando dump() y el perfilador + +Así que éste es el objetivo: en lugar de almacenar cosas en la caché del sistema de archivos, quiero cambiar el sistema de caché para almacenar en otro lugar. Antes de hacerlo, entra en`VinylController` y, para que podamos ver el resultado del cambio que vamos a hacer, en`dump($cache)`. Hasta ahora hemos utilizado `dd()`, que significa "volcar y morir". Pero en este caso quiero `dump()`... pero deja que la página se cargue. + +Actualiza ahora. Espera, ¿dónde está mi volcado? Esto es una... ¡función! Cuando utilices `dump()`, en realidad no lo verás en la página: se esconde aquí abajo, en la barra de herramientas de depuración de la web. Si miras allí, la caché es una especie de `TraceableAdapter`. Pero dentro de ella hay un objeto llamado `FilesystemAdapter`. Eso es una prueba de que el sistema de caché está guardando en el sistema de archivos. + +## Configurar el adaptador de caché + +Para hacer que se almacene en otro lugar, entra en `cache.yaml` y cambia esta clave `app`. Puedes configurarla con varias cadenas especiales diferentes, llamadas adaptadores. Si quisiéramos almacenar nuestra caché en Redis, utilizaríamos `cache.adapter.redis`. + +Para facilitar las cosas, utiliza `cache.adapter.array`. El adaptador `array` es una caché falsa en la que sí se almacenan cosas... pero sólo vive mientras dura la petición. Así que, al final de cada petición, se olvida de todo. Es una caché falsa, pero es suficiente para ver cómo el cambio de esta clave afectará al propio servicio de caché. + +Observa lo que ocurre. Actualmente, tenemos un `FilesystemAdapter`. Cuando refrescamos... ¡la caché es un `ArrayAdapter`! Y como el `ArrayAdapter` se olvida de su caché al final de la petición, puedes ver que cada petición hace ahora una petición HTTP. + +## Para llevar: Se trata de controlar cómo se instancian los servicios + +Si estás un poco confundido por esto, déjame intentar aclarar las cosas. El objetivo de este capítulo no es enseñarte a cambiar esta clave específica en el archivo de caché. En última instancia, si necesitas configurar algo en Symfony, simplemente buscarás en los documentos... que te dirán exactamente qué hacer y qué clave cambiar. + +No, la gran conclusión es que el único propósito de estos archivos de configuración es configurar los servicios de nuestra aplicación. Cada vez que cambias una clave en cualquiera de estos archivos, el resultado final es que acabas de cambiar la forma de instanciar algún servicio. Modificar una clave puede cambiar todo el nombre de la clase de un objeto de servicio, como en este caso, o puede cambiar el segundo o tercer argumento del constructor que se pasará cuando se instancie el servicio. En realidad no importa lo que cambie, siempre que te des cuenta de que esta configuración se refiere a los servicios y a cómo se instancian. + +De hecho, nada de esta configuración puede leerse directamente desde tu aplicación. No podrías, por ejemplo, pedir la configuración de la "caché" desde dentro de un controlador. No, Symfony lee esta configuración, la utiliza para configurar cómo se instanciará cada objeto de servicio, y luego la desecha. Los servicios son supremos. + +A continuación, a veces necesitarás que cierta configuración sea diferente en función de si estás desarrollando localmente o ejecutando en producción. Symfony tiene un sistema para esto llamado "entornos". Vamos a aprender todo sobre eso. diff --git a/sfcasts/ep2-fundamentals/es/cache-config.vtt b/sfcasts/ep2-fundamentals/es/cache-config.vtt new file mode 100644 index 0000000..3f0ee77 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/cache-config.vtt @@ -0,0 +1,284 @@ +WEBVTT + +00:00:01.016 --> 00:00:04.916 align:middle +Así que... quiero saber cómo puedo +configurar el servicio de caché... + +00:00:05.016 --> 00:00:07.436 align:middle +como almacenar la caché en otro lugar. + +00:00:08.316 --> 00:00:13.916 align:middle +En el mundo real, podemos simplemente buscar +"Cómo configuro el servicio de caché de Symfony". + +00:00:14.416 --> 00:00:21.156 align:middle +Pero... también podemos averiguarlo por nuestra cuenta, +utilizando los comandos que acabamos de aprender. + +00:00:21.816 --> 00:00:24.086 align:middle +Ya nos hemos dado cuenta de +que hay un archivo cache.yaml. + +00:00:24.086 --> 00:00:29.206 align:middle +Parece que FrameworkBundle es el responsable +de crear el servicio de caché... + +00:00:29.626 --> 00:00:34.606 align:middle +y tiene una clave sub cache donde podemos +pasar algunos valores para controlarlo. + +00:00:35.156 --> 00:00:37.216 align:middle +Todo esto está comentado por el momento. + +00:00:38.006 --> 00:00:40.746 align:middle +Para obtener más información +sobre FrameworkBundle, ejecuta: + +00:00:40.746 --> 00:00:46.476 align:middle +php bin/console config:dump framework +FrameworkBundle es el bundle principal dentro + +00:00:46.476 --> 00:00:47.046 align:middle +de Symfony. + +00:00:47.416 --> 00:00:50.096 align:middle +Así que puedes ver que esto vuelca... + +00:00:50.506 --> 00:00:52.246 align:middle +wow... una tonelada. + +00:00:53.056 --> 00:00:55.756 align:middle +FrameworkBundle proporciona +un montón de servicios... + +00:00:56.046 --> 00:00:57.666 align:middle +así que hay mucha configuración. + +00:00:58.316 --> 00:01:05.016 align:middle +Para... ampliar un poco, vuelve a ejecutar +el comando, pasando framework y luego cache + +00:01:05.266 --> 00:01:08.456 align:middle +para filtrar por esa subclave: Y... + +00:01:08.846 --> 00:01:14.036 align:middle +¡guay! Puede que esto no sea siempre súper +comprensible, pero es un gran punto de partida. + +00:01:14.806 --> 00:01:17.946 align:middle +Definitivamente, esto nos ha +ayudado a responder a la pregunta + +00:01:18.346 --> 00:01:22.726 align:middle +¿Por qué el sistema de caché almacena +cosas en el directorio var/cache? + +00:01:23.386 --> 00:01:24.346 align:middle +Porque... + +00:01:24.536 --> 00:01:30.296 align:middle +hay una clave directory que por +defecto es %kernel.cache_dir%... + +00:01:30.476 --> 00:01:35.726 align:middle +que es una forma elegante de apuntar +al directorio /var/cache/dev. + +00:01:36.376 --> 00:01:42.866 align:middle +Y luego vemos /pools/app, que es el +directorio real que contiene nuestra caché. + +00:01:43.626 --> 00:01:47.346 align:middle +Así que éste es el objetivo: en lugar de +almacenar cosas en el sistema de archivos, + +00:01:47.626 --> 00:01:51.176 align:middle +quiero cambiar el sistema de caché +para que se almacene en otro lugar. + +00:01:51.986 --> 00:01:58.266 align:middle +Antes de hacerlo, entra en VinylController +y, para que podamos ver el resultado + +00:01:58.266 --> 00:02:01.366 align:middle +del cambio que vamos a hacer, en dump($cache). + +00:02:02.246 --> 00:02:06.206 align:middle +Hasta ahora hemos utilizado dd(), +que significa "volcar y morir". + +00:02:06.746 --> 00:02:08.746 align:middle +Pero en este caso quiero dump()... + +00:02:09.156 --> 00:02:10.786 align:middle +pero deja que la página se cargue. + +00:02:11.676 --> 00:02:12.286 align:middle +Actualiza ahora. + +00:02:13.046 --> 00:02:15.156 align:middle +Espera, ¿dónde está mi volcado? + +00:02:15.676 --> 00:02:17.046 align:middle +Esto es una... + +00:02:17.046 --> 00:02:21.836 align:middle +¡característica! Cuando utilices dump(), +en realidad no lo verás en la página: + +00:02:22.206 --> 00:02:24.456 align:middle +se esconde aquí abajo, en la barra de +herramientas de depuración de la web. + +00:02:25.116 --> 00:02:29.626 align:middle +Si miras ahí, la caché es una +especie de TraceableAdapter. + +00:02:30.076 --> 00:02:34.946 align:middle +Pero dentro de ella hay un +objeto llamado FilesystemAdapter. + +00:02:35.506 --> 00:02:39.486 align:middle +Eso es una prueba de que el sistema de caché +está guardando en el sistema de archivos. + +00:02:40.316 --> 00:02:45.716 align:middle +Para hacer que esto se almacene en otro lugar, +entra en cache.yaml y cambia esta clave app. + +00:02:46.566 --> 00:02:50.906 align:middle +Puedes establecerla en una serie de cadenas +especiales diferentes, llamadas adaptadores. + +00:02:51.516 --> 00:02:57.566 align:middle +Si quisiéramos almacenar nuestra caché en +Redis, utilizaríamos cache.adapter.redis. + +00:02:58.446 --> 00:03:02.526 align:middle +Para facilitar las cosas, +utiliza cache.adapter.array. + +00:03:03.216 --> 00:03:08.726 align:middle +El adaptador array es una caché falsa +en la que sí se almacenan cosas... + +00:03:09.026 --> 00:03:12.796 align:middle +pero sólo vive mientras dura la petición. + +00:03:13.486 --> 00:03:17.286 align:middle +Así que, al final de cada petición, +se olvida de todo. Es una caché falsa + +00:03:17.956 --> 00:03:20.596 align:middle +, pero es suficiente + +00:03:20.596 --> 00:03:25.696 align:middle +para ver cómo el cambio de esta clave +afectará al propio servicio de caché. + +00:03:26.616 --> 00:03:27.996 align:middle +Observa lo que ocurre. + +00:03:28.516 --> 00:03:31.156 align:middle +Actualmente, tenemos un FilesystemAdapter. + +00:03:31.706 --> 00:03:32.956 align:middle +Cuando refrescamos... + +00:03:34.506 --> 00:03:37.026 align:middle +¡la caché es un ArrayAdapter ! + +00:03:37.776 --> 00:03:41.826 align:middle +Y como el ArrayAdapter olvida su +caché al final de la petición, + +00:03:42.376 --> 00:03:47.886 align:middle +puedes ver que cada petición +hace ahora una petición HTTP. + +00:03:48.676 --> 00:03:52.586 align:middle +Si estás un poco confundido por esto, +déjame que intente aclarar las cosas. El + +00:03:53.076 --> 00:03:57.006 align:middle +objetivo de este capítulo no es enseñarte a + +00:03:57.006 --> 00:04:00.226 align:middle +cambiar esta clave específica +en el archivo de caché. + +00:04:00.816 --> 00:04:06.026 align:middle +En última instancia, si necesitas configurar algo en +Symfony, simplemente buscarás en los documentos... + +00:04:06.416 --> 00:04:10.546 align:middle +que te dirán exactamente qué +hacer y qué clave cambiar. + +00:04:11.196 --> 00:04:17.066 align:middle +No, la gran conclusión es que el único +propósito de estos archivos de configuración es + +00:04:17.066 --> 00:04:20.976 align:middle +configurar los servicios +de nuestra aplicación. + +00:04:21.876 --> 00:04:25.166 align:middle +Cada vez que cambias una clave +en cualquiera de estos archivos, + +00:04:25.476 --> 00:04:31.136 align:middle +el resultado final es que acabas de cambiar +la forma de instanciar algún servicio. + +00:04:31.846 --> 00:04:37.676 align:middle +Modificar una clave puede cambiar todo el nombre de +la clase de un objeto de servicio, como en este caso, + +00:04:38.116 --> 00:04:43.896 align:middle +o puede cambiar el segundo o tercer +argumento del constructor que se pasará + +00:04:43.976 --> 00:04:46.096 align:middle +cuando se instancie el servicio. + +00:04:46.646 --> 00:04:53.226 align:middle +En realidad no importa lo que cambie, siempre +que te des cuenta de que esta configuración se + +00:04:53.226 --> 00:04:56.796 align:middle +refiere a los servicios +y a cómo se instancian. + +00:04:57.546 --> 00:05:01.836 align:middle +De hecho, nada de esta configuración puede +leerse directamente desde tu aplicación. + +00:05:02.536 --> 00:05:08.426 align:middle +No podrías, por ejemplo, pedir la configuración +de la "caché" desde dentro de un controlador. + +00:05:09.016 --> 00:05:11.696 align:middle +No, Symfony lee esta configuración, + +00:05:12.076 --> 00:05:19.026 align:middle +la utiliza para configurar cómo se instanciará +cada objeto de servicio, y luego la desecha. + +00:05:19.536 --> 00:05:21.456 align:middle +Los servicios son supremos. + +00:05:22.446 --> 00:05:27.686 align:middle +A continuación, a veces necesitarás que +cierta configuración sea diferente en + +00:05:27.686 --> 00:05:31.656 align:middle +función de si estás desarrollando +localmente o ejecutando en producción. + +00:05:32.406 --> 00:05:35.946 align:middle +Symfony tiene un sistema +para esto llamado "entornos". + +00:05:36.506 --> 00:05:38.356 align:middle +Vamos a aprender todo sobre eso diff --git a/sfcasts/ep2-fundamentals/es/cache-service.md b/sfcasts/ep2-fundamentals/es/cache-service.md new file mode 100644 index 0000000..d633aaf --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/cache-service.md @@ -0,0 +1,76 @@ +# El servicio de caché + +Ahora, cuando actualizamos la página de navegación, ¡las mezclas provienen de un repositorio en GitHub! Hacemos una petición HTTP a la API de GitHub, que obtiene este archivo de aquí, llamamos a `$response->toArray()` para decodificar ese JSON en una matriz `$mixes`... y luego lo representamos en la plantilla. Sí, ¡este archivo de GitHub es nuestra falsa base de datos temporal! + +Un problema práctico es que ahora cada carga de la página hace una petición HTTP... y las peticiones HTTP son lentas. Si desplegáramos esto en producción, nuestro sitio sería tan popular, por supuesto, que alcanzaríamos rápidamente nuestro límite de la API de GitHub. Y entonces esta página explotaría. + +Así que... estoy pensando: ¿y si guardamos en caché el resultado? Podríamos hacer esta petición HTTP y luego almacenar los datos en caché durante 10 minutos, o incluso una hora. ¿Cómo se almacenan las cosas en caché en Symfony? Lo has adivinado: ¡con un servicio! ¿Qué servicio? ¡No lo sé! Así que vamos a averiguarlo. + +## Encontrar el servicio de caché + +Ejecuta + +```terminal +php bin/console debug:autowiring cache +``` + +para buscar servicios con "caché" en su nombre. Y... ¡sí! De hecho, ¡hay varios! Hay uno llamado `CacheItemPoolInterface`, y otro llamado`StoreInterface`. Algunos de ellos no son exactamente lo que buscamos, pero`CacheItemPoolInterface`, `CacheInterface`, y `TagAwareCacheInterface` son todos servicios diferentes que puedes utilizar para el almacenamiento en caché. Todos hacen efectivamente lo mismo... pero el más fácil de usar es `CacheInterface`. + +Así que vamos a coger ese .... haciendo nuestro elegante truco de autoconexión Añade otro argumento a nuestro método escrito con `CacheInterface` (asegúrate de obtener el de`Symfony\Contracts\Cache`) y llámalo, qué tal, `$cache`: + +[[[ code('6b4b596a2d') ]]] + +Para utilizar el servicio `$cache`, copia estas dos líneas de antes, bórralas y sustitúyelas por `$mixes = $cache->get()`, como si fueras a sacar alguna clave de la caché. Podemos inventar la clave de la caché que queramos: +¿qué tal `mixes_data`. + +El objeto caché de Symfony funciona de una manera única. Llamamos a `$cache->get()` y le pasamos esta clave. Si ese resultado ya existe en la caché, se devolverá inmediatamente. Si aún no existe en la caché, entonces llamará a nuestro segundo argumento, que es una función. Aquí, nuestro trabajo es devolver los datos que deben ser almacenados en la caché. Pega las dos líneas de código que hemos copiado antes. Este `$httpClient`no está definido, así que tenemos que añadir `use ($httpClient)` para que entre en el ámbito. + +Ya está Y en lugar de establecer la variable `$mixes`, simplemente `return` esta línea`$response->toArray()`: + +[[[ code('c6c78aea17') ]]] + +Si no has utilizado antes el servicio de caché de Symfony, esto puede parecer extraño, pero a mí me encanta La primera vez que actualicemos la página, todavía no habrá ningún `mixes_data`en la caché. Así que llamará a nuestra función, devolverá el resultado y el sistema de caché lo almacenará en la caché. La próxima vez que actualicemos la página, la clave estará en la caché y devolverá el resultado inmediatamente. Así que no necesitamos ninguna sentencia "if" para ver si algo ya está en la caché... ¡sólo esto! + +## Depuración con el Perfilador de Caché + +Pero... ¿se mezclará? Vamos a averiguarlo. Refresca y... ¡bien! El primer refresco seguía haciendo la petición HTTP con normalidad. Abajo, en la barra de herramientas de depuración de la web, podemos ver que hubo tres llamadas a la caché y una escritura en la caché. Abre esto en una nueva pestaña para saltar a la sección de caché del perfilador. + +Genial: esto nos muestra que hubo una llamada a la caché para `mixes_data`, una escritura en la caché y un fallo en la caché. Un "miss" de la caché significa que se ha llamado a nuestra función y se ha escrito en la caché. + +En la siguiente actualización, observa este icono de aquí. Desaparece Eso es porque no hubo ninguna petición HTTP. Si vuelves a abrir el perfil de la caché, esta vez hubo una lectura y un acierto. Ese acierto significa que el resultado se ha cargado desde la caché y no ha hecho ninguna petición HTTP. ¡Eso es exactamente lo que queríamos! + +## Configurar el tiempo de vida de la caché + +Ahora te preguntarás: ¿cuánto tiempo permanecerá esta información en la caché? Ahora mismo... para siempre. Ooooh. Ese es el valor por defecto. + +Para que caduque antes que para siempre, dale a la función un argumento `CacheItemInterface`-asegúrate de pulsar "tab" para añadir esa declaración de uso- y llámala`$cacheItem`. Ahora podemos decir `$cacheItem->expiresAfter()` y, para hacerlo más fácil, decir `5`: + +[[[ code('205124d7cf') ]]] + +El artículo expirará después de 5 segundos. + +## Borrar la caché + +Por desgracia, si intentamos esto, el elemento que ya está en la caché está configurado para no caducar nunca. Así que... esto no funcionará hasta que borremos la caché. Pero... ¿dónde se almacena la caché? ¡Otra gran pregunta! Hablaremos más sobre esto en un segundo... pero, por defecto, se almacena en `var/cache/dev/`... junto con un montón de otros archivos de caché que ayudan a Symfony a hacer su trabajo. + +Podríamos borrar este directorio manualmente para limpiar la caché... ¡pero Symfony tiene una forma mejor! Es, por supuesto, otro comando de `bin/console`. + +Symfony tiene un montón de diferentes "categorías" de caché llamadas "cache pools". Si ejecutas + +```terminal +php bin/console cache:pool:list +``` + +verás todas ellas. La mayoría de ellas están pensadas para que Symfony las utilice internamente. El pool de caché que estamos utilizando se llama `cache.app`. Para borrarla, ejecuta: + +```terminal +php bin/console cache:pool:clear cache.app +``` + +¡Eso es todo! Esto no es algo que necesites hacer muy a menudo, pero es bueno saberlo, por si acaso. + +Bien, comprueba esto. Cuando refrescamos... obtenemos un fallo en la caché y puedes ver que hizo una llamada HTTP. Pero si volvemos a actualizar rápidamente... ¡ya no está! Vuelve a actualizar y... ¡ha vuelto! Eso es porque los cinco segundos acaban de expirar. + +Bien, equipo: ahora estamos aprovechando un servicio de cliente HTTP y un servicio de caché... ambos preparados para nosotros por uno de nuestros bundles para que podamos simplemente... ¡utilizarlos! + +Pero tengo una pregunta. ¿Qué pasa si necesitamos controlar estos servicios? Por ejemplo, ¿cómo podríamos decirle al servicio de caché que, en lugar de guardar cosas en el sistema de archivos de este directorio, queremos almacenarlas en Redis... o en memcache? Vamos a explorar la idea de controlar nuestros servicios mediante la configuración. diff --git a/sfcasts/ep2-fundamentals/es/cache-service.vtt b/sfcasts/ep2-fundamentals/es/cache-service.vtt new file mode 100644 index 0000000..6292829 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/cache-service.vtt @@ -0,0 +1,395 @@ +WEBVTT + +00:00:01.016 --> 00:00:06.986 align:middle +Ahora, cuando actualizamos la página de navegación, +¡las mezclas provienen de un repositorio en GitHub! + +00:00:07.446 --> 00:00:12.966 align:middle +Hacemos una petición HTTP a la API de +GitHub, que obtiene este archivo de aquí, + +00:00:13.376 --> 00:00:19.286 align:middle +llamamos a $response->toArray() para +decodificar ese JSON en una matriz $mixes... + +00:00:19.746 --> 00:00:23.416 align:middle +y luego lo representamos en la plantilla. + +00:00:23.976 --> 00:00:28.646 align:middle +Sí, ¡este archivo de GitHub es +nuestra falsa base de datos temporal! + +00:00:29.656 --> 00:00:36.036 align:middle +Un problema práctico es que ahora cada carga +de la página hace una petición HTTP... + +00:00:36.546 --> 00:00:39.786 align:middle +y las peticiones HTTP son lentas. + +00:00:40.656 --> 00:00:45.986 align:middle +Si desplegáramos esto en producción, nuestro +sitio sería tan popular, por supuesto, + +00:00:46.336 --> 00:00:49.666 align:middle +que alcanzaríamos rápidamente +nuestro límite de la API de GitHub. + +00:00:50.196 --> 00:00:53.026 align:middle +Y entonces esta página explotaría. + +00:00:53.476 --> 00:00:58.046 align:middle +Así que... estoy pensando: ¿y si +guardamos en caché el resultado? + +00:00:58.786 --> 00:01:05.536 align:middle +Podríamos hacer esta petición HTTP, y luego almacenar +los datos en caché durante 10 minutos, o incluso una hora. + +00:01:06.276 --> 00:01:08.076 align:middle +¡Eso podría funcionar! + +00:01:08.886 --> 00:01:11.646 align:middle +¿Cómo se almacenan las +cosas en caché en Symfony? + +00:01:12.146 --> 00:01:14.916 align:middle +Lo has adivinado: ¡con un servicio! + +00:01:15.476 --> 00:01:16.476 align:middle +¿Qué servicio? + +00:01:16.796 --> 00:01:20.036 align:middle +No lo sé Así que vamos a averiguarlo. + +00:01:20.646 --> 00:01:27.086 align:middle +Ejecuta: php bin/console debug:autowiring cache +para buscar servicios con "cache" en su nombre. + +00:01:27.816 --> 00:01:29.066 align:middle +Y... ¡sí! + +00:01:29.406 --> 00:01:31.956 align:middle +De hecho, ¡hay varios! + +00:01:32.576 --> 00:01:37.406 align:middle +Hay uno llamado CacheItemPoolInterface, +y otro llamado StoreInterface. + +00:01:38.016 --> 00:01:44.256 align:middle +Algunos de ellos no son exactamente lo que buscamos, +pero CacheItemPoolInterface, CacheInterface, + +00:01:44.376 --> 00:01:50.386 align:middle +y TagAwareCacheInterface son todos servicios diferentes +que puedes utilizar para el almacenamiento en caché. + +00:01:51.246 --> 00:01:53.656 align:middle +Todos ellos hacen efectivamente lo mismo... + +00:01:53.956 --> 00:01:56.746 align:middle +pero el más fácil de usar es CacheInterface. + +00:01:57.356 --> 00:01:58.936 align:middle +Así que vamos a coger ese .... + +00:01:59.176 --> 00:02:02.816 align:middle +¡haciendo nuestro elegante +truco de autoconexión! + +00:02:03.366 --> 00:02:08.646 align:middle +Añade otro argumento a nuestro método escrito +con CacheInterface (asegúrate de obtener el + +00:02:08.646 --> 00:02:13.356 align:middle +de Symfony\Contracts\Cache) +y llámalo, qué tal, $cache: + +00:02:14.246 --> 00:02:19.426 align:middle +Para utilizar el servicio $cache, copia +estas dos líneas de antes, bórralas + +00:02:19.806 --> 00:02:25.816 align:middle +y sustitúyelas por $mixes = +$cache->get(), como si fueras a + +00:02:25.816 --> 00:02:28.846 align:middle +sacar alguna clave de la caché. + +00:02:29.776 --> 00:02:35.456 align:middle +Podemos inventar la clave de la caché +que queramos: ¿qué tal mixes_data. + +00:02:36.266 --> 00:02:38.956 align:middle +El objeto caché de Symfony +funciona de una manera única. + +00:02:39.576 --> 00:02:42.956 align:middle +Llamamos a $cache->get() +y le pasamos esta clave. + +00:02:43.736 --> 00:02:49.226 align:middle +Si ese resultado ya existe en la +caché, se devolverá inmediatamente. + +00:02:49.766 --> 00:02:52.566 align:middle +Si aún no existe en la caché, + +00:02:52.946 --> 00:02:58.016 align:middle +entonces llamará a nuestro segundo +argumento, que es una función. + +00:02:58.726 --> 00:03:03.716 align:middle +Aquí, nuestro trabajo consiste en devolver los +datos que deben ser almacenados en la caché. + +00:03:04.556 --> 00:03:07.116 align:middle +Pega las dos líneas de código +que hemos copiado antes. + +00:03:07.706 --> 00:03:14.826 align:middle +Este $httpClient no está definido, así que tenemos que +añadir use ($httpClient) para que entre en el ámbito. + +00:03:15.646 --> 00:03:16.156 align:middle +Ya está + +00:03:16.636 --> 00:03:23.256 align:middle +Y en lugar de establecer la variable $mixes, +simplemente return esta línea $response->toArray(): + +00:03:24.016 --> 00:03:29.246 align:middle +Si no has utilizado antes el servicio de caché +de Symfony, ¡esto puede parecer extraño! + +00:03:29.586 --> 00:03:30.926 align:middle +¡Pero a mí me encanta! + +00:03:31.616 --> 00:03:38.376 align:middle +La primera vez que actualicemos la página, +aún no habrá ningún mixes_data en la caché. + +00:03:39.106 --> 00:03:42.446 align:middle +Así que llamará a nuestra +función, devolverá el resultado + +00:03:42.816 --> 00:03:46.426 align:middle +y el sistema de caché lo +almacenará en la caché. La + +00:03:47.266 --> 00:03:52.196 align:middle +próxima vez que actualicemos la +página, la clave estará en la caché + +00:03:52.546 --> 00:03:55.056 align:middle +y devolverá el resultado inmediatamente. + +00:03:55.486 --> 00:04:00.656 align:middle +Así que no necesitamos ninguna sentencia +"if" para ver si algo ya está en la caché... + +00:04:00.976 --> 00:04:02.136 align:middle +¡sólo esto! + +00:04:02.736 --> 00:04:04.356 align:middle +Pero... ¿se mezclará? + +00:04:04.886 --> 00:04:05.666 align:middle +Vamos a averiguarlo. + +00:04:06.276 --> 00:04:07.616 align:middle +Refresca y... + +00:04:08.196 --> 00:04:08.826 align:middle +¡guapa! + +00:04:09.346 --> 00:04:14.286 align:middle +El primer refresco seguía haciendo +la petición HTTP con normalidad. + +00:04:14.886 --> 00:04:17.786 align:middle +Abajo, en la barra de herramientas +de depuración de la web, podemos ver + +00:04:17.786 --> 00:04:21.886 align:middle +que hubo tres llamadas a la caché +y una escritura en la caché. + +00:04:22.646 --> 00:04:26.996 align:middle +Abre esto en una nueva pestaña para saltar +a la sección de caché del perfilador. + +00:04:27.716 --> 00:04:33.846 align:middle +Genial: esto nos muestra que hubo una +llamada a la caché para mixes_data, + +00:04:34.376 --> 00:04:37.836 align:middle +una escritura en la caché +y un fallo en la caché. + +00:04:38.676 --> 00:04:43.556 align:middle +Un "miss" de la caché significa que se ha llamado +a nuestra función y se ha escrito en la caché. + +00:04:44.366 --> 00:04:47.686 align:middle +En la siguiente actualización, +observa este icono de aquí. + +00:04:48.986 --> 00:04:50.506 align:middle +Desaparece + +00:04:50.976 --> 00:04:54.486 align:middle +Eso es porque no hubo ninguna petición HTTP. + +00:04:55.136 --> 00:05:01.386 align:middle +Si vuelves a abrir el perfil de la caché, +esta vez hubo una lectura y un acierto. + +00:05:02.146 --> 00:05:09.456 align:middle +Ese acierto significa que el resultado se ha cargado +desde la caché y no ha hecho ninguna petición HTTP. + +00:05:09.886 --> 00:05:12.156 align:middle +¡Eso es exactamente lo que queríamos! + +00:05:12.996 --> 00:05:18.116 align:middle +Ahora te preguntarás: ¿cuánto tiempo +permanecerá esta información en la caché? + +00:05:18.706 --> 00:05:19.216 align:middle +Ahora mismo... + +00:05:19.616 --> 00:05:22.246 align:middle +para siempre. Ooooh. + +00:05:22.776 --> 00:05:23.866 align:middle +Eso es lo predeterminado. + +00:05:24.776 --> 00:05:31.766 align:middle +Para que caduque antes que para siempre, dale +a la función un argumento CacheItemInterface + +00:05:32.316 --> 00:05:37.256 align:middle +-asegúrate de pulsar "tab" para añadir esa +declaración de uso- y llámala $cacheItem. + +00:05:38.206 --> 00:05:44.206 align:middle +Ahora podemos decir $cacheItem->expiresAfter() +y, para hacerlo más fácil, decir 5: + +00:05:45.116 --> 00:05:47.866 align:middle +El artículo expirará después de 5 segundos. + +00:05:48.516 --> 00:05:56.186 align:middle +Por desgracia, si intentamos esto, el elemento que ya +está en la caché está configurado para no caducar nunca. + +00:05:56.816 --> 00:06:01.236 align:middle +Así que... esto no funcionará +hasta que borremos la caché. + +00:06:02.146 --> 00:06:04.616 align:middle +Pero... ¿dónde se almacena la caché? + +00:06:05.216 --> 00:06:06.516 align:middle +¡Otra gran pregunta! + +00:06:07.076 --> 00:06:08.886 align:middle +Hablaremos más de ello en un segundo... + +00:06:09.156 --> 00:06:13.206 align:middle +pero, por defecto, se +almacena en var/cache/dev/... + +00:06:13.836 --> 00:06:17.586 align:middle +junto con un montón de otros archivos de +caché que ayudan a Symfony a hacer su trabajo. + +00:06:18.316 --> 00:06:22.076 align:middle +Podríamos borrar este directorio +manualmente para limpiar la caché... + +00:06:22.546 --> 00:06:24.486 align:middle +¡pero Symfony tiene una forma mejor! + +00:06:25.076 --> 00:06:28.636 align:middle +Es, por supuesto, otro comando de bin/console. + +00:06:29.416 --> 00:06:33.846 align:middle +Symfony tiene un montón de diferentes +"categorías" de caché llamadas "cache pools". + +00:06:34.446 --> 00:06:38.926 align:middle +Si ejecutas: php bin/console +cache:pool:list verás todas ellas. + +00:06:39.676 --> 00:06:42.926 align:middle +La mayoría de ellas están pensadas para +que Symfony las utilice internamente. + +00:06:43.546 --> 00:06:47.116 align:middle +El pool de caché que estamos +utilizando se llama cache.app. + +00:06:47.246 --> 00:06:55.066 align:middle +Para borrarla, ejecuta: php bin/console +cache:pool:clear cache.app ¡Eso es todo! + +00:06:55.736 --> 00:07:00.976 align:middle +Esto no es algo que necesites hacer muy a +menudo, pero es bueno saberlo, por si acaso. + +00:07:01.946 --> 00:07:03.296 align:middle +Bien, comprueba esto. + +00:07:03.646 --> 00:07:05.016 align:middle +Cuando refrescamos... + +00:07:05.376 --> 00:07:11.136 align:middle +obtenemos un fallo en la caché y puedes +ver que ha hecho una llamada HTTP. + +00:07:11.816 --> 00:07:14.346 align:middle +Pero si volvemos a actualizar rápidamente... + +00:07:14.826 --> 00:07:15.906 align:middle +ya no está + +00:07:16.476 --> 00:07:17.986 align:middle +Vuelve a actualizar y... + +00:07:18.186 --> 00:07:19.286 align:middle +¡ha vuelto! + +00:07:19.776 --> 00:07:22.686 align:middle +Eso es porque los cinco +segundos acaban de expirar. + +00:07:23.736 --> 00:07:29.686 align:middle +Bien equipo: ahora estamos aprovechando un +servicio de cliente HTTP y un servicio de caché... + +00:07:30.146 --> 00:07:36.506 align:middle +que nos ha preparado uno de +nuestros bundles para que podamos... + +00:07:36.726 --> 00:07:40.416 align:middle +¡utilizarlos! Pero tengo una pregunta. + +00:07:41.016 --> 00:07:44.146 align:middle +¿Qué pasa si necesitamos +controlar estos servicios? + +00:07:44.746 --> 00:07:50.246 align:middle +Por ejemplo, ¿cómo podríamos decirle al servicio +de caché que, en lugar de guardar las cosas + +00:07:50.246 --> 00:07:56.216 align:middle +en el sistema de archivos de este +directorio, queremos almacenarlas en Redis... + +00:07:56.216 --> 00:07:57.366 align:middle +o en memcache? + +00:07:58.146 --> 00:08:03.656 align:middle +Vamos a explorar la idea de controlar +nuestros servicios mediante la configuración diff --git a/sfcasts/ep2-fundamentals/es/command-extra.md b/sfcasts/ep2-fundamentals/es/command-extra.md new file mode 100644 index 0000000..4e24e05 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/command-extra.md @@ -0,0 +1,64 @@ +# Comando: Autoconexión y preguntas interactivas + +¡Equipo del último capítulo! ¡Vamos a hacerlo! + +Vale, ¿y si necesitamos un servicio desde dentro de nuestro comando? Por ejemplo, digamos que queremos utilizar `MixRepository` para imprimir una recomendación de mezcla de vinilo. ¿Cómo podemos hacerlo? + +Bueno, estamos dentro de un servicio y necesitamos acceder a otro servicio, lo que significa que necesitamos... la temida inyección de dependencia. Es broma, no es temible, ¡es fácil con el autocableado! + +Añade `public function __construct()` con `private MixRepository $mixRepository`para crear y establecer esa propiedad de una sola vez. + +[[[ code('628bac0c7d') ]]] + +Aunque, si pasas el ratón por encima de `__construct()`, dice + +> Falta una llamada al constructor padre. + +Para solucionarlo, llama a `parent::__construct()`: + +[[[ code('f2801b5b65') ]]] + +Esta es una situación súper rara en la que la clase base tiene un constructor al que tenemos que llamar. De hecho, es la única situación que se me ocurre en Symfony como ésta... así que normalmente no es algo de lo que debas preocuparte. + +## Preguntas interactivas + +Aquí abajo, vamos a dar salida a una recomendación de mezcla... pero hazlo aún más genial preguntando primero al usuario si quiere esta recomendación. + +Podemos hacer preguntas interactivas aprovechando el objeto `$io`. Diré`if ($io->confirm('Do you want a mix recommendation?'))`: + +[[[ code('cdef6583b7') ]]] + +Esto hará esa pregunta, y si el usuario responde "sí", devolverá true. +El objeto `$io` está lleno de cosas geniales como ésta, incluyendo la formulación de preguntas de opción múltiple y el autocompletado de las respuestas. Incluso podemos crear una barra de progreso + +Dentro del if, obtén todas las mezclas con`$mixes = $this->mixRepository->findAll()`. Luego... sólo necesitamos un poco de código feo - `$mix = $mixes[array_rand($mixes)]` - para obtener una mezcla aleatoria. + +Imprime la mezcla con un método más `$io` `$io->note()` pasando por`I recommend the mix` y luego introduce `$mix['title']`: + +[[[ code('bb2cf7650b') ]]] + +Y... ¡listo! Por cierto, ¿te has fijado en este `return Command::SUCCESS`? Eso controla el código de salida de tu comando, así que siempre querrás tener `Command::SUCCESS` al final de tu comando. Si hubiera un error, podrías `return Command::ERROR`. + +Bien, ¡probemos esto! Dirígete a tu terminal y ejecuta: + +```terminal +php bin/console app:talk-to-me --yell +``` + +Obtenemos la salida... y luego obtenemos: + +> ¿Quieres una recomendación de mezcla? + +¡Pues sí, la queremos! ¡Y qué excelente recomendación! + +¡Muy bien, equipo! ¡Lo hemos conseguido! ¡Hemos terminado el que creo que es el tutorial de Symfony más importante de todos los tiempos! No importa lo que necesites construir en Symfony, los conceptos que acabamos de aprender serán la base para hacerlo. + +Por ejemplo, si necesitas añadir una función o un filtro personalizado a Twig, ¡no hay problema! Lo haces creando una clase de extensión de Twig... y puedes usar MakerBundle para generarla por ti o construirla a mano. Es muy similar a la creación de un comando de consola personalizado: en ambos casos, estás construyendo algo para "engancharse" a una parte de Symfony. + +Así que, para crear una extensión Twig, debes crear una nueva clase PHP, hacer que implemente cualquier interfaz o clase base que necesiten las extensiones Twig (la documentación te lo dirá)... y luego sólo tienes que rellenar la lógica... que no mostraré aquí. + +Y ya está Entre bastidores, tu extensión Twig se vería automáticamente como un servicio, y la autoconfiguración se encargaría de integrarla en Twig... exactamente como el comando de la consola. + +En el próximo curso, pondremos en práctica nuestros nuevos superpoderes añadiendo una base de datos a nuestra aplicación para poder cargar datos reales y dinámicos. Y si tienes alguna pregunta real y dinámica, estamos aquí para ti, como siempre, abajo en la sección de comentarios. + +Muy bien, amigos. Muchas gracias por codificar conmigo y nos vemos la próxima vez. diff --git a/sfcasts/ep2-fundamentals/es/command-extra.vtt b/sfcasts/ep2-fundamentals/es/command-extra.vtt new file mode 100644 index 0000000..ac38086 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/command-extra.vtt @@ -0,0 +1,245 @@ +WEBVTT + +00:00:01.016 --> 00:00:02.686 align:middle +¡Equipo del último capítulo! + +00:00:02.906 --> 00:00:03.986 align:middle +¡Hagamos esto! + +00:00:04.546 --> 00:00:08.296 align:middle +Vale, ¿y si necesitamos un servicio +desde dentro de nuestro comando? + +00:00:08.986 --> 00:00:13.156 align:middle +Por ejemplo, digamos que +queremos utilizar MixRepository + +00:00:13.486 --> 00:00:16.356 align:middle +para imprimir una recomendación +de mezcla de vinilo. + +00:00:17.006 --> 00:00:18.106 align:middle +¿Cómo podemos hacerlo? + +00:00:18.876 --> 00:00:22.276 align:middle +Bueno, estamos dentro de un +servicio y necesitamos acceder + +00:00:22.276 --> 00:00:26.576 align:middle +a otro servicio, lo que +significa que necesitamos... + +00:00:26.576 --> 00:00:29.556 align:middle +la temida inyección de dependencia. + +00:00:30.196 --> 00:00:34.356 align:middle +Es broma, no es temible, ¡es +fácil con el autocableado! + +00:00:35.166 --> 00:00:42.506 align:middle +Añade public function __construct() con +private MixRepository $mixRepository para crear + +00:00:42.506 --> 00:00:44.656 align:middle +y establecer esa propiedad de una sola vez. + +00:00:45.416 --> 00:00:50.726 align:middle +Aunque, si pasas el ratón por encima de __construct(), +dice Falta la llamada al constructor padre. + +00:00:51.396 --> 00:00:54.366 align:middle +Para solucionarlo, llama +a parent::__construct(): + +00:00:55.086 --> 00:01:00.826 align:middle +Esta es una situación súper rara en la que la clase +base tiene un constructor al que tenemos que llamar. + +00:01:01.686 --> 00:01:06.386 align:middle +De hecho, esta es la única situación +que se me ocurre en Symfony como esta... + +00:01:06.466 --> 00:01:09.666 align:middle +así que normalmente no es algo +de lo que debas preocuparte. + +00:01:10.476 --> 00:01:13.396 align:middle +Aquí abajo, vamos a emitir +una recomendación de mezcla... + +00:01:13.716 --> 00:01:19.726 align:middle +pero hazlo aún más genial preguntando primero +al usuario si quiere esta recomendación. + +00:01:20.346 --> 00:01:24.466 align:middle +Podemos hacer preguntas interactivas +aprovechando el objeto $io. + +00:01:25.076 --> 00:01:30.506 align:middle +Diré if ($io->confirm('Do you +want a mix recommendation?')): + +00:01:32.176 --> 00:01:36.886 align:middle +Esto hará esa pregunta, y si el +usuario responde "sí", devolverá true. + +00:01:37.776 --> 00:01:40.986 align:middle +El objeto $io está lleno de +cosas geniales como ésta, + +00:01:41.436 --> 00:01:46.226 align:middle +incluyendo la formulación de preguntas de opción +múltiple y el autocompletado de las respuestas. + +00:01:46.816 --> 00:01:49.756 align:middle +Incluso podemos crear una barra de progreso + +00:01:50.546 --> 00:01:57.096 align:middle +Dentro del if, obtén todas las mezclas con +$mixes = $this->mixRepository->findAll(). + +00:01:57.906 --> 00:02:00.666 align:middle +Luego... sólo necesitamos +un poco de código feo - + +00:02:01.116 --> 00:02:07.676 align:middle +$mix = $mixes[array_rand($mixes)] - +para obtener una mezcla aleatoria. + +00:02:08.356 --> 00:02:15.116 align:middle +Imprime la mezcla con un método más $io +$io->note() pasando por I recommend the mix + +00:02:15.116 --> 00:02:18.356 align:middle +y luego introduce $mix['title']: + +00:02:19.076 --> 00:02:20.556 align:middle +Y... ¡listo! + +00:02:21.076 --> 00:02:25.156 align:middle +Por cierto, ¿te has fijado en +este return Command::SUCCESS? + +00:02:25.876 --> 00:02:30.336 align:middle +Eso controla el código de salida de +tu comando, así que siempre querrás + +00:02:30.666 --> 00:02:34.336 align:middle +tener Command::SUCCESS al final de tu comando. + +00:02:35.176 --> 00:02:38.856 align:middle +Si hubiera un error, podrías +return Command::ERROR. + +00:02:39.576 --> 00:02:41.456 align:middle +Bien, ¡probemos esto! + +00:02:42.116 --> 00:02:43.636 align:middle +Dirígete a tu terminal y ejecuta: + +00:02:43.636 --> 00:02:49.036 align:middle +php bin/console app:talk-to-me +--yell Obtenemos la salida... + +00:02:49.386 --> 00:02:52.996 align:middle +y luego obtenemos: ¿Quieres +una recomendación de mezcla? + +00:02:53.486 --> 00:02:54.916 align:middle +¡Pues sí, la queremos! + +00:02:55.576 --> 00:02:57.796 align:middle +¡Y qué excelente recomendación! + +00:02:58.766 --> 00:02:59.456 align:middle +¡Muy bien, equipo! + +00:02:59.506 --> 00:03:00.436 align:middle +¡Lo hemos conseguido! + +00:03:00.946 --> 00:03:07.626 align:middle +¡Hemos terminado el que creo que es el tutorial +de Symfony más importante de todos los tiempos! + +00:03:08.176 --> 00:03:10.956 align:middle +No importa lo que necesites +construir en Symfony, + +00:03:11.216 --> 00:03:15.536 align:middle +los conceptos que acabamos de +aprender serán la base para hacerlo. + +00:03:16.136 --> 00:03:22.096 align:middle +Por ejemplo, si necesitas añadir una función o +un filtro personalizado a Twig, ¡no hay problema! + +00:03:22.786 --> 00:03:25.966 align:middle +Lo harás creando una clase +de extensión de Twig... + +00:03:26.496 --> 00:03:30.756 align:middle +y puedes utilizar MakerBundle para +generarla por ti o construirla a mano. Es + +00:03:31.366 --> 00:03:36.096 align:middle +muy similar a la creación de un comando +de consola personalizado: en ambos casos, + +00:03:36.176 --> 00:03:39.586 align:middle +estás construyendo algo para +"engancharse" a una parte de Symfony. + +00:03:40.166 --> 00:03:45.176 align:middle +Así que, para crear una extensión +Twig , crearías una nueva clase PHP, + +00:03:47.436 --> 00:03:50.856 align:middle +harías que implementara +cualquier interfaz o clase base + +00:03:50.946 --> 00:03:55.136 align:middle +que necesiten las extensiones Tw ig +(la documentación te lo dirá)... + +00:03:55.976 --> 00:03:58.936 align:middle +y luego simplemente rellenas la lógica... + +00:03:58.936 --> 00:04:00.296 align:middle +que no mostraré aquí. + +00:04:01.006 --> 00:04:01.956 align:middle +Y ya está + +00:04:02.546 --> 00:04:07.886 align:middle +Entre bastidores, tu extensión Twig se +vería automáticamente como un servicio, + +00:04:08.176 --> 00:04:12.836 align:middle +y la autoconfiguración se +encargaría de integrarla en Tw ig... + +00:04:13.366 --> 00:04:15.696 align:middle +exactamente igual que el comando de la consola. + +00:04:16.486 --> 00:04:22.126 align:middle +En el próximo curso, pondremos en práctica nuestros +nuevos superpoderes añadiendo una base de datos + +00:04:22.126 --> 00:04:25.016 align:middle +a nuestra aplicación para poder +cargar datos reales y dinámicos. + +00:04:25.016 --> 00:04:29.676 align:middle +Y si tienes alguna pregunta real y +dinámica, estamos aquí para ti, + +00:04:29.906 --> 00:04:32.096 align:middle +como siempre, abajo en la +sección de comentarios. + +00:04:32.796 --> 00:04:33.406 align:middle +Muy bien, amigos. + +00:04:33.746 --> 00:04:37.456 align:middle +Muchas gracias por codificar +conmigo y nos vemos la próxima vez diff --git a/sfcasts/ep2-fundamentals/es/controllers-services.md b/sfcasts/ep2-fundamentals/es/controllers-services.md new file mode 100644 index 0000000..d83f232 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/controllers-services.md @@ -0,0 +1,33 @@ +# ¡Los controladores también son servicios! + +Abre `src/Controller/VinylController.php`. ¡Puede que sea obvio o no, pero nuestras clases de controlador también son servicios en el contenedor! ¡Sí! Se sienten especiales porque son controladores... pero en realidad sólo son viejos y aburridos servicios como todo lo demás. Bueno, excepto que tienen un superpoder que no tiene nada más: la capacidad de autoconectar argumentos en sus métodos de acción. Normalmente, el autocableado sólo funciona con el constructor. + +## Vinculación de los argumentos de la acción + +Los métodos de acción funcionan realmente como los constructores en lo que respecta a la autoconexión. Por ejemplo, añade un argumento `bool $isDebug` a la acción `browse()`... y a continuación `dump($isDebug)`: + +[[[ code('d77a8d195f') ]]] + +Y eso... ¡no funciona! Hasta ahora, las únicas dos cosas que sabemos que podemos tener como argumentos de nuestras "acciones" son (A), cualquier comodín en la ruta como`$slug` y (B) los servicios autocableables, como `MixRepository`. + +Pero ahora, vuelve a `config/services.yaml` y descomenta el global `bind` de antes: + +[[[ code('78858f9ac6') ]]] + +Esta vez... ¡funciona! + +## Añadir un constructor + +Yendo en la otra dirección, como los controladores son servicios, puedes absolutamente tener un constructor si quieres. Movamos `MixRepository` y `$isDebug` a un nuevo constructor. Cópialos, quítalos... añade `public function __construct()`, pégalos... y luego los pondré en sus propias líneas. Para convertirlos en propiedades, añade`private` delante de cada uno: + +[[[ code('4c824ee41c') ]]] + +De nuevo abajo, sólo tenemos que asegurarnos de cambiar a `dump($this->isDebug)` y añadir `$this->` delante de `mixRepository`: + +[[[ code('672b0fa2a8') ]]] + +¡Bien! Si probamos esto ahora... ¡funciona bien! + +Normalmente no sigo este enfoque... principalmente porque añadir argumentos al método de acción es muy fácil. Pero si necesitas un servicio u otro valor en cada método de acción de tu clase, definitivamente puedes limpiar tu lista de argumentos inyectándola a través del constructor. Voy a eliminar ese `dump()`. + +A continuación, vamos a hablar de las variables de entorno y de la finalidad del archivo `.env` que hemos visto antes. Estas cosas serán cada vez más importantes a medida que hagamos nuestra aplicación más y más realista. diff --git a/sfcasts/ep2-fundamentals/es/controllers-services.vtt b/sfcasts/ep2-fundamentals/es/controllers-services.vtt new file mode 100644 index 0000000..a0608cb --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/controllers-services.vtt @@ -0,0 +1,131 @@ +WEBVTT + +00:00:01.016 --> 00:00:03.386 align:middle +Abre src/Controller/VinylController.php. + +00:00:03.386 --> 00:00:12.196 align:middle +Puede que sea obvio o no, pero nuestras clases de +controlador también son servicios en el contenedor + +00:00:12.926 --> 00:00:17.036 align:middle +¡Sí! Parecen especiales +porque son controladores... + +00:00:17.446 --> 00:00:22.016 align:middle +pero en realidad no son más que viejos y +aburridos servicios, como todo lo demás. + +00:00:22.516 --> 00:00:27.256 align:middle +Bueno, excepto que tienen un +superpoder que nada más tiene + +00:00:27.576 --> 00:00:32.366 align:middle +la capacidad de autoconectar +argumentos en sus métodos de acción. + +00:00:33.016 --> 00:00:36.116 align:middle +Normalmente, el autocableado +sólo funciona con el constructor. + +00:00:36.746 --> 00:00:43.226 align:middle +Y los métodos de acción funcionan realmente igual que +los constructores en lo que respecta a la autoconexión. + +00:00:43.946 --> 00:00:48.656 align:middle +Por ejemplo, añade un argumento bool +$isDebug a la acción browse()... + +00:00:49.306 --> 00:00:51.416 align:middle +y a continuación dump($isDebug): + +00:00:52.396 --> 00:00:53.456 align:middle +Y eso... + +00:00:54.246 --> 00:00:55.496 align:middle +¡no funciona! Hasta + +00:00:56.006 --> 00:01:01.316 align:middle +ahora, las únicas dos cosas que sabemos que +podemos tener como argumentos de nuestras + +00:01:01.446 --> 00:01:06.216 align:middle +"acciones" son (A), cualquier +comodín en la ruta como $slug + +00:01:06.866 --> 00:01:11.686 align:middle +y (B) los servicios +autocableables, como MixRepository. + +00:01:12.516 --> 00:01:19.356 align:middle +Pero ahora, vuelve a config/services.yaml +y descomenta el global bind de antes: + +00:01:20.276 --> 00:01:20.856 align:middle +Esta vez... + +00:01:21.976 --> 00:01:27.856 align:middle +¡funciona! Yendo en la otra dirección, +como los controladores son servicios, + +00:01:28.286 --> 00:01:31.886 align:middle +puedes tener absolutamente +un constructor si quieres. + +00:01:32.646 --> 00:01:37.056 align:middle +Movamos MixRepository y +$isDebug a un nuevo constructor. + +00:01:37.716 --> 00:01:39.336 align:middle +Cópialos, quítalos... + +00:01:41.836 --> 00:01:45.856 align:middle +añade public function __construct(), pega... + +00:01:46.406 --> 00:01:48.616 align:middle +y los pondré en sus propias líneas. + +00:01:49.636 --> 00:01:53.266 align:middle +Para convertirlos en propiedades, +añade private delante de cada uno: De + +00:01:53.266 --> 00:02:00.426 align:middle +nuevo abajo, sólo tenemos que asegurarnos +de cambiar a dump($this->isDebug) + +00:02:00.736 --> 00:02:03.986 align:middle +y añadir $this-> delante de mixRepository: + +00:02:04.546 --> 00:02:07.066 align:middle +¡Bien! Si probamos esto ahora... + +00:02:07.876 --> 00:02:09.726 align:middle +¡funciona bien! + +00:02:10.516 --> 00:02:13.126 align:middle +Normalmente no sigo este enfoque... + +00:02:13.336 --> 00:02:19.516 align:middle +principalmente porque añadir argumentos +al método de la acción es muy fácil. + +00:02:20.146 --> 00:02:25.096 align:middle +Pero si necesitas un servicio u otro valor +en cada método de acción de tu clase, + +00:02:25.616 --> 00:02:30.776 align:middle +definitivamente puedes limpiar tu lista de +argumentos inyectándola a través del constructor. + +00:02:31.476 --> 00:02:33.006 align:middle +Voy a eliminar ese dump(). + +00:02:33.986 --> 00:02:38.456 align:middle +A continuación, vamos a hablar de las +variables de entorno y de la finalidad + +00:02:38.456 --> 00:02:41.356 align:middle +del archivo .env que hemos visto antes. + +00:02:41.976 --> 00:02:48.726 align:middle +Estas cosas serán cada vez más importantes a medida +que hagamos nuestra aplicación más y más realista diff --git a/sfcasts/ep2-fundamentals/es/create-service.md b/sfcasts/ep2-fundamentals/es/create-service.md new file mode 100644 index 0000000..165fc56 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/create-service.md @@ -0,0 +1,39 @@ +# Crear un servicio + +Sabemos que los bundles nos dan servicios y los servicios funcionan. De acuerdo. ¿Pero qué pasa si necesitamos escribir nuestro propio código personalizado que sí funcione? ¿Deberíamos... ponerlo en nuestra propia clase de servicio? Por supuesto Y es una gran manera de organizar tu código. + +Ya estamos haciendo algo de trabajo en nuestra aplicación. En la acción `browse()`: hacemos una petición HTTP y almacenamos en caché el resultado. Poner esta lógica en nuestro controlador está bien. Pero al trasladarla a su propia clase de servicio, hará que el propósito del código sea más claro, nos permitirá reutilizarlo desde múltiples lugares... e incluso nos permitirá hacer pruebas unitarias de ese código si queremos. + +## Crear la clase de servicio + +Eso suena increíble, así que ¡hagámoslo! ¿Cómo creamos un servicio? En el directorio `src/`, crea una nueva clase PHP donde quieras. En serio, no importa qué directorios o subdirectorios crees en `src/`: haz lo que te parezca. + +Para este ejemplo, crearé un directorio `Service/` -aunque de nuevo, podrías llamarlo `PizzaParty` o `Repository` - y dentro de él, una nueva clase PHP. Llamémosla... qué tal `MixRepository`. "Repositorio" es un nombre bastante común para un servicio que devuelve datos. Observa que cuando creo esto, PhpStorm añade automáticamente el espacio de nombres correcto. No importa cómo organicemos nuestras clases dentro de `src/`... siempre que nuestro espacio de nombres coincida con el directorio. + +Una cosa importante sobre las clases de servicio: no tienen nada que ver con Symfony. Nuestra clase de controlador es un concepto de Symfony. Pero `MixRepository` es una clase que estamos creando para organizar nuestro propio código. Eso significa que... ¡no hay reglas! No necesitamos extender una clase base o implementar una interfaz. Podemos hacer que esta clase se vea y se sienta como queramos. ¡El poder! + +Con esto en mente, vamos a crear un nuevo `public function` llamado, qué tal,`findAll()` que será `return` un `array` de todas las mezclas de nuestro sistema. De vuelta en `VinylController`, copia toda la lógica que obtiene las mezclas y pégala aquí. PhpStorm nos preguntará si queremos añadir una declaración `use` para el`CacheItemInterface`. ¡Lo hacemos totalmente! Entonces, en lugar de crear una variable `$mixes`, sólo `return`. + +Hay algunas variables no definidas en esta clase... y esas serán un problema, pero ignóralas por un momento: Primero quiero ver si podemos utilizar nuestro nuevo y brillante`MixRepository`. + +## ¿Nuestro Servicio ya está en el Contenedor? + +Entra en `VinylController`. Pensemos: de alguna manera tenemos que informar al contenedor de servicios de Symfony sobre nuestro nuevo servicio para poder autocablearlo de la misma manera que estamos autocableando servicios centrales como `HtttpClientInterface` y `CacheInterface`. + +Vaya, ¡tengo una sorpresa! Dirígete a tu terminal y ejecuta: + +```terminal +php bin/console debug:autowiring --all +``` + +Desplázate hasta la parte superior y... ¡sorpresa! ¡ `MixRepository` ya es un servicio en el contenedor! Deja que te explique dos cosas. En primer lugar, la bandera `--all` no es tan importante. Básicamente le dice a este comando que te muestre los servicios principales como`$httpClient` y `$cache`, además de nuestros propios servicios como `MixRepository`. + +En segundo lugar, el contenedor... de alguna manera ya vio nuestra clase de repositorio y la reconoció como un servicio. Aprenderemos cómo ocurrió eso en unos minutos... pero por ahora, basta con saber que nuestro nuevo `MixRepository` ya está dentro del contenedor y su id de servicio es el nombre completo de la clase. ¡Eso significa que podemos autocablearlo! + +## Autoconexión del nuevo servicio + +De vuelta a nuestro controlador, añade un tercer argumento de tipo `MixRepository` - pulsa el tabulador para añadir la declaración `use` - y llámalo... ¿qué tal `$mixRepository`? Luego, aquí abajo, ya no necesitamos nada de este código `$mixes`. Sustitúyelo por `$mixes = $mixRepository->findAll()`. + +¿Qué te parece? ¿Funcionará? ¡Averigüémoslo! Actualiza y... ¡funciona! Vale, que funcione en este caso significa que obtenemos un mensaje `Undefined variable $cache` procedente de `MixRepository`. Pero el hecho de que nuestro código haya llegado hasta aquí significa que el autocableado de `MixRepository` ha funcionado: el contenedor lo ha visto, ha instanciado`MixRepository` y nos lo ha pasado para que podamos utilizarlo. + +¡Así que hemos creado un servicio y lo hemos puesto a disposición del autocableado! Pero nuestro nuevo servicio necesita los servicios `$httpClient` y `$cache` para poder hacer su trabajo. ¿Cómo los conseguimos? La respuesta es uno de los conceptos más importantes de Symfony y de la codificación orientada a objetos en general: la inyección de dependencias. Vamos a hablar de ello a continuación. \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/create-service.vtt b/sfcasts/ep2-fundamentals/es/create-service.vtt new file mode 100644 index 0000000..4d34e1a --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/create-service.vtt @@ -0,0 +1,294 @@ +WEBVTT + +00:00:01.026 --> 00:00:05.146 align:middle +Sabemos que los bundles nos dan +servicios y los servicios funcionan. + +00:00:05.586 --> 00:00:11.446 align:middle +De acuerdo. ¿Pero qué pasa si necesitamos escribir +nuestro propio código personalizado que sí funcione? + +00:00:11.956 --> 00:00:12.386 align:middle +¿Debemos... + +00:00:12.646 --> 00:00:15.346 align:middle +ponerlo en nuestra propia clase de servicio? + +00:00:15.906 --> 00:00:17.216 align:middle +Por supuesto + +00:00:17.476 --> 00:00:20.316 align:middle +Y es una buena forma de organizar tu código. + +00:00:21.116 --> 00:00:23.936 align:middle +Ya estamos haciendo algo de +trabajo en nuestra aplicación. + +00:00:24.576 --> 00:00:29.916 align:middle +En la acción browse(): hacemos una petición +HTTP y almacenamos en caché el resultado. + +00:00:30.556 --> 00:00:34.086 align:middle +Poner esta lógica en nuestro +controlador está bien. + +00:00:34.556 --> 00:00:41.516 align:middle +Pero al trasladarla a su propia clase de servicio, +hará que el propósito del código sea más claro, + +00:00:42.006 --> 00:00:44.886 align:middle +nos permitirá reutilizarlo +desde múltiples lugares... + +00:00:45.346 --> 00:00:49.086 align:middle +e incluso nos permitirá hacer pruebas +unitarias de ese código si queremos. + +00:00:49.746 --> 00:00:52.916 align:middle +Eso suena increíble, así que ¡hagámoslo! + +00:00:53.636 --> 00:00:55.216 align:middle +¿Cómo creamos un servicio? + +00:00:55.706 --> 00:01:00.626 align:middle +En el directorio src/, crea una +nueva clase PHP donde quieras. + +00:01:01.146 --> 00:01:05.616 align:middle +En serio, no importa qué +directorios o subdirectorios crees + +00:01:05.616 --> 00:01:08.886 align:middle +en src/: haz lo que te parezca. + +00:01:09.606 --> 00:01:14.216 align:middle +Para este ejemplo, crearé un +directorio Service/ -aunque de nuevo, + +00:01:14.346 --> 00:01:21.726 align:middle +podrías llamarlo PizzaParty o Repository +- y dentro de él, una nueva clase PHP. + +00:01:22.336 --> 00:01:22.866 align:middle +Llamémosla... + +00:01:23.206 --> 00:01:25.086 align:middle +qué tal MixRepository. + +00:01:25.886 --> 00:01:30.696 align:middle +"Repositorio" es un nombre bastante común +para un servicio que devuelve datos. + +00:01:31.536 --> 00:01:36.656 align:middle +Observa que cuando creo esto, PhpStorm añade +automáticamente el espacio de nombres correcto. + +00:01:37.306 --> 00:01:41.346 align:middle +No importa cómo organicemos +nuestras clases dentro de src/... + +00:01:41.806 --> 00:01:45.446 align:middle +siempre que nuestro espacio de +nombres coincida con el directorio. + +00:01:46.386 --> 00:01:52.916 align:middle +Una cosa importante sobre las clases de +servicio: no tienen nada que ver con Symfony. + +00:01:53.606 --> 00:01:56.916 align:middle +Nuestra clase controladora +es un concepto de Symfony. + +00:01:57.376 --> 00:02:03.276 align:middle +Pero MixRepository es una clase que estamos +creando para organizar nuestro propio código. + +00:02:04.076 --> 00:02:04.616 align:middle +Eso significa que... + +00:02:04.906 --> 00:02:06.876 align:middle +¡que no hay reglas! + +00:02:07.236 --> 00:02:10.846 align:middle +No necesitamos extender una clase +base o implementar una interfaz. + +00:02:11.306 --> 00:02:15.596 align:middle +Podemos hacer que esta clase se +vea y se sienta como queramos. + +00:02:16.006 --> 00:02:16.896 align:middle +¡El poder! + +00:02:17.776 --> 00:02:22.436 align:middle +Con esto en mente, vamos a crear un +nuevo public function llamado, qué tal, + +00:02:22.736 --> 00:02:28.756 align:middle +findAll() que será return un array de +todas las mezclas de nuestro sistema. + +00:02:29.636 --> 00:02:35.546 align:middle +De vuelta en VinylController, copia toda la +lógica que obtiene las mezclas y pégala aquí. + +00:02:36.236 --> 00:02:40.936 align:middle +PhpStorm nos preguntará si queremos añadir +una declaración use para el CacheItemInterface. + +00:02:41.336 --> 00:02:42.646 align:middle +¡Lo hacemos totalmente! + +00:02:43.506 --> 00:02:47.286 align:middle +Entonces, en lugar de crear una +variable $mixes, sólo return. + +00:02:48.046 --> 00:02:51.596 align:middle +Hay algunas variables no +definidas en esta clase... + +00:02:52.036 --> 00:02:54.486 align:middle +y esas serán un problema. + +00:02:54.816 --> 00:03:02.526 align:middle +Pero ignóralas por un momento: Primero quiero ver si +podemos utilizar nuestro nuevo y brillante MixRepository. + +00:03:03.306 --> 00:03:04.626 align:middle +Entra en VinylController. + +00:03:05.416 --> 00:03:12.876 align:middle +Pensemos: de alguna manera tenemos que informar al contenedor +de servicios de Symfony sobre nuestro nuevo servicio para + +00:03:13.246 --> 00:03:19.466 align:middle +poder autocablearlo de la misma manera que +estamos autocableando servicios centrales + +00:03:19.816 --> 00:03:23.256 align:middle +como HtttpClientInterface y CacheInterface. + +00:03:24.046 --> 00:03:26.126 align:middle +Vaya, ¡tengo una sorpresa! + +00:03:26.486 --> 00:03:28.576 align:middle +Dirígete a tu terminal y ejecuta: + +00:03:28.576 --> 00:03:36.166 align:middle +php bin/console debug:autowiring --all +Desplázate hasta la parte superior y... + +00:03:36.786 --> 00:03:41.456 align:middle +¡sorpresa! ya es un servicio +en el contenedor! MixRepository + +00:03:42.146 --> 00:03:44.156 align:middle +Deja que te explique dos cosas. + +00:03:44.836 --> 00:03:49.406 align:middle +En primer lugar, la bandera +no es tan importante. --all + +00:03:49.906 --> 00:03:57.256 align:middle +Básicamente le dice a este comando que te muestre +los servicios principales como y , $httpClient $cache + +00:03:57.616 --> 00:04:01.426 align:middle +además de nuestros propios +servicios como MixRepository. + +00:04:02.396 --> 00:04:03.756 align:middle +En segundo lugar, el contenedor... + +00:04:03.876 --> 00:04:10.516 align:middle +de alguna manera ya vio nuestra clase de +repositorio y la reconoció como un servicio. + +00:04:11.176 --> 00:04:13.956 align:middle +Aprenderemos cómo ocurrió +eso en unos minutos... + +00:04:14.246 --> 00:04:20.816 align:middle +pero por ahora, basta con saber que nuestro nuevo +ya está dentro del MixRepository contenedor + +00:04:21.366 --> 00:04:25.786 align:middle +y que su id de servicio es el +nombre completo de la clase. + +00:04:26.346 --> 00:04:28.736 align:middle +¡Eso significa que podemos autoconducirlo! De + +00:04:29.646 --> 00:04:35.056 align:middle +vuelta a nuestro controlador, añade un +tercer argumento de tipo MixRepository - + +00:04:35.406 --> 00:04:38.436 align:middle +pulsa el tabulador para añadir la +declaración - y llámalo use... + +00:04:38.736 --> 00:04:40.526 align:middle +¿qué tal $mixRepository? + +00:04:41.306 --> 00:04:46.086 align:middle +Luego, aquí abajo, ya no necesitamos +nada de este código $mixes. + +00:04:46.636 --> 00:04:51.716 align:middle +Sustitúyelo por $mixes = +$mixRepository->findAll() + +00:04:52.276 --> 00:04:53.636 align:middle +¿Qué te parece? + +00:04:54.376 --> 00:04:55.446 align:middle +¿Funcionará? + +00:04:56.046 --> 00:04:56.736 align:middle +¡Averigüémoslo! + +00:04:57.346 --> 00:04:58.466 align:middle +Actualiza y... + +00:04:58.896 --> 00:05:03.446 align:middle +¡funciona! Vale, que funcione +en este caso significa + +00:05:03.446 --> 00:05:08.916 align:middle +que obtenemos un mensaje Undefined variable +$cache procedente de MixRepository. + +00:05:09.616 --> 00:05:16.406 align:middle +Pero el hecho de que nuestro código haya llegado hasta aquí +significa que el autocableado de MixRepository ha funcionado: + +00:05:16.806 --> 00:05:20.786 align:middle +el contenedor lo ha visto, +ha instanciado MixRepository + +00:05:21.216 --> 00:05:24.316 align:middle +y nos lo ha pasado para que podamos utilizarlo. + +00:05:25.146 --> 00:05:29.686 align:middle +¡Así que hemos creado un servicio y lo +hemos puesto a disposición del autocableado! + +00:05:30.186 --> 00:05:31.456 align:middle +¡Somos geniales! + +00:05:32.206 --> 00:05:39.326 align:middle +Pero nuestro nuevo servicio necesita los servicios +y para poder hacer su trabajo. $httpClient $cache + +00:05:40.006 --> 00:05:41.176 align:middle +¿Cómo los conseguimos? + +00:05:41.876 --> 00:05:48.706 align:middle +La respuesta es uno de los conceptos más importantes +de Symfony y de la codificación orientada a objetos + +00:05:48.706 --> 00:05:51.946 align:middle +en general: la inyección de dependencias. + +00:05:52.576 --> 00:05:54.266 align:middle +Vamos a hablar de ello a continuación diff --git a/sfcasts/ep2-fundamentals/es/custom-command.md b/sfcasts/ep2-fundamentals/es/custom-command.md new file mode 100644 index 0000000..f403ddd --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/custom-command.md @@ -0,0 +1,67 @@ +# Personalizar un comando + +¡Tenemos un nuevo comando de consola! Pero... todavía no hace mucho, aparte de imprimir un mensaje. Vamos a hacerlo más elegante. + +Desplázate hasta la parte superior. Aquí es donde tenemos el nombre de nuestro comando, y también hay una descripción... que aparece al lado del comando. Permíteme cambiar la nuestra por + +> Un comando autoconsciente que puede hacer... sólo una cosa. + +[[[ code('0665b762f7') ]]] + +## Configurar los argumentos y las opciones + +Nuestro comando se llama `app:talk-to-me` porque, cuando lo ejecutemos, quiero que sea posible pasar un nombre al comando -como Ryan- y que éste responda con "¡Hey Ryan!". Así que, literalmente, escribiremos `bin/console app:talk-to-me ryan` y nos responderá. + +Cuando quieres pasar un valor a un comando, eso se conoce como argumento... y se configuran abajo en... el método `configure()`. Ya hay un argumento llamado `arg1`... así que vamos a cambiarlo por `name`. + +Esta clave es completamente interna: nunca verás la palabra `name` cuando utilices este comando. Pero utilizaremos esta clave para leer el valor del argumento dentro de un minuto. También podemos dar una descripción al argumento y, si quieres, puedes hacerla obligatoria. Yo lo mantendré como opcional. + +Lo siguiente que tenemos son las opciones. Son como los argumentos... excepto que empiezan con un `--` cuando los utilizas. Quiero tener una bandera opcional en la que podamos decir `--yell` para que el comando grite nuestro nombre. + +En este caso, el nombre de la opción, `yell`, es importante: utilizaremos este nombre cuando pasemos la opción en la línea de comandos para utilizarla. El`InputOption::VALUE_NONE` significa que nuestra bandera será simplemente `--yell` y no`--yell=` algún valor. Si tu opción acepta un valor, lo cambiarías por`VALUE_REQUIRED`. Por último, dale una descripción. + +[[[ code('8001eef9d6') ]]] + +¡Precioso! Todavía no vamos a utilizar este argumento y esta opción... pero ya podemos volver a ejecutar nuestro comando con la opción `--help`: + +```terminal +php bin/console app:talk-to-me --help +``` + +Y... ¡impresionante! Vemos la descripción aquí arriba... junto con algunos detalles sobre cómo utilizar el argumento y la opción `--yell`. + +## Rellenando execute() + +Cuando llamemos a nuestro comando, de forma muy sencilla, Symfony llamará a `execute()`... que es donde empieza la diversión. Dentro, podemos hacer lo que queramos. Nos pasa dos argumentos: `$input` y `$output`. Si quieres leer alguna entrada -como el argumento`name` o la opción `yell` -, utiliza `$input`. Y si quieres dar salida a algo, utiliza `$output`. + +Pero en Symfony, normalmente metemos estas dos cosas en otro objeto llamado `SymfonyStyle`. Esta clase de ayuda hace que la lectura y la salida sean más fáciles... y más elegantes. + +Bien: empecemos por decir `$name = $input->getArgument('name')`. Si no tenemos un nombre, lo pondré por defecto en `whoever you are`. A continuación, lee la opción: `$shouldYell = $input->getOption('yell')`: + +[[[ code('c25283b263') ]]] + +Genial. Despejemos esto de aquí abajo y empecemos nuestro mensaje:`$message = sprintf('Hey %s!', $name)`. Luego, si queremos gritar, ya sabes qué hacer: `$message = strtoupper($message)`. Abajo, utiliza `$io->success()` y pon el mensaje ahí. + +[[[ code('e396b4e104') ]]] + +Este es uno de los muchos métodos de ayuda de la clase `SymfonyStyle` que ayudan a dar formato a tu salida. También está `$io->warning()`, `$io->note()`, y varios más. + +Vamos a probarlo. Gira y ejecuta: + +```terminal +php bin/console app:talk-to-me ryan +``` + +Y... ¡oh, hola! Si gritamos + +```terminal-silent +php bin/console app:talk-to-me ryan --yell +``` + +¡TAMBIÉN FUNCIONA! Incluso podemos gritar a "quienquiera que sea": + +```terminal-silent +php bin/console app:talk-to-me --yell +``` + +¡Impresionante! Pero vamos a volvernos más locos... autocableando un servicio y haciendo una pregunta de forma interactiva en la línea de comandos. Eso a continuación... ¡y es el último capítulo! diff --git a/sfcasts/ep2-fundamentals/es/custom-command.vtt b/sfcasts/ep2-fundamentals/es/custom-command.vtt new file mode 100644 index 0000000..2aa093d --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/custom-command.vtt @@ -0,0 +1,226 @@ +WEBVTT + +00:00:01.036 --> 00:00:02.946 align:middle +¡Tenemos un nuevo comando de consola! + +00:00:03.286 --> 00:00:07.426 align:middle +Pero... todavía no hace mucho, +aparte de imprimir un mensaje. + +00:00:07.876 --> 00:00:09.656 align:middle +Hagámoslo más elegante. + +00:00:10.016 --> 00:00:10.966 align:middle +Desplázate hasta la parte superior. + +00:00:11.776 --> 00:00:15.826 align:middle +Aquí es donde tenemos el nombre de nuestro +comando, y también hay una descripción... + +00:00:16.086 --> 00:00:17.696 align:middle +que aparece al lado del comando. + +00:00:18.446 --> 00:00:22.676 align:middle +Permíteme cambiar el nuestro por Un +comando autoconsciente que puede hacer... + +00:00:22.676 --> 00:00:23.716 align:middle +sólo una cosa. Nuestro comando + +00:00:24.686 --> 00:00:30.116 align:middle +se llama app:talk-to-me porque, cuando +lo ejecutemos, quiero que sea posible + +00:00:30.116 --> 00:00:36.486 align:middle +pasarle un nombre al comando -como Ryan- +y entonces responderá con "¡Hola Ryan!". + +00:00:37.116 --> 00:00:43.386 align:middle +Así que, literalmente, escribiremos bin/console +app:talk-to-me ryan y nos responderá. + +00:00:44.116 --> 00:00:48.746 align:middle +Cuando quieres pasar un valor a un +comando, eso se conoce como argumento... + +00:00:49.246 --> 00:00:50.996 align:middle +y éstos se configuran abajo en... + +00:00:50.996 --> 00:00:51.926 align:middle +el método configure(). + +00:00:52.716 --> 00:00:54.926 align:middle +Ya hay un argumento llamado arg1... + +00:00:55.116 --> 00:00:57.116 align:middle +así que vamos a cambiarlo por name. + +00:00:57.846 --> 00:01:03.226 align:middle +Esta clave es completamente interna: +nunca verás la palabra name + +00:01:03.336 --> 00:01:05.036 align:middle +cuando utilices este comando. + +00:01:05.386 --> 00:01:09.496 align:middle +Pero utilizaremos esta clave para leer el +valor del argumento dentro de un minuto. + +00:01:10.216 --> 00:01:15.606 align:middle +También podemos dar una descripción al argumento +y, si quieres, puedes hacerla obligatoria. + +00:01:16.176 --> 00:01:17.796 align:middle +Yo lo mantendré como opcional. + +00:01:18.716 --> 00:01:21.136 align:middle +Lo siguiente que tenemos son las opciones. + +00:01:21.806 --> 00:01:23.196 align:middle +Son como los argumentos... + +00:01:23.446 --> 00:01:27.026 align:middle +salvo que empiezan con un +-- cuando los utilizas. + +00:01:27.676 --> 00:01:34.306 align:middle +Quiero tener una bandera opcional en la que podamos +decir --yell para que el comando grite nuestro nombre. + +00:01:34.306 --> 00:01:40.916 align:middle +En este caso, el nombre de la opción, yell, +es importante: utilizaremos este nombre + +00:01:40.916 --> 00:01:43.966 align:middle +cuando pasemos la opción en la +línea de comandos para utilizar la. + +00:01:44.776 --> 00:01:49.176 align:middle +El InputOption::VALUE_NONE significa +que nuestra bandera será simplemente + +00:01:49.776 --> 00:01:54.106 align:middle +--yell y no --yell= algún valor. + +00:01:54.756 --> 00:01:59.216 align:middle +Si tu opción acepta un +valor, lo cambiarías por + +00:02:00.156 --> 00:02:02.106 align:middle +VALUE_REQUIRED. Por último, dale una + +00:02:03.636 --> 00:02:04.286 align:middle +descripción. + +00:02:04.816 --> 00:02:07.576 align:middle +¡Precioso! Todavía no vamos a +utilizar este argumento y esta opción.. + +00:02:08.006 --> 00:02:10.966 align:middle +. pero ya podemos volver a +ejecutar nuestro comando con + +00:02:11.206 --> 00:02:14.636 align:middle +la opción --help: php +bin/console app:talk-to-me --help + +00:02:14.636 --> 00:02:19.736 align:middle +Y... ¡impresionante! Vemos la descripción + +00:02:20.046 --> 00:02:25.016 align:middle +aquí arriba... junto con algunos detalles sobre +cómo utilizar el argumento y la opción --yell. + +00:02:25.916 --> 00:02:30.206 align:middle +Cuando llamemos a nuestro comando, de +forma muy sencilla, Symfony llamará a + +00:02:30.706 --> 00:02:32.746 align:middle +execute()... que es donde +empieza la diversión. + +00:02:33.346 --> 00:02:36.176 align:middle +Dentro, podemos hacer lo que + +00:02:36.716 --> 00:02:39.566 align:middle +queramos. Nos pasa dos argumentos: $input y + +00:02:40.416 --> 00:02:45.636 align:middle +$output. Si quieres leer alguna entrada, como +el argumento name o la opción yell, utiliza + +00:02:46.276 --> 00:02:49.366 align:middle +$input. Y si quieres dar salida a algo, utiliza + +00:02:50.216 --> 00:02:56.526 align:middle +$output. Pero en Symfony, normalmente metemos +estas dos cosas en otro objeto llamado + +00:02:57.276 --> 00:03:00.216 align:middle +SymfonyStyle. Esta clase de ayuda +hace que la lectura y la salida sean + +00:03:00.406 --> 00:03:01.666 align:middle +más fáciles... y + +00:03:02.456 --> 00:03:06.896 align:middle +más elegantes. Bien: +empecemos diciendo $name = + +00:03:07.776 --> 00:03:11.556 align:middle +$input->getArgument('name'). Si no tenemos +un nombre, lo pondré por defecto en whoever you + +00:03:12.506 --> 00:03:17.226 align:middle +are. A continuación, lee +la opción: $shouldYell = + +00:03:18.256 --> 00:03:22.216 align:middle +$input->getOption('yell'): Genial. Despejemos +esto de aquí abajo y empecemos nuestro + +00:03:22.916 --> 00:03:27.216 align:middle +mensaje: $message = sprintf('Hey %s!', + +00:03:28.246 --> 00:03:34.616 align:middle +$name). Luego, si queremos gritar, +ya sabes qué hacer: $message = + +00:03:35.556 --> 00:03:40.156 align:middle +strtoupper($message). Abajo , utiliza +$io->success() y pon el mensaje + +00:03:40.906 --> 00:03:46.386 align:middle +ahí. Este es uno de los muchos métodos de ayuda de +la clase SymfonyStyle que ayudan a dar formato a tu + +00:03:47.116 --> 00:03:51.516 align:middle +salida. También está $io->warning(), +$io->note(), y varios + +00:03:52.276 --> 00:03:52.776 align:middle +más. Vamos a probarlo + +00:03:53.246 --> 00:03:58.156 align:middle +. Gira y ejecuta: php +bin/console app:talk-to-me ryan + +00:03:58.576 --> 00:03:59.836 align:middle +Y... ¡oh, hola + +00:04:00.396 --> 00:04:03.606 align:middle +! Si gritamos: ¡Eso también funciona + +00:04:04.326 --> 00:04:07.626 align:middle +! Incluso podemos gritar +a "quienquiera que sea": + +00:04:08.376 --> 00:04:09.856 align:middle +¡Impresionante! Pero pongámonos + +00:04:10.276 --> 00:04:15.686 align:middle +más locos... autocableando un servicio y haciendo una pregunta de +forma interactiva en la línea de comandos . Eso a continuación... + +00:04:17.276 --> 00:04:19.046 align:middle +y es lo último diff --git a/sfcasts/ep2-fundamentals/es/debug-container.md b/sfcasts/ep2-fundamentals/es/debug-container.md new file mode 100644 index 0000000..1b8f69d --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/debug-container.md @@ -0,0 +1,67 @@ +# debug:container y cómo funciona el autowiring + +Vale, he mentido. Antes de hablar de los entornos, tengo que confesar algo: no te he enseñado todos los servicios de Symfony. Ni de lejos. + +Dirígete a tu terminal y ejecuta nuestro comando favorito: + +```terminal +php bin/console debug:autowiring +``` + +Sabemos que todos estos servicios están flotando en Symfony, esperando que los pidamos. Y sabemos que los bundles nos dan servicios. El servicio Twig de aquí abajo proviene de TwigBundle. + +Y como cada servicio es un objeto, algo en algún lugar debe ser responsable de instanciar estos objetos. La pregunta es: "¿Quién?" Y la respuesta es... ¡el contenedor de servicios! + +## Hola contenedor de servicios + +Resulta que todos los servicios no están realmente... "flotando": todos viven dentro de algo llamado "contenedor". Y hay muchos más servicios en el contenedor de los que nos ha contado `debug:autowiring`. Ooh... ¡secretos! Esta vez, ejecuta: + +```terminal +php bin/console debug:container +``` + +Y... ¡guau! Esto imprime una lista enorme. Es tan grande que es difícil verlo todo. Déjame hacer la fuente más pequeña. ¡Mucho mejor! + +Esta es la lista completa de todos los servicios de nuestra aplicación... o del "contenedor". El contenedor es básicamente un "array" gigante donde cada servicio tiene un nombre único que apunta a su objeto de servicio. Por ejemplo, aquí abajo... ahí vamos... podemos ver que hay un servicio cuyo nombre único - o "id" es `twig`. + +Saber que el id del servicio Twig es `twig` no suele ser importante, pero es útil entender que cada servicio tiene un id único... y que puedes verlos todos dentro del comando `debug:container`. + +## El contenedor crea objetos + +En realidad, el contenedor podría describirse mejor como un gran conjunto de instrucciones sobre cómo instanciar servicios, siempre y cuando algo los pida. Por ejemplo, el contenedor sabe exactamente cómo instanciar este servicio Twig. Sabe que su clase es `Twig\Environment`. Y aunque no lo veas en esta lista, conoce los argumentos exactos que debe pasar a su constructor. En el momento en que alguien necesite el servicio Twig, el contenedor lo instanciará y lo devolverá. + +Sí, cuando autocontratamos un servicio, básicamente estamos diciendo + +> Oye, contenedor, ¿puedes darme el servicio Cliente HTTP? + +Si nada en nuestro código ha pedido aún ese servicio durante esta petición, el contenedor lo creará. Pero si algo ya lo ha pedido, entonces el contenedor simplemente devolverá el que ya ha creado. Esto significa que si pedimos el servicio Cliente HTTP en diez lugares diferentes, el contenedor sólo creará y devolverá la misma instancia. ¡Muy bonito! + +## Cómo funciona el autocableado + +De todos modos, `debug:container` nos muestra todos los servicios que el contenedor sabe instanciar. Pero `debug:autowiring` sólo nos muestra una parte de esos servicios. ¿Por qué? + +Bueno, resulta que no todos los servicios son autocableables. Muchos de los elementos de esta lista son servicios de bajo nivel que sólo existen para ayudar a otros servicios a hacer su trabajo. Probablemente nunca necesitarás utilizar estos servicios de bajo nivel directamente... y en realidad no puedes obtenerlos mediante autoconexión. + +Pero, retrocedamos un momento. Ahora que sabemos un poco más, podemos saber exactamente cómo funciona el sistema de autoconexión de Symfony. Es maravillosamente sencillo. + +Como hemos visto, el contenedor es realmente un array donde cada servicio tiene un id que apunta a ese objeto servicio. Cuando Symfony ve este tipo `HttpClientInterface`-este es el tipo completo que ve, gracias a nuestra declaración `use` - para averiguar qué servicio del contenedor debe pasarnos, simplemente busca un servicio cuyo ID coincida exactamente con esta cadena. ¡Deja que te lo enseñe! + +Desplázate hacia la parte superior de la lista para encontrar... ¡un servicio cuyo ID es`Symfony\Contracts\HttpClient\HttpClientInterface`! La gran mayoría de los servicios del contenedor utilizan la estrategia de nomenclatura "caso serpiente". Pero si un servicio está pensado para que lo utilicemos en nuestro código, Symfony añadirá dentro un servicio adicional que coincida con su nombre de clase o interfaz. + +Gracias a ello, cuando escribimos `HttpClientInterface`, Symfony busca en el contenedor un servicio cuyo id es`Symfony\Contracts\HttpClient\HttpClientInterface`, lo encuentra y nos lo pasa. + +## Alias de servicio + +Pero fíjate en el lado derecho: dice que se trata de un alias para un ID de servicio diferente. Un "alias" es como un enlace simbólico. Significa que cuando alguien pregunte por el servicio `HttpClientInterface`, Symfony nos pasará en realidad este otro servicio. + +Podemos utilizar la misma lógica aquí abajo para el tipo `CacheInterface`. Si comprobamos la lista, aquí está el servicio cuyo id coincide con ese tipo. Pero, en realidad, es sólo un alias de un servicio llamado `cache.app`. Así que cuando autoconectamos `CacheInterface`, el servicio `cache.app` es lo que realmente se nos pasa. + +Si te sientes inseguro, aquí tienes las tres grandes conclusiones. Uno: hay un montón de objetos de servicio flotando por ahí y todos viven dentro de algo llamado "contenedor". Cada servicio tiene un identificador único. + +Dos: sólo un pequeño porcentaje de ellos nos resulta útil... y esos están configurados para que podamos autocablearlos. El autocableado funciona buscando en el contenedor un servicio cuyo id coincida exactamente con el tipo. Cuando ejecutamos `debug:autowiring`, básicamente nos muestra los servicios de esta lista cuyo id es un nombre de clase o interfaz. Ésos son los "servicios autoconducibles". + +La tercera y última conclusión es que los servicios también tienen un sistema de alias... lo que significa que cuando pedimos el servicio `CacheInterface`, lo que realmente nos dará es el servicio cuyo id es `cache.app`. + +Si te preguntas cómo podríamos utilizar en nuestro código un servicio no autoconvocable, ¡es una gran pregunta! Es algo raro, pero aprenderemos a hacerlo más adelante. + +A continuación, vamos a hablar de la utilización de una configuración diferente a nivel local y a nivel de producción. Vamos a hablar de los entornos. diff --git a/sfcasts/ep2-fundamentals/es/debug-container.vtt b/sfcasts/ep2-fundamentals/es/debug-container.vtt new file mode 100644 index 0000000..1b2c907 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/debug-container.vtt @@ -0,0 +1,373 @@ +WEBVTT + +00:00:01.016 --> 00:00:02.576 align:middle +Vale, he mentido. + +00:00:02.916 --> 00:00:06.686 align:middle +Antes de hablar de los entornos, +tengo que confesar algo: + +00:00:07.136 --> 00:00:12.016 align:middle +No te he mostrado todos +los servicios de Symfony. + +00:00:12.286 --> 00:00:13.926 align:middle +Ni de lejos. + +00:00:14.716 --> 00:00:17.496 align:middle +Dirígete a tu terminal y +ejecuta nuestro comando favorito: + +00:00:17.496 --> 00:00:23.686 align:middle +php bin/console debug:autowiring Sabemos +que todos estos servicios están flotando + +00:00:23.686 --> 00:00:27.186 align:middle +en Symfony, esperando a que los pidamos. + +00:00:27.796 --> 00:00:30.546 align:middle +Y sabemos que los bundles nos dan servicios. + +00:00:30.936 --> 00:00:33.846 align:middle +El servicio Twig de aquí +abajo proviene de TwigBundle. + +00:00:34.446 --> 00:00:37.336 align:middle +Y como cada servicio es un objeto, + +00:00:37.736 --> 00:00:43.666 align:middle +algo en algún lugar debe ser +responsable de instanciar estos objetos. + +00:00:44.246 --> 00:00:46.406 align:middle +La pregunta es: "¿Quién?" + +00:00:46.926 --> 00:00:48.456 align:middle +Y la respuesta es... + +00:00:48.776 --> 00:00:50.216 align:middle +¡el contenedor de servicios! + +00:00:50.986 --> 00:00:53.826 align:middle +Resulta que todos los servicios +no están realmente... + +00:00:53.976 --> 00:00:58.746 align:middle +"flotando": todos viven dentro +de algo llamado "contenedor". + +00:00:59.336 --> 00:01:05.936 align:middle +Y hay muchos más servicios en el contenedor +de los que nos ha contado debug:autowiring. + +00:01:06.366 --> 00:01:07.816 align:middle +Ooh... ¡secretos! + +00:01:08.646 --> 00:01:13.486 align:middle +Esta vez, ejecuta: php +bin/console debug:container Y... + +00:01:13.746 --> 00:01:17.246 align:middle +esto imprime una lista enorme. + +00:01:17.756 --> 00:01:20.446 align:middle +Es tan grande que es difícil ver todo. + +00:01:20.946 --> 00:01:22.496 align:middle +Déjame hacer la fuente más pequeña. + +00:01:23.086 --> 00:01:23.616 align:middle +¡Mucho mejor! + +00:01:24.376 --> 00:01:28.266 align:middle +Esta es la lista completa de todos los +servicios de nuestra aplicación... + +00:01:28.586 --> 00:01:30.416 align:middle +o en el "contenedor". + +00:01:31.046 --> 00:01:37.516 align:middle +El contenedor es básicamente un "array" gigante +donde cada servicio tiene un nombre único + +00:01:37.906 --> 00:01:40.256 align:middle +que apunta a su objeto de servicio. + +00:01:40.906 --> 00:01:42.506 align:middle +Por ejemplo, aquí abajo... + +00:01:43.616 --> 00:01:44.206 align:middle +ahí vamos... + +00:01:44.706 --> 00:01:49.956 align:middle +podemos ver que hay un servicio +cuyo nombre único -o "id"- es twig. + +00:01:50.636 --> 00:01:56.736 align:middle +Saber que el id del servicio Twig +es twig no suele ser importante, + +00:01:57.216 --> 00:02:02.326 align:middle +pero es útil entender que cada +servicio tiene un id único... + +00:02:02.936 --> 00:02:06.966 align:middle +y que puedes verlos todos dentro +del comando debug:container. + +00:02:07.616 --> 00:02:14.896 align:middle +Y, en realidad, el contenedor podría describirse +mejor como un gran conjunto de instrucciones + +00:02:15.016 --> 00:02:20.626 align:middle +sobre cómo instanciar servicios, +siempre y cuando algo los pida. + +00:02:21.376 --> 00:02:27.046 align:middle +Por ejemplo, el contenedor sabe exactamente +cómo instanciar este servicio Twig. + +00:02:27.646 --> 00:02:30.816 align:middle +Sabe que su clase es Twig\Environment. + +00:02:31.246 --> 00:02:34.056 align:middle +Y aunque no lo veas en esta lista, + +00:02:34.546 --> 00:02:38.626 align:middle +conoce los argumentos exactos +que debe pasar a su constructor. + +00:02:39.316 --> 00:02:45.246 align:middle +En el momento en que alguien necesite el servicio +Twig, el contenedor lo instanciará y lo devolverá. + +00:02:45.956 --> 00:02:50.816 align:middle +Sí, cuando autocontratamos un servicio, +básicamente estamos diciendo Oye, contenedor, + +00:02:51.116 --> 00:02:54.466 align:middle +¿puedes darme el servicio Cliente HTTP? + +00:02:55.076 --> 00:02:58.176 align:middle +Si nada en nuestro código +ha pedido aún ese servicio + +00:02:58.176 --> 00:03:02.136 align:middle +durante esta petición , +el contenedor lo creará. + +00:03:02.716 --> 00:03:05.406 align:middle +Pero si algo ya lo ha pedido, + +00:03:05.786 --> 00:03:10.896 align:middle +entonces el contenedor simplemente +devolverá el que ya ha creado. + +00:03:11.576 --> 00:03:17.446 align:middle +Esto significa que si pedimos el servicio +Cliente HTTP en diez lugares diferentes, + +00:03:17.846 --> 00:03:23.076 align:middle +el contenedor sólo creará y +devolverá la misma instancia. + +00:03:23.406 --> 00:03:24.026 align:middle +¡Bastante bien! De + +00:03:24.616 --> 00:03:28.616 align:middle +todos modos, debug:container +nos muestra todos los servicios + +00:03:28.676 --> 00:03:30.926 align:middle +que el contenedor sabe instanciar. + +00:03:31.676 --> 00:03:37.546 align:middle +Pero debug:autowiring sólo nos +muestra una parte de esos servicios. + +00:03:38.076 --> 00:03:44.086 align:middle +¿Por qué? Bueno, resulta que no todos +los servicios son autoconvocables. + +00:03:44.746 --> 00:03:49.656 align:middle +Muchos de los elementos de esta lista son +servicios de bajo nivel que sólo existen + +00:03:49.656 --> 00:03:52.566 align:middle +para ayudar a otros +servicios a hacer su trabajo. + +00:03:53.206 --> 00:03:56.746 align:middle +Probablemente nunca necesitarás utilizar +estos servicios de bajo nivel directamente... + +00:03:57.186 --> 00:04:01.456 align:middle +y, de hecho, no puedes obtenerlos +mediante autoconexión. + +00:04:02.316 --> 00:04:04.236 align:middle +Pero, retrocedamos un minuto. + +00:04:04.776 --> 00:04:11.526 align:middle +Ahora que sabemos un poco más, podemos saber exactamente +cómo funciona el sistema de autoconexión de Symfony. + +00:04:11.876 --> 00:04:13.386 align:middle +Es maravillosamente sencillo. Como hemos + +00:04:14.186 --> 00:04:19.866 align:middle +visto, el contenedor es realmente un +array donde cada servicio tiene un id + +00:04:20.376 --> 00:04:22.516 align:middle +que apunta a ese objeto servicio. + +00:04:23.336 --> 00:04:29.236 align:middle +Cuando Symfony ve este tipo HttpClientInterface +-este es el tipo completo que ve, + +00:04:29.376 --> 00:04:35.076 align:middle +gracias a nuestra declaración use - para +averiguar qué servicio del contenedor + +00:04:35.076 --> 00:04:42.646 align:middle +debe pasarnos, simplemente busca un servicio +cuyo ID coincida exactamente con esta cadena. + +00:04:43.296 --> 00:04:43.936 align:middle +¡Deja que te lo enseñe! + +00:04:44.546 --> 00:04:47.666 align:middle +Desplázate hacia la parte superior +de esta lista para encontrar... + +00:04:48.196 --> 00:04:53.936 align:middle +¡un servicio cuyo ID es +Symfony\Contracts\HttpClient\HttpClientInterface! + +00:04:54.146 --> 00:04:56.666 align:middle +La + +00:04:57.306 --> 00:05:03.056 align:middle +gran mayoría de los servicios del contenedor utilizan +la estrategia de nomenclatura "caso serpiente". Pero + +00:05:03.586 --> 00:05:07.996 align:middle +si un servicio está pensado para que lo +utilicemos en nuestro código, Symfony + +00:05:08.486 --> 00:05:14.946 align:middle +añadirá dentro un servicio adicional que +coincida con su nombre de clase o interfaz. Gracias + +00:05:15.746 --> 00:05:19.656 align:middle +a eso, cuando escribimos +HttpClientInterface, Symfony + +00:05:20.086 --> 00:05:26.966 align:middle +busca en el contenedor un servicio cuyo id es +Symfony\Contracts\HttpClient\HttpClientInterface, + +00:05:27.136 --> 00:05:32.446 align:middle +lo encuentra y nos lo pasa. Pero mira a la + +00:05:33.116 --> 00:05:39.736 align:middle +derecha: dice que es un alias para un +ID de servicio diferente. Un "alias" + +00:05:39.736 --> 00:05:43.226 align:middle +es como un enlace simbólico. Significa + +00:05:43.746 --> 00:05:47.876 align:middle +que cuando alguien pregunte por el +servicio HttpClientInterface, Symfony + +00:05:48.206 --> 00:05:52.046 align:middle +nos pasará en realidad +este otro servicio. Podemos + +00:05:52.846 --> 00:05:56.296 align:middle +utilizar la misma lógica aquí +abajo para el tipo CacheInterface. Si + +00:05:57.146 --> 00:06:02.336 align:middle +comprobamos la lista, aquí está el servicio +cuyo id coincide con ese tipo. Pero, en + +00:06:02.816 --> 00:06:07.676 align:middle +realidad, es sólo un alias de un servicio +llamado cache.app. Así que cuando + +00:06:09.036 --> 00:06:11.576 align:middle +autoconectamos CacheInterface, +el servicio cache.app + +00:06:11.886 --> 00:06:16.456 align:middle +es lo que realmente se nos pasa. Si te + +00:06:17.246 --> 00:06:21.316 align:middle +sientes inseguro, aquí tienes las +tres grandes conclusiones. Uno: + +00:06:21.976 --> 00:06:25.916 align:middle +hay un montón de objetos de +servicio flotando por ahí y + +00:06:26.296 --> 00:06:29.386 align:middle +todos viven dentro de algo llamado +"contenedor". Cada servicio + +00:06:29.946 --> 00:06:32.556 align:middle +tiene un identificador único. Dos: sólo + +00:06:33.486 --> 00:06:37.686 align:middle +un pequeño porcentaje de ellos nos son +útiles... y esos están configurados para que + +00:06:38.086 --> 00:06:41.786 align:middle +podamos autocablearlos. +El autocableado funciona + +00:06:42.356 --> 00:06:48.496 align:middle +buscando en el contenedor un servicio cuyo +id coincida exactamente con el tipo. Cuando + +00:06:49.306 --> 00:06:53.796 align:middle +ejecutamos debug:autowiring, básicamente nos +muestra los servicios de esta lista cuyo id + +00:06:53.796 --> 00:06:58.316 align:middle +es un nombre de clase o interfaz. Ésos son + +00:06:58.836 --> 00:07:01.326 align:middle +los "servicios autoconducibles". La tercera + +00:07:02.216 --> 00:07:07.686 align:middle +y última conclusión es que los servicios +también tienen un sistema de alias... lo que + +00:07:08.146 --> 00:07:12.136 align:middle +significa que cuando pedimos el +servicio CacheInterface, lo que + +00:07:12.576 --> 00:07:18.436 align:middle +realmente nos dará es el +servicio cuyo id es cache.app. Si + +00:07:19.246 --> 00:07:24.416 align:middle +te preguntas cómo podríamos utilizar +un servicio no autoconectable en nuestro + +00:07:24.416 --> 00:07:26.886 align:middle +código, ¡es una gran pregunta! Es algo + +00:07:27.476 --> 00:07:31.626 align:middle +raro, pero aprenderemos a hacerlo más +adelante. A continuación, vamos a + +00:07:32.516 --> 00:07:37.486 align:middle +hablar de la utilización de una configuración diferente +a nivel local y a nivel de producción. Hablemos + +00:07:37.946 --> 00:07:40.236 align:middle +de los entornos diff --git a/sfcasts/ep2-fundamentals/es/dependency-injection.md b/sfcasts/ep2-fundamentals/es/dependency-injection.md new file mode 100644 index 0000000..8cfa115 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/dependency-injection.md @@ -0,0 +1,86 @@ +# Inyección de dependencia + +Nuestro servicio `MixRepository` está más o menos funcionando. Podemos autoinstalarlo en nuestro controlador y el contenedor está instanciando el objeto y pasándonoslo. Lo comprobamos aquí porque, cuando ejecutamos el código, llama con éxito al método `findAll()`. + +Pero .... luego explota. Eso es porque, dentro de `MixRepository` tenemos dos variables indefinidas. Para que nuestra clase haga su trabajo, necesita dos servicios: el servicio`$cache` y el servicio `$httpClient`. + +## La autoconexión con los métodos es un superpoder exclusivo de los controladores + +Sigo diciendo que hay muchos servicios flotando dentro de Symfony, esperando que los usemos. Eso es cierto. Pero, no puedes cogerlos de la nada desde cualquier parte de tu código. Por ejemplo, no hay ningún método estático de `Cache::get()` que puedas llamar cuando quieras y que devuelva el objeto de servicio `$cache`. No existe nada así en Symfony. ¡Y eso es bueno! Permitirnos coger objetos de la nada es una receta para escribir mal código. + +Entonces, ¿cómo podemos acceder a estos servicios? Actualmente, sólo conocemos una forma: autocableándolos en nuestro controlador. Pero eso no funcionará aquí. El autocableado de servicios en un método es un superpoder que sólo funciona para los controladores. + +Fíjate: si añadimos un argumento `CacheInterface`... y luego pasamos y refrescamos, veríamos: + +> Demasiados argumentos para la función [...]findAll(), 0 pasados [...] y exactamente 1 esperado. + +Eso es porque estamos llamando a `findAll()`. Así que si `findAll()` necesita un argumento, es nuestra responsabilidad pasarlo: no hay magia de Symfony. Lo que quiero decir es que el autocableado funciona en los métodos del controlador, pero no esperes que funcione en ningún otro método. + +## ¿Pasar manualmente los servicios a un método? + +Una forma de conseguir que esto funcione es añadir ambos servicios al método`findAll()` y luego pasarlos manualmente desde el controlador. Esta no será la solución definitiva, pero vamos a probarla. + +Ya tengo un argumento `CacheInterface`... así que ahora añade el argumento`HttpClientInterface` y llámalo `$httpClient`. + +¡Perfecto! El código de este método está ahora contento. + +De vuelta a nuestro controlador, para `findAll()`, pasa `$httpClient` y `$cache`. + +Y ahora... ¡funciona! + +## "Dependencias" frente a "Argumentos" + +Así que, a alto nivel, esta solución tiene sentido. Sabemos que podemos autoconectar servicios en nuestro controlador... y luego simplemente los pasamos a `MixRepository`. Pero si piensas un poco más en profundidad, los servicios `$httpClient` y `$cache` no son realmente una entrada para la función `findAll()`. No tienen realmente sentido como argumentos. + +Veamos un ejemplo. Imagina que decidimos cambiar el método `findAll()` para que acepte un argumento `string $genre` para que el método sólo devuelva mezclas de ese género. Este argumento tiene mucho sentido: al pasar diferentes géneros cambia lo que devuelve. El argumento controla el comportamiento del método. + +Pero los argumentos `$httpClient` y `$cache` no controlan el comportamiento de la función. En realidad, pasaríamos estos dos mismos valores cada vez que llamemos al método... para que las cosas funcionen. + +En lugar de argumentos, son realmente dependencias que el servicio necesita. ¡Son cosas que deben estar disponibles para que `findAll()` pueda hacer su trabajo! + +## Inyección de dependencias y el constructor + +Para las "dependencias" como ésta, ya sean objetos de servicio o configuración estática que necesita tu servicio, en lugar de pasarlas a los métodos, las pasamos al constructor. Elimina ese supuesto argumento de `$genre`... y añade un `public function __construct()`. Copia los dos argumentos, bórralos y muévelos hasta aquí. + +Antes de terminar esto, tengo que decirte que el autocableado funciona en dos sitios. Ya sabemos que podemos autoconectar argumentos en los métodos de nuestro controlador. Pero también podemos autoconectar argumentos en el método `__construct()` de cualquier servicio. De hecho, ¡éste es el lugar principal en el que se supone que funciona la autoconexión! El hecho de que la autoconexión también funcione en los métodos del controlador es... una especie de "extra" para hacer la vida más agradable. + +En cualquier caso, la autoconexión funciona en el método `__construct()` de nuestros servicios. Así que, siempre que indiquemos los argumentos (y lo hemos hecho), cuando Symfony instancie nuestro servicio, nos pasará estos dos servicios. ¡Sí! + +¿Y qué hacemos con estos dos argumentos? Los establecemos en propiedades. + +Creamos una propiedad `private $httpClient` y una propiedad `private $cache`. Luego, abajo, en el constructor, les asignamos: `$this->httpClient = $httpClient`, y`$this->cache = $cache`. + +Así, cuando Symfony instancie nuestro `MixRepository`, nos pasará estos dos argumentos y los almacenaremos en propiedades para poder utilizarlos después. + +¡Observa! Aquí abajo, en lugar de `$cache`, utiliza `$this->cache`. Y entonces no necesitamos este `use ($httpClient)` de aquí... porque podemos decir `$this->httpClient`. + +Este servicio está ahora en perfecto estado. + +De vuelta a `VinylController`, ¡ahora podemos simplificar! El método `findAll()`no necesita ningún argumento... y así ni siquiera necesitamos autoconducir`$httpClient` o `$cache` en absoluto. Voy a celebrarlo eliminando esas declaraciones `use`de la parte superior. + +¡Mira qué fácil es! Autocableamos el único servicio que necesitamos, llamamos al método en él, y... ¡hasta funciona! Así es como escribimos servicios. Añadimos las dependencias al constructor, las establecemos en las propiedades y luego las utilizamos. + +## ¡Hola a la inyección de dependencias! + +Por cierto, lo que acabamos de hacer tiene un nombre extravagante: "Inyección de dependencias", ¡pero no huyas! Puede que sea un término que asuste... o que al menos suene "aburrido", pero es un concepto muy sencillo. + +Cuando estás dentro de un servicio como `MixRepository` y te das cuenta de que necesitas otro servicio (o tal vez alguna configuración como una clave de API), para obtenerlo, crea un constructor, añade un argumento para lo que necesitas, ponlo en una propiedad, y luego úsalo abajo en tu código. Sí Eso es la inyección de dependencia. + +En pocas palabras, la inyección de dependencia dice + +> Si necesitas algo, en lugar de cogerlo de la nada, obliga a Symfony a +> te lo pase a través del constructor. + +Este es uno de los conceptos más importantes de Symfony... y lo haremos una y otra vez. + +## Promoción de propiedades en PHP 8 + +Bien, sin relación con la inyección de dependencia y el autocableado, hay dos pequeñas mejoras que podemos hacer a nuestro servicio. La primera es que podemos añadir tipos a nuestras propiedades: `HttpClientInterface` y `CacheInterface`. Eso no cambia el funcionamiento de nuestro código... es sólo una forma agradable y responsable de hacer las cosas. + +¡Pero podemos ir más allá! En PHP 8, hay una nueva sintaxis más corta para crear una propiedad y establecerla en el constructor como estamos haciendo. Tiene el siguiente aspecto. En primer lugar, moveré mis argumentos a varias líneas... sólo para mantener las cosas organizadas. Ahora añade la palabra `private` delante de cada argumento. Termina borrando las propiedades... así como el interior del método. + +Esto puede parecer raro al principio, pero en cuanto añades `private`, `protected`, o`public` delante de un argumento `__construct()`, se crea una propiedad con este nombre y se fija el argumento en esa propiedad. Así que parece diferente, pero es exactamente lo mismo que teníamos antes. + +Cuando lo probamos... ¡sí! Sigue funcionando. + +Siguiente: Sigo diciendo que el contenedor contiene servicios. Es cierto Pero también contiene otra cosa: una simple configuración llamada "parámetros". diff --git a/sfcasts/ep2-fundamentals/es/dependency-injection.vtt b/sfcasts/ep2-fundamentals/es/dependency-injection.vtt new file mode 100644 index 0000000..e46d61d --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/dependency-injection.vtt @@ -0,0 +1,467 @@ +WEBVTT + +00:00:00.996 --> 00:00:04.816 align:middle +Nuestro servicio MixRepository +está funcionando más o menos. + +00:00:05.186 --> 00:00:07.346 align:middle +Podemos autoconectarlo a nuestro controlador + +00:00:07.426 --> 00:00:11.626 align:middle +y el contenedor está instanciando +el objeto y pasándonoslo. + +00:00:12.376 --> 00:00:16.216 align:middle +Lo comprobamos aquí porque, +cuando ejecutamos el código, + +00:00:16.516 --> 00:00:19.316 align:middle +llama con éxito al método findAll(). + +00:00:19.876 --> 00:00:22.846 align:middle +Pero .... luego explota. + +00:00:23.346 --> 00:00:29.276 align:middle +Eso es porque, dentro de MixRepository +tenemos dos variables indefinidas. + +00:00:30.016 --> 00:00:34.866 align:middle +Para que nuestra clase haga su +trabajo, necesita dos servicios: + +00:00:35.206 --> 00:00:39.136 align:middle +el servicio $cache y el servicio $httpClient. + +00:00:39.806 --> 00:00:44.176 align:middle +Sigo diciendo que hay muchos +servicios flotando dentro + +00:00:44.176 --> 00:00:46.566 align:middle +de Symfony, esperando a que los usemos. + +00:00:47.116 --> 00:00:48.286 align:middle +Eso es cierto. + +00:00:48.716 --> 00:00:54.156 align:middle +Pero, no puedes cogerlos de la nada +desde cualquier parte de tu código. Por + +00:00:54.676 --> 00:01:02.466 align:middle +ejemplo, no hay ningún método estático de +Cache::get() que puedas llamar cuando quieras + +00:01:02.686 --> 00:01:05.676 align:middle +y que devuelva el objeto de servicio $cache. + +00:01:06.146 --> 00:01:09.006 align:middle +No existe nada parecido en Symfony. + +00:01:09.446 --> 00:01:10.486 align:middle +¡Y eso es bueno! + +00:01:11.066 --> 00:01:16.316 align:middle +Permitirnos coger objetos de la nada es +una receta para escribir mal código. + +00:01:17.056 --> 00:01:20.496 align:middle +Entonces, ¿cómo podemos +acceder a estos servicios? + +00:01:21.246 --> 00:01:26.226 align:middle +Actualmente, sólo conocemos una forma: +autocableándolos en nuestro controlador. + +00:01:26.876 --> 00:01:29.396 align:middle +Pero eso no funcionará aquí. + +00:01:30.126 --> 00:01:36.446 align:middle +El autocableado de servicios en un método es un +superpoder que sólo funciona para los controladores. + +00:01:36.976 --> 00:01:40.546 align:middle +Observa: si añadimos un +argumento CacheInterface... + +00:01:41.946 --> 00:01:47.426 align:middle +y luego pasamos y refrescamos, veríamos: Demasiados +argumentos para la función [...]findAll(), + +00:01:47.426 --> 00:01:49.446 align:middle +0 pasados [...] y + +00:01:49.446 --> 00:01:51.366 align:middle +exactamente 1 esperado. Eso es porque + +00:01:52.036 --> 00:01:54.986 align:middle +estamos llamando a findAll(). Así que si + +00:01:55.506 --> 00:01:59.936 align:middle +findAll() necesita un argumento, +es nuestra responsabilidad pasarlo + +00:01:59.936 --> 00:02:03.426 align:middle +: no hay magia de Symfony. Lo que quiero + +00:02:04.346 --> 00:02:08.296 align:middle +decir es que el autocableado funciona +en los métodos del controlador, pero no + +00:02:08.576 --> 00:02:12.126 align:middle +esperes que funcione en +ningún otro método. Pero una + +00:02:12.976 --> 00:02:20.106 align:middle +forma de conseguir que esto funcione es añadir +ambos servicios al método findAll() y luego + +00:02:20.576 --> 00:02:23.996 align:middle +pasarlos manualmente desde +el controlador. Esta no + +00:02:24.586 --> 00:02:28.026 align:middle +será la solución definitiva, +pero vamos a probarla. Ya + +00:02:28.666 --> 00:02:31.266 align:middle +tengo un argumento +CacheInterface... así que ahora + +00:02:31.806 --> 00:02:37.236 align:middle +añade el argumento HttpClientInterface +y llámalo $httpClient. ¡Perfecto! El + +00:02:38.106 --> 00:02:41.916 align:middle +código de este método está +ahora contento. De vuelta + +00:02:42.586 --> 00:02:50.326 align:middle +a nuestro controlador, para findAll(), +pasa $httpClient y $cache. Y ahora... + +00:02:50.326 --> 00:02:50.956 align:middle +¡funciona! + +00:02:52.236 --> 00:02:57.846 align:middle +Así que, a alto nivel, esta +solución tiene sentido. Sabemos + +00:02:58.376 --> 00:03:02.446 align:middle +que podemos autoconducir servicios +a nuestro controlador... y luego + +00:03:02.906 --> 00:03:06.416 align:middle +simplemente los pasamos +a MixRepository. Pero si + +00:03:07.246 --> 00:03:11.156 align:middle +piensas un poco más en profundidad, +los servicios $httpClient y $cache + +00:03:11.156 --> 00:03:16.176 align:middle +no son realmente una entrada a +la función findAll(). No tienen + +00:03:16.716 --> 00:03:19.836 align:middle +realmente sentido como argumentos. Veamos + +00:03:20.556 --> 00:03:21.786 align:middle +un ejemplo. Imagina que + +00:03:22.346 --> 00:03:28.236 align:middle +decidimos cambiar el método findAll() para que +acepte un argumento string $genre para que el + +00:03:28.676 --> 00:03:32.786 align:middle +método sólo devuelva mezclas +de ese género. Este argumento + +00:03:33.476 --> 00:03:40.576 align:middle +tiene mucho sentido: al pasar diferentes +géneros cambia lo que devuelve. El argumento + +00:03:41.176 --> 00:03:44.866 align:middle +controla el comportamiento del +método. Pero los argumentos + +00:03:44.866 --> 00:03:51.696 align:middle +$httpClient y $cache no controlan el +comportamiento de la función. En realidad, + +00:03:52.316 --> 00:03:58.626 align:middle +pasaríamos estos dos mismos valores cada +vez que llamemos al método... para que + +00:03:59.046 --> 00:04:01.276 align:middle +las cosas funcionen. En lugar de + +00:04:02.256 --> 00:04:07.626 align:middle +argumentos, son realmente dependencias +que el servicio necesita. Son + +00:04:08.286 --> 00:04:13.836 align:middle +cosas que deben estar disponibles para que findAll() +pueda hacer su trabajo Para las "dependencias" + +00:04:14.546 --> 00:04:21.826 align:middle +como ésta, ya sean objetos de servicio +o configuración estática que tu + +00:04:21.826 --> 00:04:26.046 align:middle +servicio necesita, en lugar de +pasarlas a los métodos, las pasamos + +00:04:26.336 --> 00:04:29.196 align:middle +al constructor. Elimina ese + +00:04:30.086 --> 00:04:32.166 align:middle +supuesto argumento de $genre... y añade + +00:04:32.746 --> 00:04:36.486 align:middle +un public function __construct(). Copia los + +00:04:37.476 --> 00:04:42.116 align:middle +dos argumentos, bórralos y +muévelos hasta aquí. Antes de + +00:04:43.116 --> 00:04:49.436 align:middle +terminar esto, tengo que decirte que el +autocableado funciona en dos sitios. Ya + +00:04:50.186 --> 00:04:54.746 align:middle +sabemos que podemos autoconectar argumentos +en los métodos de nuestro controlador. Pero + +00:04:55.376 --> 00:05:01.206 align:middle +también podemos autoconectar argumentos en el +método __construct() de cualquier servicio. De hecho, + +00:05:01.846 --> 00:05:06.326 align:middle +¡éste es el lugar principal en el que se +supone que funciona la autoconexión! El hecho de + +00:05:07.176 --> 00:05:11.876 align:middle +que la autoconexión también funcione +en los métodos del controlador es... + +00:05:12.106 --> 00:05:15.016 align:middle +una especie de "extra" para hacer la vida más +agradable. En cualquier caso, la autoconexión + +00:05:15.956 --> 00:05:20.476 align:middle +funciona en el método __construct() +de nuestros servicios. Así que, + +00:05:20.946 --> 00:05:28.216 align:middle +siempre que indiquemos los argumentos (y lo hemos +hecho), cuando Symfony instancie nuestro servicio, + +00:05:28.616 --> 00:05:31.656 align:middle +nos pasará estos dos servicios. ¡Sí! ¿Y + +00:05:32.146 --> 00:05:36.846 align:middle +qué hacemos con estos dos +argumentos? Los establecemos + +00:05:37.376 --> 00:05:39.386 align:middle +en propiedades. Creamos una propiedad + +00:05:40.076 --> 00:05:44.886 align:middle +private $httpClient y una propiedad +private $cache. Luego, abajo, + +00:05:45.616 --> 00:05:48.906 align:middle +en el constructor, les +asignamos: $this->httpClient = + +00:05:48.946 --> 00:05:54.556 align:middle +$httpClient, y $this->cache += $cache. Así, cuando + +00:05:55.336 --> 00:06:01.676 align:middle +Symfony instancie nuestro MixRepository, +nos pasará estos dos argumentos y + +00:06:02.176 --> 00:06:06.256 align:middle +los almacenaremos en propiedades para +poder utilizarlos más adelante. ¡Observa! + +00:06:06.976 --> 00:06:11.786 align:middle +Aquí abajo, en lugar de $cache, +utiliza $this->cache. Y entonces no + +00:06:12.656 --> 00:06:16.626 align:middle +necesitamos este use +($httpClient) de aquí... porque + +00:06:17.056 --> 00:06:20.166 align:middle +podemos decir +$this->httpClient. Este servicio + +00:06:21.006 --> 00:06:23.896 align:middle +está ahora en perfecto estado. De vuelta a + +00:06:24.746 --> 00:06:28.686 align:middle +VinylController , ¡ahora podemos +simplificar! El método findAll() + +00:06:29.316 --> 00:06:31.776 align:middle +no necesita ningún argumento +... y así ni siquiera + +00:06:32.106 --> 00:06:38.156 align:middle +necesitamos autoconducir $httpClient +o $cache en absoluto. Voy a + +00:06:38.646 --> 00:06:42.936 align:middle +celebrarlo eliminando esas declaraciones +use de la parte superior. ¡ Mira qué + +00:06:43.756 --> 00:06:45.496 align:middle +fácil es! Autocableamos + +00:06:45.916 --> 00:06:51.696 align:middle +el único servicio que necesitamos, +llamamos al método en él, y... ¡hasta + +00:06:52.556 --> 00:06:54.326 align:middle +funciona! Así es + +00:06:54.706 --> 00:06:57.116 align:middle +como escribimos servicios. Añadimos + +00:06:57.746 --> 00:07:03.656 align:middle +las dependencias al constructor, las establecemos +en las propiedades y luego las utilizamos. Por + +00:07:04.446 --> 00:07:10.786 align:middle +cierto, lo que acabamos de hacer tiene un nombre +extravagante: "inyección de dependencias". ¡ Pero no huyas + +00:07:11.246 --> 00:07:12.166 align:middle +! Puede que + +00:07:12.676 --> 00:07:14.186 align:middle +sea un término que asuste... o que al + +00:07:14.406 --> 00:07:19.646 align:middle +menos suene "aburrido", pero es un +concepto muy sencillo. Cuando estás + +00:07:20.376 --> 00:07:23.456 align:middle +dentro de un servicio como MixRepository y + +00:07:23.956 --> 00:07:31.456 align:middle +te das cuenta de que necesitas otro servicio (o tal vez alguna +configuración como una clave de API), para obtenerlo, crea un + +00:07:31.836 --> 00:07:37.286 align:middle +constructor, añade un argumento para lo que +necesitas, ponlo en una propiedad, y luego + +00:07:37.676 --> 00:07:39.916 align:middle +úsalo abajo en tu código. Sí Eso +es la inyección de dependencia. + +00:07:40.606 --> 00:07:43.216 align:middle +En pocas palabras, la inyección de + +00:07:44.046 --> 00:07:49.926 align:middle +dependencia dice: Si necesitas +algo, en lugar de cogerlo de la nada + +00:07:49.926 --> 00:07:55.226 align:middle +, obliga a Symfony a pasártelo a +través del constructor. Este es + +00:07:55.976 --> 00:07:59.416 align:middle +uno de los conceptos más +importantes de Symfony... y + +00:07:59.846 --> 00:08:02.666 align:middle +lo haremos una y otra vez. Bien, sin relación + +00:08:03.566 --> 00:08:07.756 align:middle +con la inyección de dependencia +y el autocableado, hay + +00:08:08.146 --> 00:08:11.546 align:middle +dos pequeñas mejoras que podemos +hacer a nuestro servicio. La primera + +00:08:12.046 --> 00:08:15.746 align:middle +es que podemos añadir tipos a nuestras +propiedades: HttpClientInterface y + +00:08:16.086 --> 00:08:19.156 align:middle +CacheInterface. Eso no + +00:08:20.006 --> 00:08:22.596 align:middle +cambia el funcionamiento de +nuestro código... es sólo + +00:08:22.756 --> 00:08:25.916 align:middle +una forma agradable y responsable +de hacer las cosas. ¡ Pero + +00:08:26.546 --> 00:08:28.326 align:middle +podemos ir más allá! En PHP + +00:08:28.916 --> 00:08:34.956 align:middle +8, hay una nueva sintaxis más corta para +crear una propiedad y establecerla en el + +00:08:34.956 --> 00:08:36.956 align:middle +constructor como estamos haciendo. +Tiene el siguiente aspecto. + +00:08:37.606 --> 00:08:38.606 align:middle +En primer lugar, + +00:08:39.146 --> 00:08:42.386 align:middle +moveré mis argumentos a +varias líneas... sólo para + +00:08:42.546 --> 00:08:44.326 align:middle +mantener las cosas organizadas. Ahora añade + +00:08:45.246 --> 00:08:49.116 align:middle +la palabra private delante +de cada argumento. Termina + +00:08:52.056 --> 00:08:53.716 align:middle +borrando las propiedades ... así como el + +00:08:54.306 --> 00:08:56.386 align:middle +interior del método. Esto puede + +00:08:57.246 --> 00:09:03.666 align:middle +parecer raro al principio, pero en cuanto añades +private, protected, o public delante de un argumento + +00:09:03.666 --> 00:09:08.726 align:middle +__construct() , se crea una +propiedad con este nombre y se fija + +00:09:09.146 --> 00:09:11.756 align:middle +el argumento en esa propiedad. Así que + +00:09:12.606 --> 00:09:16.786 align:middle +parece diferente, pero es exactamente +lo mismo que teníamos antes. Cuando + +00:09:17.576 --> 00:09:19.206 align:middle +lo probamos... ¡sí! + +00:09:19.206 --> 00:09:21.146 align:middle +Sigue funcionando. Siguiente: Sigo + +00:09:21.976 --> 00:09:25.816 align:middle +diciendo que el contenedor +contiene servicios. Es cierto + +00:09:26.346 --> 00:09:27.256 align:middle +Pero + +00:09:27.686 --> 00:09:33.666 align:middle +también contiene otra cosa: una simple +configuración llamada "parámetros" diff --git a/sfcasts/ep2-fundamentals/es/environment-variables.md b/sfcasts/ep2-fundamentals/es/environment-variables.md new file mode 100644 index 0000000..b95f481 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/environment-variables.md @@ -0,0 +1,85 @@ +# Variables de entorno + +Abrir `config/packages/framework.yaml`. No necesitamos estar autentificados para utilizar esta parte de la API de GitHub dedicada al contenido en bruto del usuario: + +[[[ code('d0e2649747') ]]] + +Pero si accedemos mucho a esta ruta, podríamos llegar a su límite de velocidad, que es bastante bajo para los usuarios anónimos. Así que vamos a autenticar nuestra petición. + +## Añadir una cabecera de autorización a la petición HTTP + +En primer lugar, si estás codificando conmigo, dirígete a "github.com" y crea tu propio token de acceso personal. Una vez hecho esto, abre `MixRepository` y baja hasta donde hacemos la petición HTTP. Para adjuntar el token de acceso a la petición pasa un tercer argumento, que es un array. Dentro, añade una clave `headers` fijada en otro array, con una cabecera `Authorization` asignada a la palabra `Token` y luego el token de acceso. Empieza utilizando un token falso: + +[[[ code('9cb7084ddc') ]]] + +Puedes saber que esto funciona porque, cuando volvemos a la página y la refrescamos... ¡estalla! Nuestra llamada a la API ahora falla con un 404 porque reconoce que estamos intentando autenticarnos con un token... pero el que hemos pasado es falso. + +Ahora añade tu token real. Inténtalo de nuevo y... ¡funciona! + +## Moviendo el encabezado de autorización a framework.yaml + +¡Así que esto es genial! Pero sería mejor si el servicio viniera preconfigurado para establecer automáticamente esta cabecera de autorización... especialmente si queremos utilizar este servicio de Cliente HTTP en varios sitios. ¿Podemos hacerlo? Por supuesto + +Copia la línea `Token`, entra en `framework.yaml`, y después de `base_uri`, pasa una clave `headers` con `Authorization` ajustada a nuestra cadena larga. En realidad, déjame poner un token falso ahí temporalmente: + +[[[ code('63d8ced9e1') ]]] + +En `MixRepository`, elimina ese tercer argumento: + +[[[ code('614d5f78d4') ]]] + +Y ahora, cuando probemos esto... ¡genial! Las cosas se rompen, lo que demuestra que estamos enviando esa cabecera... sólo que con el valor equivocado. Si cambiamos a nuestro token real... una vez más... ¡funciona! ¡Genial! + +## Hola Variables de Entorno + +Hasta ahora, esto es sólo una bonita característica del HttpClient. Pero esto también ayuda a poner de manifiesto un problema común. No es... genial tener nuestro sensible token de la API de GitHub codificado en este archivo. Es decir, este archivo va a ser enviado a nuestro repositorio. Quiero a mis compañeros de equipo... pero no los quiero tanto como para compartir mi token de acceso con ellos... o el token de acceso de nuestra empresa. + +Aquí es donde las variables de entorno resultan útiles. Si no estás familiarizado con las variables de entorno, son variables que puedes establecer en cualquier sistema (Windows, Linux, lo que sea)... y luego puedes leerlas desde dentro de PHP. Muchas plataformas de alojamiento hacen que sea súper fácil establecerlas. ¿Cómo nos ayuda eso? Porque, en teoría, podríamos establecer nuestro token de acceso como una variable de entorno y luego simplemente leerlo en PHP. Eso nos permitiría evitar poner ese valor sensible dentro de nuestro código. + +## Lectura de variables de entorno + +Pero, antes de hablar de establecer variables de entorno, ¿cómo leemos las variables de entorno en Symfony? Copia tu token de acceso para no perderlo, pon comillas simples alrededor de `Token`, y luego vamos a utilizar una sintaxis muy especial para leer una variable de entorno. En realidad va a parecer un parámetro. Empieza y termina con `%`, y dentro, di `env()` con el nombre de la variable de entorno. ¿Qué te parece `GITHUB_TOKEN`. Me acabo de inventar ese nombre: + +[[[ code('812eee7285') ]]] + +Si volvemos atrás y refrescamos... ahora estamos leyendo esa variable de entorno `GITHUB_TOKEN`... pero aún no la hemos configurado, por lo que obtenemos este error "Variable de entorno no encontrada". + +## Configurar las variables de entorno y el .env + +En el mundo real, configurar las variables de entorno es... en realidad algo complicado. Es diferente en Windows que en Linux. Y aunque muchas plataformas de alojamiento hacen que sea súper fácil configurar las variables de entorno, no es muy sencillo hacerlo localmente en tu ordenador. + +Por eso existe este archivo `.env`. Muy sencillo, cuando Symfony arranca, lee el archivo `.env` y convierte todo esto en variables de entorno. Esto significa que podemos decir `GITHUB_TOKEN=` y pegar nuestro token... y ahora... ¡funciona! + +[[[ code('6de74e6cd7') ]]] + +Por cierto, si hubiera una variable de entorno real `GITHUB_TOKEN` en mi sistema, esa variable de entorno real ganaría a lo que tenemos en este archivo. + +## El archivo .env.local + +Vale... esto es genial... ¡pero seguimos teniendo el mismo problema! Tenemos un valor sensible que está dentro de un archivo... que está comprometido en nuestro repositorio. + +Bien, entonces, vamos a intentar otra cosa. Copia el token de GitHub, elimina el valor de este archivo y crea un nuevo archivo llamado `.env.local`. Establece la variable de entorno aquí. + +Y ahora... ¡las cosas siguen funcionando! + +Esto es lo que pasa. Cuando Symfony arranca, primero lee el archivo `.env` y convierte todo esto en variables de entorno. Luego lee `.env.local` y convierte todo lo que hay aquí en variables de entorno... que anulan cualquier valor establecido en `.env`. + +El resultado es que tu archivo `.env` está destinado a contener valores seguros, por defecto, que están bien para ser consignados en tu repositorio. Entonces, localmente (y quizás también en producción, dependiendo de cómo se despliegue), creas un archivo `.env.local` y pones allí los valores sensibles. La clave es que `.env.local` es ignorado por Git. Puedes ver que ya está en nuestro archivo `.gitignore`. Así que, aunque este archivo contenga valores sensibles, no se confirmará en el repositorio. + +Hay algunos otros archivos `.env` que puedes crear... y puedes verlos mencionados aquí. No son tan importantes, pero si quieres leer sobre ellos, puedes consultar la documentación. + +## Visualización de las variables de entorno con debug:dotenv + +Otra cosa genial sobre las variables de entorno es que puedes visualizarlas ejecutando: + +```terminal +php bin/console debug:dotenv +``` + +¡Genial! Puedes ver el valor actual de `GITHUB_TOKEN`... y que este valor también está establecido en `.env.local`. En cambio, `APP_ENV` y `APP_SECRET` tienen `n/a`aquí, lo que significa que sus valores no están siendo anulados en `.env.local`. También nos dice qué archivos `.env` ha detectado. + +## Procesadores de variables de entorno + +Hay algunos trucos que puedes utilizar con las variables de entorno. Por ejemplo, hay algo llamado "sistema de procesadores" en el que podrías utilizar `trim` para "recortar" el espacio en blanco en `GITHUB_TOKEN`. O podrías utilizar `file` donde la variable `GITHUB_TOKEN`es en realidad una ruta a un archivo que contiene el valor verdadero. En cualquier caso, esto se llama "procesadores de variables de entorno" si quieres leer más sobre ellos. + +A continuación, vamos a hablar rápidamente sobre el despliegue... pero aún más sobre cómo podemos almacenar de forma segura estos valores sensibles cuando se despliega a producción. Una opción es la bóveda de secretos de Symfony. diff --git a/sfcasts/ep2-fundamentals/es/environment-variables.vtt b/sfcasts/ep2-fundamentals/es/environment-variables.vtt new file mode 100644 index 0000000..d2d5791 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/environment-variables.vtt @@ -0,0 +1,417 @@ +WEBVTT + +00:00:01.016 --> 00:00:03.286 align:middle +Abrir config/packages/framework.yaml. + +00:00:03.976 --> 00:00:09.906 align:middle +No necesitamos estar autentificados para utilizar esta parte de +la API de GitHub que contiene el contenido bruto del usuario: + +00:00:10.716 --> 00:00:15.356 align:middle +Pero si accedemos mucho a esta ruta , +podríamos llegar a su límite de velocidad, + +00:00:15.536 --> 00:00:18.516 align:middle +que es bastante bajo para +los usuarios anónimos. + +00:00:18.726 --> 00:00:21.786 align:middle +Así que vamos a autenticar nuestra petición. + +00:00:22.676 --> 00:00:25.886 align:middle +Primero, si estás codificando +conmigo, dirígete a "github.com" + +00:00:25.986 --> 00:00:28.776 align:middle +y crea tu propio token de acceso personal. + +00:00:29.476 --> 00:00:35.896 align:middle +Una vez hecho esto, abre MixRepository y +baja hasta donde hacemos la petición HTTP. + +00:00:36.636 --> 00:00:42.596 align:middle +Para adjuntar el token de acceso a la petición +pasa un tercer argumento, que es un array. + +00:00:43.346 --> 00:00:49.626 align:middle +Dentro, añade una clave headers fijada en otro +array, con una cabecera Authorization asign ada + +00:00:49.626 --> 00:00:52.456 align:middle +a la palabra Token y luego el token de acceso. + +00:00:52.876 --> 00:00:54.846 align:middle +Empieza utilizando un token falso: + +00:00:55.796 --> 00:01:00.716 align:middle +Puedes saber que esto funciona porque, cuando +volvemos a la página y la actualizamos... + +00:01:01.126 --> 00:01:02.486 align:middle +¡estalla! + +00:01:02.986 --> 00:01:07.406 align:middle +Nuestra llamada a la API ahora falla +con un 404 porque reconoce que estamos + +00:01:07.406 --> 00:01:10.286 align:middle +intentando autenticarnos con un to ken... + +00:01:10.536 --> 00:01:12.796 align:middle +pero el que hemos pasado es falso. + +00:01:13.546 --> 00:01:15.146 align:middle +Ahora añade tu token real. + +00:01:15.946 --> 00:01:17.356 align:middle +Inténtalo de nuevo y... + +00:01:17.616 --> 00:01:20.356 align:middle +¡funciona! ¡Así que esto es genial! + +00:01:20.906 --> 00:01:24.696 align:middle +Pero estaría mejor si el +servicio viniera preconfigurado + +00:01:24.696 --> 00:01:27.656 align:middle +para establecer automáticamente +esta cabecera de autorización... + +00:01:28.076 --> 00:01:32.906 align:middle +especialmente si queremos utilizar este +servicio de Cliente HTTP en varios sitios. + +00:01:33.546 --> 00:01:34.646 align:middle +¿Podemos hacerlo? + +00:01:35.176 --> 00:01:41.386 align:middle +Por supuesto Copia la línea Token, entra +en framework.yaml, y después de base_uri, + +00:01:41.586 --> 00:01:46.456 align:middle +pasa una clave headers con Authorization +ajustada a nuestra cadena larga. + +00:01:47.346 --> 00:01:50.556 align:middle +En realidad, déjame poner un +token falso ahí temporalmente: + +00:01:51.516 --> 00:01:55.436 align:middle +En MixRepository, elimina ese tercer argumento: + +00:01:58.336 --> 00:02:00.556 align:middle +Y ahora, cuando probemos esto... + +00:02:01.546 --> 00:02:06.416 align:middle +¡genial! Las cosas se rompen, lo que +demuestra que estamos enviando esa cabecera... + +00:02:06.576 --> 00:02:07.986 align:middle +sólo que con el valor equivocado. + +00:02:08.646 --> 00:02:10.586 align:middle +Si cambiamos a nuestro token real... + +00:02:11.106 --> 00:02:11.916 align:middle +una vez más... + +00:02:12.496 --> 00:02:14.486 align:middle +¡funciona! ¡Impresionante! + +00:02:15.336 --> 00:02:20.006 align:middle +Hasta aquí, esto es sólo una bonita +característica del HttpClient. + +00:02:20.526 --> 00:02:23.226 align:middle +Pero esto también ayuda a +resaltar un problema común. + +00:02:23.926 --> 00:02:31.386 align:middle +No es... genial tener nuestro token sensible +de la API de GitHub codificado en este archivo. + +00:02:32.066 --> 00:02:35.916 align:middle +Es decir, este archivo va a ser +enviado a nuestro repositorio. + +00:02:36.216 --> 00:02:37.766 align:middle +Quiero a mis compañeros de equipo... + +00:02:37.856 --> 00:02:43.456 align:middle +pero no los quiero tanto como para +compartir con ellos mi token de acceso... + +00:02:43.456 --> 00:02:45.556 align:middle +o el token de acceso de nuestra empresa. + +00:02:46.376 --> 00:02:49.656 align:middle +Aquí es donde las variables +de entorno resultan útiles. + +00:02:50.256 --> 00:02:54.826 align:middle +Si no estás familiarizado con las variables +de entorno, son variables que puedes establecer + +00:02:54.826 --> 00:02:57.656 align:middle +en cualquier sistema (Windows, +Linux, lo que sea)... + +00:02:58.046 --> 00:03:01.746 align:middle +y luego puedes leerlas desde dentro de PHP. + +00:03:01.746 --> 00:03:05.856 align:middle +Muchas plataformas de alojamiento hacen +que sea súper fácil establecerlas. + +00:03:06.556 --> 00:03:08.256 align:middle +¿Cómo nos ayuda eso? + +00:03:08.716 --> 00:03:12.516 align:middle +Porque, en teoría, podríamos +establecer nuestro token de acceso + +00:03:12.516 --> 00:03:18.046 align:middle +como una variable de entorno y +luego simplemente leerlo en PHP. + +00:03:18.046 --> 00:03:22.416 align:middle +Eso nos permitiría evitar poner ese +valor sensible dentro de nuestro código. + +00:03:23.246 --> 00:03:26.656 align:middle +Pero, antes de hablar de +establecer variables de entorno, + +00:03:26.926 --> 00:03:30.016 align:middle +¿cómo leemos las variables +de entorno en Symfony? + +00:03:30.816 --> 00:03:36.486 align:middle +Copia tu token de acceso para no perderlo, pon +comillas simples alrededor de Token, y luego vamos a + +00:03:36.486 --> 00:03:40.716 align:middle +utilizar una sintaxis muy especial +para leer una variable de entorno. + +00:03:41.316 --> 00:03:43.606 align:middle +En realidad va a parecer un parámetro. + +00:03:44.116 --> 00:03:51.586 align:middle +Empieza y termina con %, y dentro, di env() +con el nombre de la variable de entorno. + +00:03:51.976 --> 00:03:54.126 align:middle +¿Qué te parece GITHUB_TOKEN. + +00:03:54.646 --> 00:03:56.236 align:middle +Me acabo de inventar ese nombre: + +00:03:56.976 --> 00:03:58.506 align:middle +Si volvemos atrás y refrescamos... + +00:03:59.146 --> 00:04:02.456 align:middle +ahora estamos leyendo esa variable +de entorno GITHUB_TOKEN... + +00:04:02.886 --> 00:04:08.066 align:middle +pero aún no la hemos configurado, por lo que obtenemos +este error "Variable de entorno no encontrada". + +00:04:08.856 --> 00:04:12.466 align:middle +En el mundo real, establecer +variables de entorno es... + +00:04:12.966 --> 00:04:14.646 align:middle +un poco complicado. + +00:04:15.256 --> 00:04:18.046 align:middle +Es diferente en Windows que en Linux. + +00:04:18.416 --> 00:04:24.466 align:middle +Y aunque muchas plataformas de alojamiento hacen que +sea súper fácil establecer variables de entorno, + +00:04:24.996 --> 00:04:28.456 align:middle +no es muy sencillo hacerlo +localmente en tu ordenador. + +00:04:29.036 --> 00:04:31.856 align:middle +Por eso existe este archivo .env. + +00:04:32.616 --> 00:04:38.496 align:middle +Muy sencillo, cuando Symfony arranca, +lee el archivo .env y convierte todo + +00:04:38.496 --> 00:04:40.946 align:middle +esto en variables de entorno. + +00:04:41.676 --> 00:04:45.556 align:middle +Esto significa que podemos decir +GITHUB_TOKEN= y pegar nuestro token... + +00:04:46.196 --> 00:04:47.036 align:middle +y ahora... + +00:04:47.766 --> 00:04:54.896 align:middle +¡funciona! Por cierto, si hubiera una +variable de entorno real GITHUB_TOKEN + +00:04:54.896 --> 00:05:01.126 align:middle +en mi sistema, esa variable de entorno real +ganaría a lo que tenemos en este archivo. + +00:05:02.046 --> 00:05:03.716 align:middle +Vale... esto es genial... + +00:05:04.046 --> 00:05:06.386 align:middle +¡pero seguimos teniendo el mismo problema! + +00:05:06.646 --> 00:05:09.716 align:middle +Tenemos un valor sensible que +está dentro de un archivo... + +00:05:10.206 --> 00:05:12.726 align:middle +que está comprometido en nuestro repositorio. + +00:05:13.496 --> 00:05:15.956 align:middle +Bien, entonces, vamos a intentar otra cosa. + +00:05:16.446 --> 00:05:19.326 align:middle +Copia el token de GitHub, +elimina el valor de este archivo + +00:05:19.626 --> 00:05:23.956 align:middle +y crea un nuevo archivo llamado .env.local. + +00:05:24.576 --> 00:05:27.156 align:middle +Establece la variable de entorno aquí. + +00:05:27.876 --> 00:05:28.736 align:middle +Y ahora... + +00:05:29.396 --> 00:05:31.156 align:middle +¡las cosas siguen funcionando! + +00:05:31.916 --> 00:05:32.806 align:middle +Esto es lo que pasa. + +00:05:33.326 --> 00:05:38.276 align:middle +Cuando Symfony arranca, primero lee +el archivo .env y convierte todo + +00:05:38.276 --> 00:05:40.256 align:middle +esto en variables de entorno. + +00:05:40.706 --> 00:05:47.456 align:middle +Luego lee .env.local y convierte todo lo +que hay aquí en variables de entorno... + +00:05:47.676 --> 00:05:51.416 align:middle +que anulan cualquier valor +establecido en .env. El + +00:05:52.076 --> 00:05:58.856 align:middle +resultado es que tu archivo .env está destinado a +contener valores seguros, por defecto, que están bien + +00:05:58.856 --> 00:06:00.786 align:middle +para ser consignados en tu repositorio. + +00:06:01.446 --> 00:06:06.386 align:middle +Entonces, localmente (y quizás también en +producción, dependiendo de cómo se despliegue), + +00:06:06.916 --> 00:06:12.886 align:middle +creas un archivo .env.local y +pones allí los valores sensibles. + +00:06:13.576 --> 00:06:19.416 align:middle +La clave es que .env.local es ignorado por Git. + +00:06:20.136 --> 00:06:23.426 align:middle +Puedes ver que ya está en +nuestro archivo .gitignore. + +00:06:23.886 --> 00:06:26.876 align:middle +Así que, aunque este archivo +contenga valores sensibles, + +00:06:27.166 --> 00:06:29.556 align:middle +no se confirmará en el repositorio. + +00:06:30.216 --> 00:06:33.506 align:middle +Hay otros archivos .env que puedes crear... + +00:06:33.746 --> 00:06:35.686 align:middle +y puedes verlos mencionados aquí. + +00:06:36.306 --> 00:06:39.416 align:middle +No son tan importantes, pero +si quieres leer sobre ellos, + +00:06:39.446 --> 00:06:41.356 align:middle +puedes consultar la documentación. + +00:06:42.236 --> 00:06:46.976 align:middle +Otra cosa genial de las variables de entorno +es que puedes visualizarlas ejecutando: + +00:06:46.976 --> 00:06:50.986 align:middle +php bin/console debug:dotenv ¡Genial! + +00:06:51.376 --> 00:06:53.856 align:middle +Puedes ver el valor actual de GITHUB_TOKEN... + +00:06:54.226 --> 00:06:58.086 align:middle +y que este valor también +está fijado en .env.local . En + +00:06:58.926 --> 00:07:03.756 align:middle +cambio, APP_ENV y APP_SECRET tienen n/a aquí, + +00:07:04.086 --> 00:07:08.156 align:middle +lo que significa que sus valores no +se están anulando en .env.local. + +00:07:08.906 --> 00:07:12.026 align:middle +También nos dice qué +archivos de .env ha detectado. + +00:07:12.916 --> 00:07:16.406 align:middle +Hay algunos trucos que puedes utilizar +con las variables de entorno. Por + +00:07:16.946 --> 00:07:22.276 align:middle +ejemplo, hay algo que se llama "sistema de +procesador" en el que podrías utilizar trim + +00:07:22.476 --> 00:07:24.856 align:middle +para "recortar" el espacio +en blanco en GITHUB_TOKEN. + +00:07:25.106 --> 00:07:30.536 align:middle +O podrías utilizar file donde la variable +GITHUB_TOKEN es en realidad una ruta + +00:07:30.776 --> 00:07:33.546 align:middle +a un archivo que contiene el valor verdadero. + +00:07:34.146 --> 00:07:38.656 align:middle +En cualquier caso, esto se llama "procesadores de +variables de entorno" si quieres leer más sobre ellos. + +00:07:39.546 --> 00:07:42.676 align:middle +A continuación, vamos a hablar +rápidamente sobre el despliegue... + +00:07:43.146 --> 00:07:47.456 align:middle +pero aún más sobre cómo podemos almacenar +de forma segura estos valores sensibles + +00:07:47.606 --> 00:07:49.426 align:middle +cuando se despliega en producción. + +00:07:50.016 --> 00:07:52.776 align:middle +Una opción es la bóveda +de secretos de Symfony diff --git a/sfcasts/ep2-fundamentals/es/environments.md b/sfcasts/ep2-fundamentals/es/environments.md new file mode 100644 index 0000000..7ef5774 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/environments.md @@ -0,0 +1,83 @@ +# Entornos + +Nuestra aplicación es como una máquina: es un conjunto de servicios y clases de PHP que hacen su trabajo... y que, en última instancia, renderizan algunas páginas. Pero podemos hacer que nuestra máquina funcione de forma diferente alimentándola con una configuración distinta. + +Por ejemplo, en `SongController`, estamos utilizando el servicio `$logger` para registrar cierta información: + +[[[ code('ec32177521') ]]] + +Si alimentamos al registrador con una configuración que diga "registrar todo", lo registrará todo, incluidos los mensajes de depuración de bajo nivel. Pero si cambiamos la configuración para que diga "sólo registrar errores", entonces sólo registrará los errores. En otras palabras, la misma máquina puede comportarse de forma diferente en función de nuestra configuración. Y a veces, como en el caso del registro, podemos necesitar que esa configuración sea diferente mientras estamos desarrollando localmente y en producción. + +Para manejar esto, Symfony tiene un concepto importante llamado "entornos". No me refiero a entornos como local vs staging vs beta vs producción. Un entorno Symfony es un conjunto de configuraciones. + +Por ejemplo, puedes ejecutar tu código en el entorno `dev` con un conjunto de configuración diseñado para el desarrollo. O puedes ejecutar tu aplicación en el entorno `prod`con un conjunto de configuraciones optimizadas para producción. ¡Deja que te lo enseñe! + +## La variable APP_ENV + +En la raíz de nuestro proyecto, tenemos un archivo `.env`: + +[[[ code('40c34e875b') ]]] + +Más adelante hablaremos de este archivo. Pero ¿ves este `APP_ENV=dev`? +Esto le dice a Symfony que el entorno actual es `dev`, que es perfecto para el desarrollo local. Cuando despleguemos a producción, cambiaremos esto a `prod`. +Más sobre esto en unos minutos. + +Pero... ¿qué diferencia hay? ¿Qué ocurre en nuestra aplicación cuando cambiamos esto de `dev` a `prod`? Para responder, déjame cerrar algunas carpetas... y abrir`public/index.php`: + +[[[ code('7e152d62fa') ]]] + +Recuerda: este es nuestro controlador frontal. Es el primer archivo que se ejecuta en cada petición. Realmente no nos importa mucho este archivo, pero su función es importante: arranca Symfony. + +Lo interesante es que lee el valor de `APP_ENV` y lo pasa como primer argumento a esta clase `Kernel`. Y... ¡esta clase `Kernel` está realmente en nuestro código! Vive en `src/Kernel.php`. + +Genial. Así que lo que quiero saber ahora es ¿Qué controla el primer argumento de `Kernel`? + +Si abrimos la clase, no encontramos... absolutamente nada. Está vacía. Eso es porque la mayor parte de la lógica vive en este rasgo. Mantén pulsado "cmd" o "control" y haz clic en `MicroKernelTrait` para abrirlo. + +## El directorio config/packages/{ENV} Directorio + +El trabajo de `Kernel` es cargar todos los servicios y rutas de nuestra aplicación. Si te desplazas hacia abajo, tiene un método llamado `configureContainer()`. ¡Ooh! ¡Ahora sabemos qué es el contenedor! ¡Y mira lo que hace! Toma este objeto `$container`e importa `$configDir.'/{packages}/*.{php,yaml}'`. Esta línea dice + +> ¡Oye, contenedor! Quiero cargar todos los archivos del directorio `config/packages/`. + +Carga todos esos archivos, y luego pasa la configuración de cada uno a cualquier bundle que esté definido como clave raíz. Pero lo realmente interesante para los entornos es la siguiente línea `import` +`$configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'`. ¡Si escarbas un poco, aprenderás que `$this->environment` es igual al primer argumento que se pasa a `Kernel`! + +En otras palabras, en el entorno `dev`, éste será `dev`. Así que, además de los archivos de configuración principales, esto también cargará todo lo que haya en el directorio`config/packages/dev/`. Sí, podemos añadir allí una configuración extra que anule la configuración principal en el entorno `dev`. Por ejemplo, podemos añadir una configuración de registro que le diga al registrador que lo registre todo + +Debajo de esto, también cargamos un archivo llamado `services.yaml` y, si lo tenemos,`services_dev.yaml`. Pronto hablaremos más sobre `services.yaml`. + +## El when@{ENV} Config + +Así que, si quieres añadir una configuración específica del entorno, puedes ponerla en el directorio del entorno correcto. Pero hay otra forma. Es una característica bastante nueva y la vimos en la parte inferior de `twig.yaml`. Es la sintaxis `when@`: + +[[[ code('c2bc8660e5') ]]] + +En Symfony, por defecto, hay tres entornos: `dev` `prod` , y luego, si ejecutas pruebas automatizadas, hay un entorno llamado `test`. Dentro de `twig.yaml`, al decir, `when@test`, significa que esta configuración sólo se cargará si el entorno es igual a `test`. + +El mejor ejemplo de esto podría estar en `monolog.yaml`. `monolog` es el bundle que controla el servicio de registro. Tiene una configuración que se utiliza en todos los entornos. Pero, por debajo de éste, tiene `when@dev`. No hablaremos demasiado de la configuración específica de `monolog`, pero esto controla cómo se manejan los mensajes de registro. En el entorno `dev`, esto dice que se debe registrar todo y que se debe registrar en un archivo, utilizando esta sintaxis extravagante `%kernel.logs_dir%` de la que aprenderemos pronto. + +En cualquier caso, esto apunta a un archivo `var/logs/dev.log` y la parte `level: debug` significa que registrará todos los mensajes en `dev.log`... independientemente de lo importante o no que sea ese mensaje. + +Por debajo de esto, para el entorno `prod`, es bastante diferente. La línea más importante es `action_level: error`. Que dice: + +> ¡Hola Sra. Logger! Esta aplicación probablemente registra una tonelada de mensajes, pero sólo quiero que +> guarde realmente los mensajes que tengan un nivel de importancia `error` o superior. + +¡Eso tiene sentido! En producción, no queremos que nuestros archivos de registro se llenen de toneladas y toneladas de mensajes de depuración. Con esto, sólo registramos los mensajes de error. + +El gran punto es éste: utilizando estos trucos, podemos configurar nuestros servicios de forma diferente en función del entorno. + +## Enrutamiento específico del entorno + +Incluso podemos hacer lo mismo con las rutas A veces tienes rutas enteras que sólo quieres cargar en un entorno determinado. Volviendo a `MicroKernelTrait`, si bajas, hay un método llamado `configureRoutes()`. Este es el responsable de cargar todas nuestras rutas... y es muy similar al otro código. Carga `$configDir.'/{routes}/*.{php,yaml}'` así como este directorio de entorno `dev`, si tienes uno. Nosotros no lo tenemos. + +También puedes utilizar el truco de `when@dev`. Este archivo se encarga de registrar las rutas que utiliza la barra de herramientas de depuración web. No queremos que la barra de herramientas de depuración web esté en producción... así que estas rutas sólo se importan en el entorno `dev`. + +[[[ code('c32be9baf6') ]]] + +Diablos, ¡algunos bundles sólo están habilitados en algunos entornos! Si abres`config/bundles.php`, tenemos el nombre del bundle... y luego, a la derecha, los entornos en los que ese bundle debe estar habilitado. Este `all` significa todos los entornos.... y la mayoría están habilitados en todos los entornos. + +Sin embargo, el `WebProfilerBundle` -el bundle que nos proporciona la barra de herramientas de depuración web y el perfilador- sólo se carga en los entornos `dev` y `test`. Sí, todo el bundle -y los servicios que proporciona- nunca se cargan en el entorno `prod`. + +Así que, ahora que entendemos los fundamentos de los entornos, vamos a ver si podemos cambiar nuestra aplicación al entorno `prod`. Y luego, como reto, configuraremos nuestro servicio de caché de forma diferente en `dev`. Eso a continuación. diff --git a/sfcasts/ep2-fundamentals/es/environments.vtt b/sfcasts/ep2-fundamentals/es/environments.vtt new file mode 100644 index 0000000..31a9738 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/environments.vtt @@ -0,0 +1,439 @@ +WEBVTT + +00:00:01.036 --> 00:00:07.686 align:middle +Nuestra aplicación es como una máquina: es un conjunto +de servicios y clases PHP que hacen el trabajo... + +00:00:08.076 --> 00:00:10.116 align:middle +y, en última instancia, +renderizan algunas páginas. + +00:00:10.576 --> 00:00:15.886 align:middle +Pero podemos hacer que nuestra máquina funcione de forma +diferente alimentándola con una configuración distinta. + +00:00:16.646 --> 00:00:22.116 align:middle +Por ejemplo, en SongController, estamos utilizando el +servicio $logger para registrar cierta información: + +00:00:22.896 --> 00:00:29.786 align:middle +Si le damos al registrador una configuración +que diga "registrar todo", lo registrará todo, + +00:00:30.016 --> 00:00:32.906 align:middle +incluidos los mensajes de +depuración de bajo nivel. + +00:00:33.476 --> 00:00:39.936 align:middle +Pero si cambiamos la configuración para que diga "sólo +registrar errores", sólo registrará los errores. + +00:00:40.546 --> 00:00:46.486 align:middle +En otras palabras, la misma máquina puede comportarse +de forma diferente en función de nuestra configuración. + +00:00:47.116 --> 00:00:51.166 align:middle +Y a veces, como con el registro, +podemos necesitar que esa configuración + +00:00:51.166 --> 00:00:55.026 align:middle +sea diferente mientras estamos +desarrollando localmente o en producción. + +00:00:55.776 --> 00:01:00.656 align:middle +Para manejar esto, Symfony tiene un +concepto importante llamado "entornos". + +00:01:01.096 --> 00:01:06.116 align:middle +No me refiero a entornos como local +vs staging vs beta vs producción. + +00:01:06.116 --> 00:01:10.386 align:middle +Un entorno Symfony es un +conjunto de configuraciones. Por + +00:01:11.046 --> 00:01:15.716 align:middle +ejemplo, puedes ejecutar tu código en el +entorno dev con un conjunto de configuraciones + +00:01:15.716 --> 00:01:18.306 align:middle +diseñadas para el desarrollo. + +00:01:18.936 --> 00:01:23.206 align:middle +O puedes ejecutar tu aplicación en el entorno +prod con un conjunto de configuraciones + +00:01:23.206 --> 00:01:25.756 align:middle +optimizadas para producción. + +00:01:26.446 --> 00:01:26.946 align:middle +Deja que te lo enseñe + +00:01:26.946 --> 00:01:31.056 align:middle +En la raíz de nuestro proyecto, +tenemos un archivo .env: + +00:01:31.686 --> 00:01:33.726 align:middle +Hablaremos más sobre este +archivo más adelante. + +00:01:34.346 --> 00:01:36.546 align:middle +Pero, ¿ves este APP_ENV=dev? + +00:01:37.206 --> 00:01:43.556 align:middle +Esto le dice a Symfony que el entorno actual es +dev, que es perfecto para el desarrollo local. + +00:01:44.276 --> 00:01:48.166 align:middle +Cuando despleguemos a producción, +cambiaremos esto a prod. + +00:01:48.646 --> 00:01:50.366 align:middle +Más sobre esto en unos minutos. + +00:01:51.116 --> 00:01:53.626 align:middle +Pero... ¿qué diferencia hay? + +00:01:54.076 --> 00:01:58.256 align:middle +¿Qué ocurre en nuestra aplicación +cuando cambiamos esto de dev a prod? + +00:01:59.036 --> 00:02:01.936 align:middle +Para responder, déjame +cerrar algunas carpetas... + +00:02:03.806 --> 00:02:07.176 align:middle +y abrir public/index.php: + +00:02:07.176 --> 00:02:09.986 align:middle +Recuerda: éste es nuestro controlador frontal. + +00:02:10.556 --> 00:02:14.386 align:middle +Es el primer archivo que se +ejecuta en cada petición. + +00:02:14.946 --> 00:02:21.216 align:middle +En realidad no nos importa mucho este archivo, +pero su función es importante: arranca Symfony. + +00:02:21.976 --> 00:02:28.376 align:middle +Lo interesante es que lee +el valor APP_ENV y lo pasa + +00:02:28.376 --> 00:02:31.446 align:middle +como primer argumento a esta clase Kernel. + +00:02:32.076 --> 00:02:36.056 align:middle +Y... ¡esta clase Kernel está +realmente en nuestro código! + +00:02:36.566 --> 00:02:38.916 align:middle +Vive en src/Kernel.php. + +00:02:39.906 --> 00:02:46.376 align:middle +Genial. Así que lo que quiero saber ahora es +¿Qué controla el primer argumento de Kernel? + +00:02:47.236 --> 00:02:49.616 align:middle +Si abrimos la clase encontramos... + +00:02:50.256 --> 00:02:52.146 align:middle +absolutamente nada. + +00:02:52.386 --> 00:02:52.986 align:middle +Está vacía. + +00:02:53.516 --> 00:02:57.296 align:middle +Eso es porque la mayor parte de +la lógica vive en este rasgo. + +00:02:57.976 --> 00:03:02.096 align:middle +Mantén pulsada la tecla "cmd" o "control" +y haz clic en MicroKernelTrait para abrirlo. + +00:03:02.976 --> 00:03:07.386 align:middle +La función de Kernel es cargar todos los +servicios y rutas de nuestra aplicación. + +00:03:08.226 --> 00:03:13.516 align:middle +Si te desplazas hacia abajo, tiene un +método llamado configureContainer(). + +00:03:13.516 --> 00:03:17.186 align:middle +¡Ya sabemos qué es el contenedor! + +00:03:17.846 --> 00:03:18.966 align:middle +¡Y mira lo que hace! + +00:03:18.966 --> 00:03:25.206 align:middle +Toma este objeto $container e importa +$configDir.'/{packages}/*.{php,yaml}'. + +00:03:25.206 --> 00:03:26.256 align:middle +Esta + +00:03:27.116 --> 00:03:29.196 align:middle +línea dice ¡Eh, contenedor! + +00:03:29.196 --> 00:03:33.756 align:middle +Quiero cargar todos los archivos +del directorio config/packages/. + +00:03:34.486 --> 00:03:39.656 align:middle +Carga todos esos archivos y luego pasa +la configuración de cada uno de ellos al + +00:03:39.996 --> 00:03:42.986 align:middle +bundle definido como clave raíz. Pero + +00:03:43.686 --> 00:03:48.586 align:middle +lo realmente interesante para los +entornos es esta línea siguiente import + +00:03:49.106 --> 00:03:51.346 align:middle +$configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'. +¡ Si + +00:03:51.346 --> 00:03:54.466 align:middle +escarbaras + +00:03:54.466 --> 00:03:55.316 align:middle +un poco, + +00:03:55.996 --> 00:04:00.976 align:middle +aprenderías que $this->environment +es igual al primer argumento que se + +00:04:00.976 --> 00:04:03.946 align:middle +pasa a Kernel! En otras palabras, en el entorno + +00:04:04.606 --> 00:04:07.806 align:middle +dev, será dev. Así, además de los + +00:04:08.486 --> 00:04:13.776 align:middle +archivos de configuración principales, también cargará +todo lo que haya en el directorio config/packages/dev/. + +00:04:13.776 --> 00:04:17.506 align:middle +Sí, podemos añadir + +00:04:18.346 --> 00:04:20.586 align:middle +allí una configuración extra que anule la + +00:04:21.046 --> 00:04:25.286 align:middle +configuración principal del +entorno dev. Por ejemplo, podemos + +00:04:26.046 --> 00:04:31.646 align:middle +añadir una configuración de registro que le diga al +registrador que lo registre todo Debajo de esto, también + +00:04:32.546 --> 00:04:36.766 align:middle +cargamos un archivo llamado +services.yaml y, si lo tenemos, + +00:04:37.056 --> 00:04:40.426 align:middle +services_dev.yaml. Hablaremos más sobre + +00:04:40.946 --> 00:04:43.556 align:middle +services.yaml muy pronto. Así que, si quieres + +00:04:44.446 --> 00:04:48.106 align:middle +añadir una configuración específica +del entorno, puedes poner la + +00:04:48.356 --> 00:04:51.416 align:middle +en el directorio de entorno +correcto. Pero hay otra + +00:04:52.316 --> 00:04:53.626 align:middle +forma. Es una característica bastante nueva + +00:04:54.076 --> 00:04:58.316 align:middle +y la vimos al final de +twig.yaml. Es la sintaxis when@: + +00:04:58.866 --> 00:05:01.506 align:middle +En Symfony, por defecto, + +00:05:02.316 --> 00:05:07.616 align:middle +hay tres entornos: dev prod , y luego, si + +00:05:07.616 --> 00:05:12.996 align:middle +ejecutas pruebas automatizadas, hay un +entorno llamado test. Dentro de twig.yaml, al + +00:05:13.846 --> 00:05:17.156 align:middle +decir, when@test, significa que esta + +00:05:17.656 --> 00:05:23.826 align:middle +configuración sólo se cargará si el +entorno es igual a test. El mejor ejemplo de + +00:05:24.546 --> 00:05:28.026 align:middle +esto podría estar en +monolog.yaml. monolog es el bundle + +00:05:28.616 --> 00:05:31.656 align:middle +que controla el servicio de logger. +Tiene una configuración que se + +00:05:32.186 --> 00:05:36.696 align:middle +utiliza en todos los entornos. +Pero, por debajo de éste, tiene + +00:05:37.156 --> 00:05:40.406 align:middle +when@dev. No hablaremos + +00:05:41.386 --> 00:05:45.276 align:middle +demasiado de la configuración específica +de monolog, pero esto controla cómo + +00:05:45.686 --> 00:05:49.256 align:middle +se gestionan los mensajes de +registro. En el entorno dev, + +00:05:50.096 --> 00:05:56.426 align:middle +esto dice que se debe registrar todo y que se debe registrar en +un archivo, utilizando esta elegante sintaxis %kernel.logs_dir% + +00:05:56.846 --> 00:06:02.756 align:middle +de la que aprenderemos pronto. +En cualquier caso, esto apunta a + +00:06:03.616 --> 00:06:10.426 align:middle +un archivo var/logs/dev.log y la parte +level: debug significa que registrará + +00:06:10.426 --> 00:06:16.486 align:middle +cada mensaje en dev.log... +independientemente de lo importante + +00:06:16.946 --> 00:06:22.056 align:middle +o no que sea ese mensaje. +Debajo de esto, para el + +00:06:22.976 --> 00:06:27.166 align:middle +entornoprod, es bastante diferente. +La línea más importante + +00:06:27.706 --> 00:06:31.156 align:middle +es action_level: error. Que dice: ¡Hola Sra. + +00:06:31.746 --> 00:06:33.686 align:middle +Logger! Esta aplicación probablemente registra + +00:06:34.016 --> 00:06:41.486 align:middle +una tonelada de mensajes, pero sólo quiero que guardes realmente +los mensajes que tengan un nivel de importancia de error + +00:06:41.746 --> 00:06:45.126 align:middle +o superior. ¡Eso tiene sentido! En + +00:06:45.876 --> 00:06:46.706 align:middle +producción, no + +00:06:47.046 --> 00:06:53.416 align:middle +queremos que nuestros archivos de registro se llenen de +toneladas y toneladas de mensajes de depuración. Con esto, sólo + +00:06:53.886 --> 00:06:56.646 align:middle +registramos los mensajes +de error. Lo importante es + +00:06:57.276 --> 00:07:00.636 align:middle +lo siguiente: utilizando estos +trucos, podemos configurar nuestros + +00:07:00.746 --> 00:07:05.486 align:middle +servicios de forma diferente en función +del entorno. ¡ E incluso podemos hacer + +00:07:06.146 --> 00:07:09.056 align:middle +lo mismo con las rutas! A +veces tienes rutas enteras + +00:07:09.606 --> 00:07:14.936 align:middle +que sólo quieres cargar en un entorno +determinado. Volviendo a MicroKernelTrait, si + +00:07:15.736 --> 00:07:21.516 align:middle +bajas, hay un método llamado +configureRoutes(). Éste es el responsable + +00:07:22.106 --> 00:07:26.106 align:middle +de cargar todas nuestras +rutas... y es muy similar + +00:07:26.546 --> 00:07:28.996 align:middle +al otro código. Carga +$configDir.'/{routes}/*.{php,yaml}' + +00:07:29.546 --> 00:07:32.406 align:middle +así como este + +00:07:32.406 --> 00:07:37.226 align:middle +directorio de entornodev, si tienes uno. +Nosotros no lo tenemos. También puedes + +00:07:37.606 --> 00:07:42.056 align:middle +utilizar el truco when@dev. +Este archivo se encarga de + +00:07:42.676 --> 00:07:47.716 align:middle +registrar las rutas que utiliza la barra de herramientas de depuración +web. No queremos la barra de herramientas de depuración web + +00:07:48.406 --> 00:07:51.956 align:middle +en producción... así que estas rutas sólo se + +00:07:52.316 --> 00:07:56.586 align:middle +importan en el entorno dev. Diablos, +¡ciertos bundles sólo están + +00:07:57.176 --> 00:08:00.876 align:middle +habilitados en algunos entornos! +Si abres config/bundles.php, + +00:08:01.646 --> 00:08:05.956 align:middle +tenemos el nombre del bundle... +y luego, a la derecha, + +00:08:06.406 --> 00:08:11.836 align:middle +los entornos en los que ese bundle debe estar +habilitado. Este all significa todos los entornos.... + +00:08:12.716 --> 00:08:15.126 align:middle +y la mayoría están habilitados en + +00:08:15.446 --> 00:08:18.326 align:middle +todos los entornos. Sin embargo, +el WebProfilerBundle -el + +00:08:19.076 --> 00:08:25.356 align:middle +bundle que nos proporciona la barra de herramientas de +depuración web y el perfilador- sólo se carga en los entornos + +00:08:25.936 --> 00:08:29.476 align:middle +dev y test. Sí, todo el bundle + +00:08:30.246 --> 00:08:34.556 align:middle +-y los servicios que proporciona- +nunca se cargan en el entorno + +00:08:34.866 --> 00:08:37.666 align:middle +prod. Así que, ahora que entendemos + +00:08:38.426 --> 00:08:41.726 align:middle +lo básico de los entornos, veamos si podemos + +00:08:42.076 --> 00:08:46.156 align:middle +cambiar nuestra aplicación al +entorno prod. Y luego, como reto, + +00:08:46.806 --> 00:08:52.836 align:middle +configuraremos nuestro servicio de caché de +forma diferente en dev. + +00:08:53.336 --> 00:08:54.276 align:middle +Eso a continuación. \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/global-bind.md b/sfcasts/ep2-fundamentals/es/global-bind.md new file mode 100644 index 0000000..265ce2e --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/global-bind.md @@ -0,0 +1,36 @@ +# Vincular argumentos globalmente + +En la práctica, rara vez tienes que hacer algo dentro de `services.yaml`. La mayoría de las veces, cuando añades un argumento al constructor de un servicio, es autoconductible. Así que añades el argumento, le das una pista de tipo... ¡y sigues codificando! + +Pero el argumento de `$isDebug` no es autoconductible... ya que no es un servicio. Y eso nos obligó a anular por completo el servicio para poder especificar ese único argumento con `bind`. Funciona, pero... ¡eso era... mucho teclear para hacer una cosa tan pequeña! +# Mover el bind a `_defaults` + +Así que aquí tienes una solución diferente. Copia esa tecla `bind`, borra el servicio por completo, y arriba, bajo `_defaults`, pega. + +Cuando nos movemos y probamos esto... ¡la página sigue funcionando! ¿No es genial? Y tiene sentido. Esta sección registrará automáticamente `MixRepository` como servicio... y entonces todo lo que esté bajo `_defaults` se aplicará a ese servicio. Así que el resultado final es exactamente el que teníamos antes. + +¡Me encanta hacer esto! Me permite establecer convenciones para todo el proyecto. Ahora que tenemos esto, podemos añadir un argumento `$isDebug` al constructor de cualquier servicio y funcionará al instante. + +## Vinculación con Type_hints + +Por cierto, si quieres, también puedes incluir el tipo con el bind. + +Así, ahora sólo funcionaría si utilizamos la sugerencia de tipo `bool` con el argumento. Si utilizáramos `string`, por ejemplo, Symfony no intentaría pasar ese valor. + +## El atributo Autowire + +Así que el bind global es genial. Pero a partir de Symfony 6.1, hay otra forma de especificar un argumento no autoconductible. Comenta el global `bind`. Todavía me gusta hacer esto... pero probemos la nueva forma. + +Si actualizamos ahora, obtendremos un error porque Symfony no sabe qué pasar al argumento `$isDebug`. Para solucionarlo, entra en `MixRepository` y, por encima del argumento (o antes del argumento si no estás usando varias líneas), añade un atributo de PHP 8 llamado `Autowire`. Normalmente, los atributos de PHP 8 se autocompletan, pero este no se autocompleta para mí. Esto se debe a un error en PhpStorm. Para evitarlo, voy a escribir `Autowire`... luego iré a la parte superior y empezaré a añadir la declaración `use`para esto manualmente, lo que sí nos da una opción de autocompletar. Pulsa "tab" y... ¡tah dah! Si quieres hacerlos por orden alfabético, puedes moverlo. + +También puedes observar que está subrayado con un mensaje + +> No se puede aplicar el atributo a una propiedad [...] + +De nuevo, PhpStorm está un poco confundido porque esto es tanto una propiedad como un argumento. + +De todos modos, sigue adelante y pasa esto como un argumento `%kernel.debug%`. + +Actualiza ahora y... ¡lo tienes! Bastante bien, ¿verdad? + +Siguiente: la mayoría de las veces, cuando se autoconduce un argumento como `HttpClientInterface`, sólo hay un servicio en el contenedor que implementa esa interfaz. Pero, ¿y si hubiera varios clientes HTTP en nuestro contenedor? ¿Cómo podríamos elegir el que queremos? Es hora de hablar del autocableado con nombre. diff --git a/sfcasts/ep2-fundamentals/es/global-bind.vtt b/sfcasts/ep2-fundamentals/es/global-bind.vtt new file mode 100644 index 0000000..042d3f1 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/global-bind.vtt @@ -0,0 +1,195 @@ +WEBVTT + +00:00:01.036 --> 00:00:06.086 align:middle +En la práctica, rara vez necesitas +hacer algo dentro de services.yaml. + +00:00:06.506 --> 00:00:12.196 align:middle +La mayoría de las veces, cuando añades un argumento +al constructor de un servicio, éste es autoinstalable. + +00:00:12.676 --> 00:00:15.396 align:middle +Así que añades el argumento, +le das una pista de tipo... + +00:00:15.726 --> 00:00:17.516 align:middle +¡y sigues codificando! + +00:00:18.306 --> 00:00:21.506 align:middle +Pero el argumento $isDebug +no es autoconductible... + +00:00:21.646 --> 00:00:23.236 align:middle +ya que no es un servicio. + +00:00:23.816 --> 00:00:28.146 align:middle +Y eso nos obligó a anular +completamente el servicio + +00:00:28.416 --> 00:00:31.796 align:middle +para poder especificar ese +único argumento con bind. + +00:00:32.516 --> 00:00:34.006 align:middle +Funciona pero... + +00:00:34.416 --> 00:00:34.886 align:middle +eso fue... + +00:00:35.056 --> 00:00:38.786 align:middle +un montón de texto para +hacer una cosa tan pequeña + +00:00:39.586 --> 00:00:41.486 align:middle +Así que he aquí una solución diferente. + +00:00:41.916 --> 00:00:49.076 align:middle +Copia esa tecla bind, borra el servicio por +completo, y arriba, bajo _defaults, pega. + +00:00:49.946 --> 00:00:51.886 align:middle +Cuando nos movemos y probamos esto... + +00:00:52.616 --> 00:00:54.486 align:middle +¡la página sigue funcionando! + +00:00:54.576 --> 00:00:55.766 align:middle +¿No es genial? + +00:00:56.426 --> 00:00:57.646 align:middle +Y tiene sentido. + +00:00:57.946 --> 00:01:02.706 align:middle +Esta sección registrará automáticamente +MixRepository como servicio... + +00:01:03.246 --> 00:01:08.826 align:middle +y entonces todo lo que esté bajo +_defaults se aplicará a ese servicio. + +00:01:09.186 --> 00:01:12.046 align:middle +Así que el resultado final es +exactamente el que teníamos antes. + +00:01:12.746 --> 00:01:14.406 align:middle +¡Me encanta hacer esto! + +00:01:15.006 --> 00:01:18.256 align:middle +Me permite establecer convenciones +para todo el proyecto. + +00:01:19.046 --> 00:01:23.546 align:middle +Ahora que tenemos esto, podemos añadir +un argumento $isDebug al constructor + +00:01:23.546 --> 00:01:26.526 align:middle +de cualquier servicio y +funcionará al instante. + +00:01:27.316 --> 00:01:31.896 align:middle +Por cierto, si quieres, también +puedes incluir el tipo con el bind. + +00:01:32.486 --> 00:01:37.736 align:middle +Así que ahora sólo funcionaría si +utilizamos el tipo bool con el argumento. + +00:01:38.446 --> 00:01:43.716 align:middle +Si usáramos string, por ejemplo, +Symfony no intentaría pasar ese valor. + +00:01:44.476 --> 00:01:47.286 align:middle +Así que el bind global es genial. + +00:01:47.736 --> 00:01:55.406 align:middle +Pero a partir de Symfony 6.1, hay otra forma +de especificar un argumento no autocontenido. + +00:01:56.166 --> 00:01:57.536 align:middle +Comenta el global bind. + +00:01:58.346 --> 00:02:00.156 align:middle +Todavía me gusta hacer esto... + +00:02:00.246 --> 00:02:02.486 align:middle +pero vamos a probar la nueva forma. + +00:02:03.196 --> 00:02:07.306 align:middle +Si actualizamos ahora, obtendremos +un error porque Symfony no sabe qué + +00:02:07.306 --> 00:02:09.466 align:middle +pasar al argumento $isDebug. + +00:02:10.176 --> 00:02:17.486 align:middle +Para solucionarlo, entra en MixRepository y, +por encima del argumento (o antes del argumento + +00:02:17.566 --> 00:02:23.416 align:middle +si no estás usando varias líneas), añade +un atributo de PHP 8 llamado Autowire. + +00:02:24.236 --> 00:02:30.286 align:middle +Normalmente, los atributos de PHP 8 se +autocompletan, pero este no se autocompleta para mí. + +00:02:31.006 --> 00:02:33.516 align:middle +Esto se debe a un error en PhpStorm. + +00:02:34.206 --> 00:02:37.016 align:middle +Para evitarlo, voy a escribir Autowire... + +00:02:37.546 --> 00:02:42.356 align:middle +y luego ir a la parte superior y empezar a +añadir la declaración use para esto manualmente, + +00:02:42.816 --> 00:02:45.686 align:middle +lo que nos da una opción de autocompletar. + +00:02:46.206 --> 00:02:47.606 align:middle +Pulsa "tab" y... + +00:02:47.716 --> 00:02:51.896 align:middle +¡tah dah! Si quieres hacerlos por +orden alfabético, puedes moverlo. + +00:02:53.316 --> 00:02:56.616 align:middle +También puedes observar que +está subrayado con un mensaje + +00:02:56.996 --> 00:02:59.026 align:middle +No se puede aplicar un +atributo a una propiedad [ ...] + +00:02:59.646 --> 00:03:06.406 align:middle +De nuevo, PhpStorm está un poco confundido porque esto +es tanto una propiedad como un argumento. De todos modos, + +00:03:07.216 --> 00:03:12.866 align:middle +sigue adelante y pasa esto como un +argumento %kernel.debug%. Actualiza + +00:03:13.646 --> 00:03:15.536 align:middle +ahora y... + +00:03:16.046 --> 00:03:18.096 align:middle +¡lo tienes! Bastante bien, +¿verdad? Lo siguiente: la + +00:03:19.046 --> 00:03:24.116 align:middle +mayoría de las veces, cuando se autoconduce +un argumento como HttpClientInterface, + +00:03:24.406 --> 00:03:28.686 align:middle +sólo hay un servicio en el contenedor +que implementa esa interfaz. Pero, + +00:03:29.276 --> 00:03:32.686 align:middle +¿y si hubiera varios clientes +HTTP en nuestro contenedor? ¿Cómo + +00:03:33.286 --> 00:03:36.266 align:middle +podríamos elegir el que queremos? Es + +00:03:36.886 --> 00:03:39.586 align:middle +hora de hablar del autocableado con nombre diff --git a/sfcasts/ep2-fundamentals/es/http-client.md b/sfcasts/ep2-fundamentals/es/http-client.md new file mode 100644 index 0000000..7fae01d --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/http-client.md @@ -0,0 +1,85 @@ +# El servicio cliente HTTP + +Todavía no tenemos una base de datos... y eso lo dejaremos para un futuro tutorial. Pero para hacer las cosas un poco más divertidas, he creado un repositorio de GitHub - https://github.com/SymfonyCasts/vinyl-mixes - con un archivo `mixes.json` que contiene una base de datos falsa de mezclas de vinilos. Hagamos una petición HTTP desde nuestra aplicación Symfony a este archivo y usémoslo como nuestra base de datos temporal. + +Entonces... ¿cómo podemos hacer peticiones HTTP en Symfony? Bueno, hacer una petición HTTP es un trabajo, y -dilo conmigo ahora- "El trabajo lo hace un servicio". Así que la siguiente pregunta es ¿Existe ya un servicio en nuestra aplicación que pueda hacer peticiones HTTP? + +¡Averigüémoslo! Dirígete a tu terminal y ejecuta + +```terminal +php bin/console debug:autowiring http +``` + +para buscar "http" en los servicios. Obtenemos un montón de resultados, pero... nada que parezca un cliente HTTP. Y, eso es correcto. Actualmente no hay ningún servicio en nuestra aplicación que pueda hacer peticiones HTTP. + +## Instalar el componente HTTPClient + +Pero podemos instalar otro paquete que nos proporcione uno. En tu terminal, escribe: + +```terminal +composer require symfony/http-client +``` + +Pero, antes de ejecutarlo, quiero mostrarte de dónde viene este comando. Busca "symfony http client". Uno de los primeros resultados es la documentación de Symfony.com que enseña sobre un componente Cliente HTTP. Recuerda: Symfony es una colección de muchas bibliotecas diferentes, llamadas componentes. ¡Y éste nos ayuda a realizar peticiones HTTP! + +Cerca de la parte superior, verás una sección llamada "Instalación", ¡y ahí está la línea de nuestro terminal! + +De todos modos, si ejecutamos eso... ¡genial! Una vez que termine, prueba de nuevo el comando `debug:autowiring`: + +```terminal-silent +php bin/console debug:autowiring http +``` + +Y... ¡aquí está! Justo en la parte inferior: `HttpClientInterface` que + +> Proporciona métodos flexibles para solicitar recursos HTTP de forma sincrónica o +> de forma asíncrona. + +## El FrameworkBundle superinteligente + +¡Guau! ¡Acabamos de conseguir un nuevo servicio! Eso significa que debemos haber instalado un nuevo bundle, ¿verdad? Porque... ¿los bundles nos dan servicios? Bueno... ve a ver`config/bundles.php`: + +[[[ code('31a0dbdbc7') ]]] + +¡Woh! ¡Aquí no hay ningún bundle nuevo! Prueba a ejecutar + +```terminal +git status +``` + +Sí... eso sólo instaló un paquete PHP normal. Dentro de `composer.json`, aquí está el nuevo paquete... Pero es sólo una "biblioteca": no un bundle. + +[[[ code('d946582375') ]]] + +Así que, normalmente, si instalas "sólo" una biblioteca PHP, te da clases PHP, pero no se engancha a Symfony para darte nuevos servicios. Lo que acabamos de ver es un truco especial que utilizan muchos de los componentes de Symfony. El bundle principal de nuestra aplicación es `framework-bundle`. De hecho, cuando empezamos nuestro proyecto, éste era el único bundle que teníamos. `framework-bundle` es inteligente. Cuando instalas un nuevo componente de Symfony -como el componente Cliente HTTP- ese bundle se da cuenta de la nueva biblioteca y añade automáticamente los servicios para ella. + +Así que el nuevo servicio proviene de `framework-bundle`... que lo añade en cuanto detecta que el paquete `http-client` está instalado. + +## Utilizar el servicio HttpClientInterface + +De todos modos, es hora de utilizar el nuevo servicio. El tipo que necesitamos es `HttpClientInterface`. Dirígete a `VinylController.php` y, aquí arriba, en la acción `browse()`, autocablea `HttpClientInterface` y llamémoslo `$httpClient`: + +[[[ code('f67b91f6c3') ]]] + +Entonces, en lugar de llamar a `$this->getMixes()`, di `$response = $httpClient->`. +Esto lista todos sus métodos... probablemente queramos `request()`. Pasa este `GET`... +y luego pegaré la URL: puedes copiarla del bloque de código de esta página. +Es un enlace directo al contenido del archivo `mixes.json`: + +[[[ code('f33fdc893f') ]]] + +¡Genial! Así que hacemos la petición y nos devuelve una respuesta que contiene los datos de `mixes.json`que vemos aquí. Afortunadamente, estos datos tienen todas las mismas claves que los antiguos datos que estábamos utilizando aquí abajo... así que deberíamos poder cambiarlos con mucha facilidad. Para obtener los datos mixtos de la respuesta, podemos decir `$mixes = $response->toArray()`: + +[[[ code('b6660098c4') ]]] + +¡un práctico método que decodifica los datos en JSON por nosotros! + +¡Momento de la verdad! Muévete, actualiza y... ¡funciona! Ahora tenemos seis mezclas en la página. Y... ¡superguay! Apareció un nuevo icono en la barra de herramientas de depuración de la web: "Total de peticiones: 1". El servicio Cliente HTTP se engancha a la barra de herramientas de depuración web para añadir esto, lo cual es bastante impresionante. Si hacemos clic en él, podemos ver información sobre la petición y la respuesta. Eso me encanta. + +Para celebrar que esto funciona, vuelve a girar y elimina el método `getMixes()`codificado: + +[[[ code('fe38cc18e5') ]]] + +El único problema que se me ocurre ahora es que, cada vez que alguien visita nuestra página, estamos haciendo una petición HTTP a la API de GitHub... ¡y las peticiones HTTP son lentas! Para empeorar las cosas, una vez que nuestro sitio se haga superpopular -lo que no tardará mucho- la API de GitHub probablemente empezará a limitarnos la velocidad. + +Para solucionar esto, vamos a aprovechar otro servicio de Symfony: el servicio de caché. diff --git a/sfcasts/ep2-fundamentals/es/http-client.vtt b/sfcasts/ep2-fundamentals/es/http-client.vtt new file mode 100644 index 0000000..86527f2 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/http-client.vtt @@ -0,0 +1,302 @@ +WEBVTT + +00:00:01.026 --> 00:00:02.726 align:middle +Todavía no tenemos una base de datos... + +00:00:03.086 --> 00:00:05.786 align:middle +y lo dejaremos para un futuro tutorial. + +00:00:06.386 --> 00:00:10.236 align:middle +Pero para hacer las cosas un poco más +divertidas, he creado un repositorio de GitHub - + +00:00:10.486 --> 00:00:15.656 align:middle +https://github.com/SymfonyCasts/vinyl-mixes +- con un archivo mixes.json + +00:00:15.776 --> 00:00:19.016 align:middle +que contiene una base de datos +falsa de mezclas de vinilos. + +00:00:19.586 --> 00:00:24.856 align:middle +Hagamos una petición HTTP desde nuestra +aplicación Symfony a este archivo + +00:00:25.176 --> 00:00:28.226 align:middle +y usémoslo como nuestra +base de datos temporal. + +00:00:29.186 --> 00:00:33.226 align:middle +Entonces... ¿cómo podemos +hacer peticiones HTTP en Symfony? + +00:00:33.806 --> 00:00:42.596 align:middle +Bueno, hacer una petición HTTP es un trabajo, y +-dilo conmigo ahora- "El trabajo lo hace un servicio". + +00:00:43.416 --> 00:00:51.236 align:middle +Así que la siguiente pregunta es: ¿Existe ya un servicio +en nuestra aplicación que pueda hacer peticiones HTTP? + +00:00:51.966 --> 00:00:52.736 align:middle +¡Averigüémoslo! + +00:00:53.146 --> 00:00:58.276 align:middle +Dirígete a tu terminal y ejecuta +php bin/console debug:autowiring http + +00:00:58.666 --> 00:01:01.266 align:middle +para buscar "http" en los servicios. + +00:01:02.076 --> 00:01:04.676 align:middle +Obtenemos un montón de resultados, pero... + +00:01:04.816 --> 00:01:08.236 align:middle +nada que parezca un cliente HTTP. + +00:01:08.746 --> 00:01:10.386 align:middle +Y, eso es correcto. + +00:01:10.876 --> 00:01:16.516 align:middle +Actualmente no hay ningún servicio en nuestra +aplicación que pueda hacer peticiones HTTP. + +00:01:17.276 --> 00:01:20.876 align:middle +Pero podemos instalar otro +paquete que nos proporcione uno. + +00:01:21.696 --> 00:01:29.416 align:middle +En tu terminal, escribe: composer require +symfony/http-client Pero, antes de ejecutarlo, + +00:01:29.776 --> 00:01:32.686 align:middle +quiero mostrarte de dónde viene este comando. + +00:01:33.166 --> 00:01:35.706 align:middle +Busca "symfony http client". + +00:01:36.386 --> 00:01:40.056 align:middle +Uno de los primeros resultados es +la documentación de Symfony.com + +00:01:40.346 --> 00:01:44.176 align:middle +que enseña el componente Cliente HTTP. + +00:01:44.686 --> 00:01:50.866 align:middle +Recuerda: Symfony es una colección de muchas +bibliotecas diferentes, llamadas componentes. + +00:01:51.406 --> 00:01:55.006 align:middle +¡Y éste nos ayuda a realizar peticiones HTTP! + +00:01:55.786 --> 00:01:59.186 align:middle +Cerca de la parte superior, verás +una sección llamada "Instalación", + +00:01:59.556 --> 00:02:02.226 align:middle +¡y ahí está la línea de nuestro terminal! + +00:02:02.866 --> 00:02:05.186 align:middle +De todos modos, si ejecutamos eso... + +00:02:05.836 --> 00:02:12.626 align:middle +¡genial! Una vez que termine, prueba de +nuevo el comando debug:autowiring: Y... + +00:02:13.046 --> 00:02:13.806 align:middle +¡aquí está! + +00:02:14.106 --> 00:02:20.136 align:middle +Justo en la parte inferior: HttpClientInterface +que proporciona métodos flexibles + +00:02:20.136 --> 00:02:24.986 align:middle +para solicitar recursos HTTP de +forma sincrónica o asincrónica. + +00:02:25.776 --> 00:02:28.246 align:middle +¡Vaya! ¡Acabamos de +conseguir un nuevo servicio! + +00:02:28.616 --> 00:02:33.686 align:middle +Eso significa que debemos haber +instalado un nuevo bundle, ¿no? + +00:02:34.326 --> 00:02:35.036 align:middle +Porque... + +00:02:35.186 --> 00:02:36.996 align:middle +¿los bundles nos dan servicios? + +00:02:37.676 --> 00:02:41.346 align:middle +Bueno... ve a ver config/bundles.php: + +00:02:42.346 --> 00:02:45.486 align:middle +¡Woh! ¡Aquí no hay ningún bundle nuevo! + +00:02:46.416 --> 00:02:49.116 align:middle +Prueba a ejecutar git status Sí... + +00:02:49.506 --> 00:02:53.136 align:middle +que sólo ha instalado un paquete PHP normal. + +00:02:53.696 --> 00:02:57.566 align:middle +Dentro de composer.json, aquí +está el nuevo pa quete... + +00:02:58.096 --> 00:03:01.756 align:middle +Pero es sólo una "biblioteca": no un bundle. + +00:03:02.616 --> 00:03:09.826 align:middle +Así que, normalmente, si instalas "sólo" +una biblioteca PHP, te da clases PHP, + +00:03:10.216 --> 00:03:14.356 align:middle +pero no se engancha a Symfony +para darte nuevos servicios. + +00:03:14.356 --> 00:03:20.116 align:middle +Lo que acabamos de ver es un truco especial que +utilizan muchos de los componentes de Symfony. + +00:03:20.806 --> 00:03:23.596 align:middle +El bundle principal de nuestra +aplicación es framework-bundle. + +00:03:24.076 --> 00:03:29.266 align:middle +De hecho, cuando empezamos nuestro proyecto, +éste era el único bundle que teníamos. + +00:03:29.916 --> 00:03:31.856 align:middle +framework-bundle es inteligente. + +00:03:32.376 --> 00:03:37.206 align:middle +Cuando instalas un nuevo componente de +Symfony -como el componente Cliente HTTP- + +00:03:37.746 --> 00:03:43.956 align:middle +ese bundle se da cuenta de la nueva biblioteca y +añade automáticamente los servicios para ella. + +00:03:44.616 --> 00:03:47.786 align:middle +Así que el nuevo servicio +viene de framework-bundle... + +00:03:48.236 --> 00:03:53.926 align:middle +que lo añade en cuanto detecta que el +paquete http-client está instalado. + +00:03:54.776 --> 00:03:57.256 align:middle +De todos modos, es hora de +utilizar el nuevo servicio. + +00:03:57.946 --> 00:04:00.886 align:middle +El tipo que necesitamos es +HttpClientInterface. Dirígete + +00:04:01.546 --> 00:04:06.606 align:middle +a VinylController.php y, aquí +arriba, en la acción browse(), + +00:04:06.916 --> 00:04:12.656 align:middle +autocablea HttpClientInterface +y llamémosla $httpClient: + +00:04:13.446 --> 00:04:20.006 align:middle +Entonces, en lugar de llamar a $this->getMixes(), +di $response = $httpClient->. + +00:04:20.776 --> 00:04:22.946 align:middle +Esto hace que aparezcan todos sus métodos... + +00:04:22.946 --> 00:04:25.156 align:middle +probablemente queramos request(). + +00:04:25.916 --> 00:04:26.996 align:middle +Pasa este GET... + +00:04:27.606 --> 00:04:33.036 align:middle +y luego pegaré la URL: puedes copiarla +del bloque de código de esta página. + +00:04:33.746 --> 00:04:37.786 align:middle +Es un enlace directo al +contenido del archivo mixes.json: + +00:04:38.746 --> 00:04:44.886 align:middle +¡Genial! Así que hacemos la petición y nos devuelve +una respuesta que contiene los datos de mixes.json + +00:04:44.916 --> 00:04:46.086 align:middle +que vemos aquí. + +00:04:46.906 --> 00:04:52.916 align:middle +Afortunadamente, estos datos tienen todas las mismas claves +que los antiguos datos que estábamos utilizando aquí abajo... + +00:04:53.386 --> 00:04:56.916 align:middle +así que deberíamos poder +intercambiarlos con mucha facilidad. + +00:04:57.746 --> 00:05:04.086 align:middle +Para obtener los datos mixtos de la respuesta, +podemos decir $mixes = $response->toArray(): + +00:05:04.686 --> 00:05:08.346 align:middle +¡un práctico método que decodifica +los datos en JSON por nosotros! + +00:05:09.046 --> 00:05:10.596 align:middle +¡Momento de la verdad! + +00:05:11.106 --> 00:05:13.486 align:middle +Muévete, refresca y... + +00:05:14.026 --> 00:05:18.436 align:middle +¡funciona! Ahora tenemos +seis mezclas en la página. + +00:05:19.086 --> 00:05:20.716 align:middle +Y... ¡superguay! + +00:05:21.036 --> 00:05:26.856 align:middle +Apareció un nuevo icono en la barra de herramientas +de depuración de la web: "Total de peticiones: 1". + +00:05:27.486 --> 00:05:34.686 align:middle +El servicio Cliente HTTP se engancha a la barra de herramientas de +depuración web para añadir esto, lo cual es bastante impresionante. + +00:05:35.416 --> 00:05:40.096 align:middle +Si hacemos clic en él, podemos ver +información sobre la petición y la respuesta. + +00:05:40.406 --> 00:05:41.616 align:middle +Eso me encanta. + +00:05:42.776 --> 00:05:49.376 align:middle +Para celebrar que esto funciona, vuelve a girar +y elimina el método getMixes() codificado: + +00:05:51.536 --> 00:05:57.196 align:middle +El único problema que se me ocurre ahora es +que, cada vez que alguien visita nuestra página, + +00:05:57.486 --> 00:06:02.516 align:middle +estamos haciendo una petición +HTTP a la API de GitHub... + +00:06:02.516 --> 00:06:04.726 align:middle +¡y las peticiones HTTP son lentas! Para + +00:06:05.446 --> 00:06:11.316 align:middle +empeorar las cosas, una vez que nuestro sitio se +vuelva superpopular -lo que no tardará mucho- + +00:06:11.836 --> 00:06:15.256 align:middle +la API de GitHub probablemente +empezará a limitarnos la tasa. + +00:06:16.076 --> 00:06:21.906 align:middle +Para solucionar esto, vamos a aprovechar otro +servicio de Symfony: el servicio de caché diff --git a/sfcasts/ep2-fundamentals/es/maker-command.md b/sfcasts/ep2-fundamentals/es/maker-command.md new file mode 100644 index 0000000..92a1184 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/maker-command.md @@ -0,0 +1,70 @@ +# MakerBundle y Autoconfiguración + +¡Felicidades, equipo! ¡Hemos terminado con lo más pesado de este tutorial! Así que es hora de dar la vuelta de la victoria. Vamos a instalar uno de mis bundles favoritos de Symfony: MakerBundle. Busca tu terminal y ejecuta: + +```terminal +composer require maker --dev +``` + +En este caso, estoy usando la bandera `--dev` porque se trata de una utilidad de generación de código que sólo necesitamos localmente, no en producción. + +Este bundle, por supuesto, proporciona servicios. Pero estos servicios no están pensados para que los utilicemos directamente. En su lugar, todos los servicios de este bundle potencian un montón de nuevos comandos de `bin/console`. Ejecuta + +```terminal +php bin/console +``` + +y busca la sección `make`. Ooh. Aquí hay un montón de cosas para configurar la seguridad, generar entidades de doctrina para la base de datos (lo que haremos en el siguiente tutorial), hacer un CRUD, y mucho más. + +## Generar una nueva clase de mando + +Vamos a probar una: ¿qué tal si intentamos construir nuestro propio y nuevo comando de consola personalizado que aparecerá en esta lista? Para ello, ejecuta: + +```terminal +php bin/console make:command +``` + +Esto te pedirá interactivamente el nombre del comando. Digamos`app:talk-to-me`. No es necesario, pero es bastante habitual anteponer a tus comandos personalizados el prefijo `app:`. Y... ¡listo! + +Eso ha creado exactamente un nuevo archivo: `src/Command/TalkToMeCommand.php`. Vamos a abrirlo: + +[[[ code('01fe532ac9') ]]] + +¡Genial! ¡Arriba, puedes ver que el nombre y la descripción del comando se hacen en un atributo PHP! Luego, abajo en este método `configure()`, del que hablaremos más en un minuto, podemos configurar los argumentos y opciones que se pueden pasar desde la línea de comandos. + +Cuando ejecutemos el comando, se llamará a `execute()`... donde podemos imprimir cosas en la pantalla o leer opciones y argumentos. + +Quizá lo mejor de esta clase es que... ya funciona. ¡Compruébalo! De vuelta a tu terminal, ejecuta; + +```terminal +php bin/console app:talk-to-me +``` + +Y... ¡está vivo! No hace mucho, pero esta salida viene de aquí abajo. ¡Guau! + +## Autoconfiguración: Descubriendo automáticamente los "plugins" + +Pero espera... ¿cómo ha visto Symfony instantáneamente nuestra nueva clase `Command` y ha sabido que debe empezar a utilizarla? ¿Es porque vive en el directorio `src/Command/`... y Symfony escanea las clases que viven aquí? No Podríamos cambiar el nombre de este directorio a`ThereAreDefinitelyNoCommandsInHere`... y Symfony seguiría viendo el comando. + +La forma en que esto funciona es mucho más genial. Abre `config/services.yaml` y mira la sección `_defaults`: + +[[[ code('99a7530291') ]]] + +Hemos hablado de lo que significa `autowire: true`, pero no he explicado el propósito de`autoconfigure: true`. Como está por debajo de `_defaults`, la autoconfiguración está activa en todos nuestros servicios, incluido nuestro nuevo servicio `TalkToMeCommand`. +Cuando `autoconfiguration` está activado, básicamente le dice a Symfony: + +> Oye, por favor, mira la clase base o la interfaz de cada servicio, y si +> parece que una clase debe ser un comando de consola... o un suscriptor de eventos... +> o cualquier otra clase que se enganche a una parte de Symfony, por favor, integra automáticamente +> integra el servicio en ese sistema. Bien, gracias. ¡Adiós! + +¡Si! Symfony ve que nuestra clase extiende `Command` y piensa: + +> Hmm, puede que no sea una IA autoconsciente... pero apuesto a que esto es un comando. Será mejor que se lo notifique al +¡> al sistema de la consola sobre ello! + +Me encanta la autoconfiguración. Significa que podemos crear una clase PHP, extender cualquier clase base o implementar cualquier interfaz necesaria para la "cosa" que estamos construyendo, y... simplemente funcionará. + +Internamente, si quieres conocer todos los detalles frikis, la autoconfiguración añade una etiqueta a tu servicio, como `console.command`, que es lo que, en última instancia, ayuda a que el sistema de la consola se fije en él. + +Muy bien, ahora que nuestro comando funciona, vamos a divertirnos un poco y a personalizarlo a continuación. diff --git a/sfcasts/ep2-fundamentals/es/maker-command.vtt b/sfcasts/ep2-fundamentals/es/maker-command.vtt new file mode 100644 index 0000000..d0bf694 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/maker-command.vtt @@ -0,0 +1,237 @@ +WEBVTT + +00:00:01.016 --> 00:00:01.756 align:middle +¡Felicidades, equipo! + +00:00:02.006 --> 00:00:04.976 align:middle +¡Hemos terminado con lo +más pesado de este tutorial! + +00:00:04.976 --> 00:00:07.526 align:middle +Así que es hora de dar +la vuelta de la victoria. + +00:00:08.256 --> 00:00:11.676 align:middle +Vamos a instalar uno de mis bundles +favoritos de Symfony: MakerBundle. + +00:00:11.676 --> 00:00:18.836 align:middle +Busca tu terminal y ejecuta: composer require +maker --dev En este caso, estoy usando la + +00:00:19.026 --> 00:00:23.186 align:middle +bandera--dev porque se trata de una +utilidad de generación de código + +00:00:23.276 --> 00:00:26.596 align:middle +que sólo necesitamos +localmente, no en producción. + +00:00:27.536 --> 00:00:30.506 align:middle +Este bundle, por supuesto, +proporciona servicios. + +00:00:30.986 --> 00:00:35.166 align:middle +Pero estos servicios no están pensados +para que los utilicemos directamente. + +00:00:35.616 --> 00:00:42.606 align:middle +En su lugar, todos los servicios de este bundle +potencian un montón de nuevos comandos de bin/console. + +00:00:43.286 --> 00:00:47.586 align:middle +Ejecuta php bin/console +y busca la sección make. + +00:00:48.136 --> 00:00:54.766 align:middle +Ooh. Aquí hay un montón de cosas para configurar +la seguridad, generar entidades de doctrina + +00:00:54.766 --> 00:01:00.156 align:middle +para la base de datos (lo que haremos en el próximo +tutorial), hacer un CRUD, y mucho más. Vamos a + +00:01:00.916 --> 00:01:06.166 align:middle +probar una: ¿qué tal si intentamos construir +nuestro propio comando de consola personalizado + +00:01:06.406 --> 00:01:08.036 align:middle +que aparecerá en esta lista? + +00:01:08.676 --> 00:01:15.856 align:middle +Para ello, ejecuta: php bin/console +make:command Esto te pedirá interactivamente + +00:01:15.856 --> 00:01:17.066 align:middle +el nombre del comando. + +00:01:17.546 --> 00:01:19.856 align:middle +Digamos app:talk-to-me. + +00:01:20.516 --> 00:01:26.106 align:middle +No es necesario, pero es bastante habitual anteponer +a tus comandos personalizados el prefijo app:. + +00:01:26.876 --> 00:01:28.156 align:middle +Y... ¡listo! + +00:01:28.946 --> 00:01:36.376 align:middle +Eso ha creado exactamente un nuevo +archivo: src/Command/TalkToMeCommand.php. + +00:01:36.376 --> 00:01:37.416 align:middle +Vamos a abrirlo: + +00:01:39.636 --> 00:01:43.256 align:middle +¡Genial! ¡Arriba , puedes ver +que el nombre y la descripción + +00:01:43.256 --> 00:01:45.836 align:middle +del comando se hacen en un atributo PHP! + +00:01:46.576 --> 00:01:50.846 align:middle +Luego, abajo en este método configure(), +del que hablaremos más en un minuto, + +00:01:51.146 --> 00:01:54.976 align:middle +podemos configurar los argumentos y opciones +que se pueden pasar desde la línea de comandos. + +00:01:55.716 --> 00:01:58.726 align:middle +Cuando ejecutemos el comando, +se llamará a execute()... + +00:01:59.076 --> 00:02:03.856 align:middle +donde podemos imprimir cosas en la pantalla +o leer las opciones y los argumentos. + +00:02:04.566 --> 00:02:07.816 align:middle +Quizá lo mejor de esta clase es que... + +00:02:07.816 --> 00:02:09.596 align:middle +ya funciona. + +00:02:10.116 --> 00:02:10.636 align:middle +¡Compruébalo! + +00:02:10.906 --> 00:02:15.626 align:middle +De vuelta a tu terminal, ejecuta; +php bin/console app:talk-to-me Y... + +00:02:16.046 --> 00:02:17.356 align:middle +¡está vivo! + +00:02:17.906 --> 00:02:22.526 align:middle +No hace mucho, pero esta +salida viene de aquí abajo. + +00:02:22.906 --> 00:02:24.866 align:middle +¡Guau! Pero espera... + +00:02:25.306 --> 00:02:30.966 align:middle +¿cómo es que Symfony ha visto instantáneamente nuestra +nueva clase Command y ha sabido empezar a utilizarla? + +00:02:31.606 --> 00:02:35.556 align:middle +¿Es porque vive en el +directorio src/Command/... + +00:02:35.906 --> 00:02:38.796 align:middle +y Symfony busca las clases que viven aquí? + +00:02:39.486 --> 00:02:44.936 align:middle +No Podríamos cambiar el nombre de este +directorio a ThereAreDefinitelyNoCommandsInHere... + +00:02:44.936 --> 00:02:48.066 align:middle +y Symfony seguiría viendo el comando. + +00:02:48.776 --> 00:02:50.796 align:middle +La forma en que esto funciona +es mucho más genial. + +00:02:51.316 --> 00:02:56.026 align:middle +Abre config/services.yaml y +mira la sección _defaults: + +00:02:56.616 --> 00:02:58.996 align:middle +Hemos hablado de lo que +significa autowire: true, + +00:02:59.176 --> 00:03:02.316 align:middle +pero no he explicado el +propósito de autoconfigure: true. + +00:03:03.066 --> 00:03:08.296 align:middle +Como está por debajo de _defaults, la +autoconfiguración está activa en todos + +00:03:08.296 --> 00:03:12.576 align:middle +nuestros servicios, incluido nuestro +nuevo servicio TalkToMeCommand. + +00:03:13.376 --> 00:03:18.456 align:middle +Cuando autoconfiguration está activado, +básicamente le dice a Symfony: Oye, + +00:03:18.846 --> 00:03:22.976 align:middle +por favor, mira la clase base +o la interfaz de cada servicio, + +00:03:23.376 --> 00:03:27.466 align:middle +y si parece que una clase debe +ser un comando de consola... + +00:03:27.466 --> 00:03:29.186 align:middle +o un suscriptor de eventos... o + +00:03:29.546 --> 00:03:32.696 align:middle +cualquier otra clase que se enganche +a una parte de Symfony, por favor, + +00:03:33.016 --> 00:03:36.896 align:middle +integra automáticamente +el servicio en ese sistema. + +00:03:37.246 --> 00:03:37.866 align:middle +Bien, gracias. + +00:03:37.866 --> 00:03:43.416 align:middle +¡Adiós! ¡Sí! Symfony ve que nuestra +clase extiende Command y piensa: + +00:03:43.776 --> 00:03:47.466 align:middle +Hmm, puede que no sea una IA autoconsciente... + +00:03:47.466 --> 00:03:49.806 align:middle +pero seguro que esto es un comando. + +00:03:50.376 --> 00:03:53.496 align:middle +¡Será mejor que lo notifique +al sistema de la consola! + +00:03:53.496 --> 00:03:55.416 align:middle +Me encanta la autoconfiguración. + +00:03:55.876 --> 00:04:00.566 align:middle +Significa que podemos crear una clase +PHP, extender cualquier clase base + +00:04:00.566 --> 00:04:05.736 align:middle +o implementar cualquier interfaz necesaria +para la "cosa" que estamos construyendo, y... + +00:04:05.996 --> 00:04:07.766 align:middle +simplemente funcionará. + +00:04:08.576 --> 00:04:15.026 align:middle +Internamente, si quieres conocer todos los detalles frikis, +la autoconfiguración añade una etiqueta a tu servicio, + +00:04:15.116 --> 00:04:20.726 align:middle +como console.command, que es lo que, en última instancia, +ayuda a que el sistema de la consola se fije en él. + +00:04:21.586 --> 00:04:26.276 align:middle +Muy bien, ahora que nuestro comando funciona, vamos a +divertirnos un poco y a personalizarlo a continuación diff --git a/sfcasts/ep2-fundamentals/es/named-autowiring.md b/sfcasts/ep2-fundamentals/es/named-autowiring.md new file mode 100644 index 0000000..10fdc41 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/named-autowiring.md @@ -0,0 +1,51 @@ +# Autoconexión con nombre y clientes HTTP con alcance + +En `MixRepository`, sería genial que no tuviéramos que especificar el nombre del host cuando hacemos la petición HTTP. Sería genial que eso estuviera preconfigurado y sólo tuviéramos que incluir la ruta. Además, muy pronto, vamos a configurar un token de acceso que se utilizará cuando hagamos peticiones a la API de GitHub. Podríamos pasar ese token de acceso manualmente aquí en nuestro servicio, pero ¿a qué sería genial que el servicio HttpClient viniera preconfigurado para incluir siempre el token de acceso? + +Entonces, ¿tiene Symfony una forma de "preconfigurar" el servicio HttpClient? ¡La tiene! Se llama "scoped clients": una característica de HttpClient que permite crear varios servicios HttpClient, cada uno preconfigurado de forma diferente. + +## Crear un Scoped Client + +Así es como funciona. Abre `config/packages/framework.yaml`. Para crear un scoped client, bajo la clave `framework`, añade `http_client` seguido de `scoped_clients`. Ahora, dale a tu scoped client un nombre, como `githubContentClient`... ya que estamos utilizando una parte de su API que devuelve el contenido de los archivos. Añade también `base_uri`, ve copiando el nombre del host por aquí... y pégalo: + +[[[ code('ebc7952f44') ]]] + +Recuerda: el objetivo de estos archivos de configuración es cambiar los servicios del contenedor. El resultado final de este nuevo código es que se añadirá un segundo servicio HttpClient al contenedor. Lo veremos dentro de un minuto. Y, por cierto, no hay forma de que adivines que necesitas las claves `http_client` y `scoped_clients`para que esto funcione. La configuración es el tipo de cosa en la que realmente tienes que confiar en la documentación. + +De todos modos, ahora que hemos preconfigurado este cliente, deberíamos poder entrar en`MixRepository` y hacer una petición directamente a la ruta: + +[[[ code('2f3d36b908') ]]] + +Pero si nos dirigimos y refrescamos... ah... + +> URL no válida: falta el esquema [...]. ¿Te has olvidado de añadir "http(s)://"? + +No creí que nos hubiéramos olvidado... ya que lo configuramos mediante la opción `base_uri`... pero parece que no funcionó. Y puede que hayas adivinado por qué. Busca tu terminal y ejecuta: + +```terminal +php bin/console debug:autowiring client +``` + +Ahora hay dos servicios HttpClient en el contenedor: El normal, no configurado, y el que acabamos de configurar. Aparentemente, en`MixRepository`, Symfony nos sigue pasando el servicio HttpClient no configurado. + +¿Cómo puedo estar seguro? Bueno, piensa en cómo funciona el autocableado. Symfony mira el tipo de pista de nuestro argumento, que es`Symfony\Contracts\HttpClient\HttpClientInterface`, y luego busca en el contenedor un servicio cuyo ID coincida exactamente. Es así de sencillo + +## Obtener la versión con nombre de un servicio + +Entonces... si hay varios servicios con el mismo "tipo" en nuestro contenedor, ¿sólo el principal es autoconvocable? Afortunadamente, ¡no! Podemos utilizar algo llamado "autoconexión con nombre"... y ya nos muestra cómo hacerlo. Si tecleamos un argumento con`HttpClientInterface` y nombramos el argumento `$githubContentClient`, Symfony nos pasará el segundo. + +Probemos: cambiemos el argumento de `$httpClient` a `$githubContentClient`: + +[[[ code('319f5e30f8') ]]] + +y ahora... no funciona. Ups... + +> Propiedad no definida `MixRepository::$httpClient` + +Eso es... que me he descuidado. Cuando cambié el nombre del argumento, cambió el nombre de la propiedad. Así que... hay que ajustar el código de abajo: + +[[[ code('2764098093') ]]] + +Y ahora... ¡está vivo! ¡Acabamos de autocablear un servicio específico de HttpClientInterface! + +A continuación, vamos a abordar otro problema complicado con la autoconexión, aprendiendo a obtener uno de los muchos servicios de nuestro contenedor que no está disponible para la autoconexión. diff --git a/sfcasts/ep2-fundamentals/es/named-autowiring.vtt b/sfcasts/ep2-fundamentals/es/named-autowiring.vtt new file mode 100644 index 0000000..9df2b33 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/named-autowiring.vtt @@ -0,0 +1,219 @@ +WEBVTT + +00:00:01.016 --> 00:00:05.116 align:middle +En MixRepository, sería +genial que no tu viéramos + +00:00:05.116 --> 00:00:09.416 align:middle +que especificar el nombre del host +cuando hagamos la petición HTTP. + +00:00:10.116 --> 00:00:15.806 align:middle +Sería genial que eso estuviera preconfigurado +y sólo tuviéramos que incluir la ruta. + +00:00:16.516 --> 00:00:21.796 align:middle +Además, muy pronto, vamos a configurar un +token de acceso que se utilizará cuando + +00:00:21.796 --> 00:00:24.446 align:middle +hagamos peticiones a la API de GitHub. + +00:00:25.456 --> 00:00:32.446 align:middle +Podríamos pasar ese token de acceso manualmente +aquí en nuestro servicio, pero ¿a qué sería genial + +00:00:32.446 --> 00:00:38.356 align:middle +que el servicio HttpClient viniera preconfigurado +para incluir siempre el token de acceso? + +00:00:38.976 --> 00:00:45.346 align:middle +Entonces, ¿tiene Symfony una forma de +"preconfigurar" el servicio HttpClient? + +00:00:45.896 --> 00:00:52.466 align:middle +¡La tiene! Se llama "scoped clients": +una característica de HttpClient + +00:00:52.636 --> 00:00:58.416 align:middle +que permite crear varios servicios HttpClient, +cada uno preconfigurado de forma diferente. + +00:00:59.146 --> 00:01:00.016 align:middle +Así es como funciona. + +00:01:00.486 --> 00:01:03.226 align:middle +Abre config/packages/framework.yaml. + +00:01:04.056 --> 00:01:12.896 align:middle +Para crear un cliente de ámbito, bajo la clave +framework, añade http_client seguido de scoped_clients. + +00:01:13.706 --> 00:01:18.446 align:middle +Ahora, dale a tu cliente de ámbito +un nombre, como githubContentClient... + +00:01:18.966 --> 00:01:23.516 align:middle +ya que estamos utilizando una parte de su API +que devuelve el contenido de los archivos. + +00:01:24.356 --> 00:01:28.636 align:middle +Añade también base_uri, ve a copiar +el nombre del host por aquí... + +00:01:31.306 --> 00:01:32.366 align:middle +y pégalo: + +00:01:33.216 --> 00:01:39.826 align:middle +Recuerda: el objetivo de estos archivos de configuración +es cambiar los servicios del contenedor. El + +00:01:40.776 --> 00:01:46.766 align:middle +resultado final de este nuevo código es que +se añadirá un segundo servicio HttpClient + +00:01:46.766 --> 00:01:47.626 align:middle +al contenedor. + +00:01:47.916 --> 00:01:49.366 align:middle +Lo veremos dentro de un minuto. + +00:01:50.016 --> 00:01:56.866 align:middle +Y, por cierto, no hay forma de que adivines +que necesitas las claves http_client + +00:01:57.266 --> 00:02:01.006 align:middle +y scoped_clients para que esto funcione. + +00:02:01.756 --> 00:02:07.356 align:middle +La configuración es el tipo de cosa en la que +realmente tienes que confiar en la documentación. De + +00:02:08.086 --> 00:02:12.756 align:middle +todos modos, ahora que hemos preconfigurado +este cliente, deberíamos poder + +00:02:12.756 --> 00:02:17.446 align:middle +entrar en MixRepository y hacer una +petición directamente a la ruta: + +00:02:18.506 --> 00:02:20.786 align:middle +Pero si nos dirigimos y refrescamos... + +00:02:21.336 --> 00:02:24.886 align:middle +ah... URL no válida: falta el esquema [...]. + +00:02:24.946 --> 00:02:28.416 align:middle +¿Te has olvidado de añadir "http(s)://"? + +00:02:29.046 --> 00:02:31.526 align:middle +No creí que nos hubiéramos olvidado... ya que + +00:02:31.576 --> 00:02:35.216 align:middle +lo configuramos mediante +la opción base_uri... pero + +00:02:35.686 --> 00:02:38.086 align:middle +parece que no funcionó. Y + +00:02:38.506 --> 00:02:40.576 align:middle +puede que hayas adivinado por qué. Busca + +00:02:40.946 --> 00:02:48.236 align:middle +tu terminal y ejecuta: php bin/console debug:autowiring +client Ahora hay dos servicios HttpClient + +00:02:48.236 --> 00:02:54.076 align:middle +en el contenedor: El +normal, no configurado, y el + +00:02:54.446 --> 00:02:56.786 align:middle +que acabamos de configurar. Aparentemente, + +00:02:57.626 --> 00:03:04.696 align:middle +en MixRepository, Symfony nos sigue pasando +el servicio HttpClient no configurado. ¿Cómo + +00:03:05.186 --> 00:03:06.386 align:middle +puedo estar seguro? Bueno, + +00:03:07.146 --> 00:03:10.306 align:middle +piensa en cómo funciona +el autocableado. Symfony + +00:03:10.976 --> 00:03:13.506 align:middle +mira el tipo de pista de nuestro argumento, que + +00:03:13.916 --> 00:03:18.316 align:middle +es Symfony\Contracts\HttpClient\HttpClientInterface, +y luego busca + +00:03:18.886 --> 00:03:24.946 align:middle +en el contenedor un servicio cuyo +ID coincida exactamente. Es así de + +00:03:25.486 --> 00:03:27.686 align:middle +sencillo Entonces... si + +00:03:27.956 --> 00:03:32.476 align:middle +hay varios servicios con el mismo +"tipo" en nuestro contenedor, ¿sólo es + +00:03:32.946 --> 00:03:36.116 align:middle +autocable el principal? Afortunadamente, ¡no! + +00:03:36.936 --> 00:03:38.276 align:middle +Podemos + +00:03:38.946 --> 00:03:41.886 align:middle +utilizar algo llamado +"autoconexión con nombre"... y + +00:03:42.426 --> 00:03:44.386 align:middle +ya nos muestra cómo hacerlo. Si + +00:03:44.936 --> 00:03:48.586 align:middle +tecleamos un argumento con +HttpClientInterface y nombramos + +00:03:48.916 --> 00:03:56.116 align:middle +el argumento $githubContentClient, +Symfony nos pasará el segundo. Probemos + +00:03:56.816 --> 00:04:04.276 align:middle +: cambia el argumento de $httpClient +a $githubContentClient: y ahora ... no + +00:04:04.916 --> 00:04:06.226 align:middle +funciona. Ups... Propiedad no definida: + +00:04:06.746 --> 00:04:11.936 align:middle +MixRepository::$httpClient Eso es... que me + +00:04:12.076 --> 00:04:13.586 align:middle +he descuidado. Cuando + +00:04:14.136 --> 00:04:17.426 align:middle +cambié el nombre del argumento, cambió +el nombre de la propiedad. Así que ... hay + +00:04:17.816 --> 00:04:20.526 align:middle +que ajustar el código de abajo: Y ahora... + +00:04:21.686 --> 00:04:22.576 align:middle +¡está vivo! ¡ + +00:04:23.566 --> 00:04:24.806 align:middle +Acabamos de + +00:04:25.186 --> 00:04:30.236 align:middle +autocablear un servicio específico de +HttpClientInterface! A continuación, vamos a + +00:04:31.176 --> 00:04:36.716 align:middle +abordar otro problema complicado con la +autoconexión, aprendiendo a obtener uno de los + +00:04:36.716 --> 00:04:42.536 align:middle +muchos servicios de nuestro contenedor que +no está disponible para la autoconexión diff --git a/sfcasts/ep2-fundamentals/es/non-autowireable-services.md b/sfcasts/ep2-fundamentals/es/non-autowireable-services.md new file mode 100644 index 0000000..fff2971 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/non-autowireable-services.md @@ -0,0 +1,75 @@ +# Servicios no autoconvocables + +Ejecuta: + +```terminal +php bin/console debug:container +``` + +Y... Haré esto un poco más pequeño para que todo aparezca en una sola línea. Como sabemos, este comando muestra todos los servicios de nuestro contenedor... pero sólo un pequeño número de ellos son autoconectables. Lo sabemos porque un servicio es autoconectable sólo si su ID, que es esto de aquí, es un nombre de clase o de interfaz. + +Así que, al principio, podría parecer que el servicio Twig no es autoconectable. Después de todo, su ID - `twig` - no es en absoluto una clase o interfaz. Pero si te desplazas hasta la parte superior... veamos... ¡sí! Hay otro servicio en el contenedor cuyo ID es `Twig\Environment`, que es un alias del servicio `twig`. Este es un pequeño truco que hace Symfony para hacer que los servicios sean autoconducibles. Si escribimos un argumento con `Twig\Environment`, obtendremos el servicio `twig`. + +Sin embargo, la mayoría de los servicios de esta lista no tienen un alias como ese. Por tanto, no son autoconectables. Y, por lo general, eso está bien. Si un servicio no es autoconectable, probablemente sea porque nunca necesitarás utilizarlo. Pero supongamos que sí queremos utilizar uno de ellos. + +Fíjate en éste Se llama `twig.command.debug`. Abre otra pestaña. Antes hemos ejecutado: + +```terminal +php bin/console debug:twig +``` + +Esto nos muestra todas las funciones y filtros de Twig... ¡lo que está muy bien! Bueno, ¡sorpresa! ¡Este comando viene del servicio `twig.command.debug`! Porque "todo en Symfony lo hace un servicio", incluso los comandos de la consola. + +Como reto, veamos si podemos inyectar este servicio en `MixRepository`, ejecutarlo y volcar su salida. + +## Inyección de dependencia: Añadiendo el nuevo argumento + +Lo primero es lo primero. En `MixRepository`, acabamos de descubrir que, para hacer nuestro trabajo, necesitamos acceder a otro servicio. ¿Qué hacemos? La respuesta: Inyección de dependencia, que es esa elegante palabra para añadir otro argumento de construcción y fijarlo en una propiedad, lo que podemos hacer de una vez con`private $twigDebugCommand`: + +[[[ code('db9765d0f6') ]]] + +Si nos detenemos ahora mismo y refrescamos... ¡no hay sorpresa! Obtendríamos un error. Symfony no tiene ni idea de qué pasar para ese argumento. + +¿Y si añadimos el tipo de esta clase? De vuelta a nuestro terminal, podemos ver que este servicio es una instancia de `DebugCommand`. Por aquí, vamos a añadir ese tipo -consejo: `DebugCommand`... queremos el de `Symfony\Bridge\Twig\Command`. Pulsa "tab" para autocompletarlo: + +[[[ code('1d003f5f47') ]]] + +Y luego... actualiza. ¡Sigue habiendo un error! Vale, deberíamos añadir la sugerencia de tipo porque somos buenos programadores. Pero... por mucho que lo intentemos, esto no es un servicio autocompletable. Entonces, ¿cómo lo arreglamos? + +## Vinculando el argumento en YAML + +Hay dos formas principales. Primero te mostraré la forma antigua, que es la que más hago porque la verás en la documentación y en las entradas del blog por todas partes. En`config/services.yaml`, al igual que hicimos antes para el argumento `$isDebug`, anula nuestro servicio por completo. Digamos `App\Service\MixRepository`, y añadimos una clave`bind`. Entonces, vamos a insinuar lo que hay que pasar al argumento `$twigDebugCommand`. + +Lo único complicado es averiguar qué valor hay que poner. Por ejemplo, si voy y copio el ID del servicio - `twig.command.debug` - y lo pego aquí... ¡eso no va a funcionar! Eso va a pasar literalmente por esa cadena. Si refrescas, ¡sí! + +> El argumento 4 debe ser del tipo `DebugCommand`, cadena dada. + +Tenemos que decirle a Symfony que pase el servicio que tiene este ID. En estos archivos YAML, hay una sintaxis especial para hacer precisamente eso: anteponer al ID del servicio el símbolo`@`: + +[[[ code('447697206a') ]]] + +En cuanto lo hagamos... ¡el hecho de que esto no explote significa que está funcionando! + +## El atributo Autowire + +Pero... vamos a eliminar esto. Porque quiero mostrarte la nueva forma de hacerlo... que aprovecha ese mismo atributo `Autowire`. + +Aquí arriba, digamos `#[Autowire()]`, pero en lugar de pasar sólo una cadena, digamos`service: 'twig.command.debug'`: + +[[[ code('b2b20613e3') ]]] + +## Usando el nuevo Argumento + +¡Me encanta! Antes de probar esto, vamos a utilizar realmente el servicio. Dirígete a`findAll()`. Ejecutar un comando de consola manualmente en tu código PHP es totalmente posible. Es un poco raro, ¡pero genial! Tenemos que crear un objeto`$output = new BufferedOutput()`... luego podemos ejecutar el comando diciendo`$this->twigDebugCommand->run(new ArrayInput())` - esto es, más o menos, fingir los argumentos de la línea de comandos - pasarle un `[]` vacío - luego `$output`. Lo que produzca el comando se fijará en ese objeto. + +Para ver si funciona, basta con `dd($output)`: + +[[[ code('d77a8d195f') ]]] + +¡Tiempo de prueba! Refresca... ¡y lo tienes! Qué divertido es esto + +Muy bien, ahora que esto funciona, vamos a comentar esta tontería. Mantendré nuestro`$twigDebugCommand` inyectado sólo como referencia. + +La conclusión clave es la siguiente: la mayoría de los argumentos a los servicios serán autoconvocables. Pero cuando encuentres un argumento que no sea autoconductible, puedes utilizar el atributo `Autowire`para apuntar al valor o servicio que necesitas. + +Siguiente: ¿Recuerdas cuando te dije que `MixRepository` fue el primer servicio que creamos? Pues... Mentí. ¡Resulta que nuestros controladores han sido servicios todo este tiempo! \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/non-autowireable-services.vtt b/sfcasts/ep2-fundamentals/es/non-autowireable-services.vtt new file mode 100644 index 0000000..d150368 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/non-autowireable-services.vtt @@ -0,0 +1,345 @@ +WEBVTT + +00:00:01.006 --> 00:00:04.146 align:middle +Ejecuta: php bin/console debug:container Y... + +00:00:04.146 --> 00:00:08.316 align:middle +Haré esto un poco más pequeño para +que todo aparezca en una sola línea. + +00:00:09.146 --> 00:00:14.426 align:middle +Como sabemos, este comando muestra todos +los servicios de nuestro contenedor... + +00:00:14.776 --> 00:00:18.966 align:middle +pero sólo un pequeño número +de ellos son autoconectables. + +00:00:19.776 --> 00:00:25.176 align:middle +Lo sabemos porque un servicio es +autoconectable sólo si su ID, + +00:00:25.376 --> 00:00:29.576 align:middle +que es esto de aquí, es un +nombre de clase o de interfaz. + +00:00:29.576 --> 00:00:35.466 align:middle +Así que, al principio, podría parecer +que el servicio Twig no es autoconectable. + +00:00:35.466 --> 00:00:42.226 align:middle +Después de todo, su ID - twig - no +es en absoluto una clase o interfaz. + +00:00:42.946 --> 00:00:45.406 align:middle +Pero si te desplazas hasta la parte superior... + +00:00:45.836 --> 00:00:46.716 align:middle +veamos... + +00:00:47.246 --> 00:00:53.176 align:middle +¡sí! Hay otro servicio en el +contenedor cuyo ID es Twig\Environment, + +00:00:53.376 --> 00:00:56.136 align:middle +que es un alias del servicio twig. + +00:00:56.806 --> 00:01:00.956 align:middle +Este es un pequeño truco que hace Symfony para +hacer que los servicios sean autoconducibles. + +00:01:01.696 --> 00:01:07.056 align:middle +Si escribimos un argumento con +Twig\Environment, obtendremos el servicio twig. + +00:01:07.816 --> 00:01:13.836 align:middle +Sin embargo, la mayoría de los servicios +de esta lista no tienen un alias como ese. + +00:01:13.836 --> 00:01:16.336 align:middle +Por tanto, no son autoconectables. + +00:01:16.936 --> 00:01:19.026 align:middle +Y, por lo general, eso está bien. + +00:01:19.706 --> 00:01:25.906 align:middle +Si un servicio no es autoconectable, probablemente +sea porque nunca necesitarás utilizarlo. + +00:01:26.586 --> 00:01:31.096 align:middle +Pero supongamos que sí +queremos utilizar uno de ellos. + +00:01:31.746 --> 00:01:32.476 align:middle +Fíjate en éste + +00:01:32.746 --> 00:01:35.586 align:middle +Se llama twig.command.debug. + +00:01:36.446 --> 00:01:37.526 align:middle +Abre otra pestaña. + +00:01:38.306 --> 00:01:43.786 align:middle +Antes hemos ejecutado: php bin/console +debug:twig Esto nos muestra todas las + +00:01:43.786 --> 00:01:45.706 align:middle +funciones y filtros de Twig... + +00:01:45.946 --> 00:01:47.126 align:middle +¡lo que está muy bien! + +00:01:47.876 --> 00:01:49.356 align:middle +Bueno, ¡sorpresa! + +00:01:49.606 --> 00:01:54.316 align:middle +¡Este comando proviene del +servicio twig.command.debug! + +00:01:54.876 --> 00:02:00.816 align:middle +Porque "todo en Symfony lo hace un servicio", +incluso los comandos de la consola. + +00:02:01.606 --> 00:02:05.226 align:middle +Como reto, vamos a ver si +podemos inyectar este servicio + +00:02:05.226 --> 00:02:09.836 align:middle +en MixRepository, ejecutarlo +y volcar su salida. + +00:02:09.836 --> 00:02:11.436 align:middle +Lo primero es lo primero. + +00:02:11.756 --> 00:02:16.836 align:middle +En MixRepository, acabamos de descubrir +que, para hacer nuestro trabajo, + +00:02:17.046 --> 00:02:19.596 align:middle +necesitamos acceder a otro servicio. + +00:02:20.106 --> 00:02:20.926 align:middle +¿Qué hacemos? + +00:02:21.476 --> 00:02:28.906 align:middle +La respuesta: Inyección de dependencia, que es esa elegante +palabra para añadir otro argumento de construcción + +00:02:29.346 --> 00:02:36.106 align:middle +y fijarlo en una propiedad, lo que podemos +hacer de una vez con private $twigDebugCommand: + +00:02:36.976 --> 00:02:39.236 align:middle +Si nos detenemos ahora mismo y refrescamos... + +00:02:39.616 --> 00:02:40.406 align:middle +¡no te sorprendas! + +00:02:40.706 --> 00:02:41.896 align:middle +Obtendríamos un error. + +00:02:41.896 --> 00:02:45.976 align:middle +Symfony no tiene ni idea de +qué pasar para ese argumento. + +00:02:46.816 --> 00:02:49.396 align:middle +¿Y si añadimos el tipo de esta clase? + +00:02:50.046 --> 00:02:56.336 align:middle +De vuelta a nuestro terminal, podemos ver que +este servicio es una instancia de DebugCommand. + +00:02:57.116 --> 00:03:00.606 align:middle +Por aquí, vamos a añadir ese +tipo -pista: DebugCommand... + +00:03:01.276 --> 00:03:04.276 align:middle +queremos el de Symfony\Bridge\Twig\Command. + +00:03:04.916 --> 00:03:06.536 align:middle +Pulsa "tab" para autocompletarlo: + +00:03:07.176 --> 00:03:07.926 align:middle +Y luego... + +00:03:08.216 --> 00:03:10.796 align:middle +actualiza. ¡Sigue habiendo un error! + +00:03:11.316 --> 00:03:16.026 align:middle +Vale, deberíamos añadir la sugerencia +de tipo porque somos buenos programadores. + +00:03:16.526 --> 00:03:22.266 align:middle +Pero... por mucho que lo intentemos, +esto no es un servicio autocompletable. + +00:03:22.266 --> 00:03:25.036 align:middle +Entonces, ¿cómo lo arreglamos? + +00:03:25.706 --> 00:03:27.466 align:middle +Hay dos formas principales. + +00:03:27.756 --> 00:03:32.706 align:middle +Primero te mostraré la forma antigua, +que hago principalmente porque la verás + +00:03:32.706 --> 00:03:36.386 align:middle +en la documentación y en las +entradas del blog por todas partes. + +00:03:37.176 --> 00:03:41.306 align:middle +En config/services.yaml, +al igual que hicimos antes + +00:03:41.306 --> 00:03:45.886 align:middle +para el argumento $isDebug, anula +nuestro servicio por completo. + +00:03:45.886 --> 00:03:51.306 align:middle +Digamos App\Service\MixRepository, +y añadimos una clave bind. + +00:03:52.106 --> 00:03:56.906 align:middle +Entonces, vamos a insinuar lo que hay que +pasar al argume nto $twigDebugCommand. + +00:03:57.536 --> 00:04:01.266 align:middle +Lo único complicado es averiguar +qué valor hay que poner. + +00:04:01.266 --> 00:04:09.506 align:middle +Por ejemplo, si voy y copio el ID del servicio +- twig.command.debug - y lo pego aquí... + +00:04:09.976 --> 00:04:11.406 align:middle +¡eso no va a funcionar! + +00:04:11.846 --> 00:04:14.826 align:middle +Eso va a pasar literalmente por esa cadena. + +00:04:15.576 --> 00:04:17.776 align:middle +Si refrescas, ¡sí! + +00:04:18.146 --> 00:04:22.196 align:middle +El argumento 4 debe ser del +tipo DebugCommand, cadena dada. + +00:04:22.916 --> 00:04:27.136 align:middle +Tenemos que decirle a Symfony que +pase el servicio que tiene este ID. + +00:04:27.996 --> 00:04:32.606 align:middle +En estos archivos YAML, hay una sintaxis +especial para hacer precisamente eso + +00:04:33.096 --> 00:04:36.656 align:middle +prefijar el ID del servicio con el símbolo @: + +00:04:37.356 --> 00:04:38.876 align:middle +En cuanto hagamos eso... + +00:04:39.306 --> 00:04:43.456 align:middle +¡el hecho de que esto no explote +significa que está funcionando! + +00:04:44.216 --> 00:04:46.426 align:middle +Pero... vamos a eliminar esto. + +00:04:46.786 --> 00:04:49.796 align:middle +Porque quiero mostrarte la +nueva forma de hacer esto... + +00:04:50.076 --> 00:04:54.116 align:middle +que aprovecha ese mismo +atributo de lujo Autowire. + +00:04:54.956 --> 00:05:00.196 align:middle +Aquí arriba, digamos #[Autowire()], +pero en lugar de pasar sólo una cadena, + +00:05:00.356 --> 00:05:04.526 align:middle +digamos service: 'twig.command.debug': + +00:05:05.206 --> 00:05:06.536 align:middle +¡Me encanta! + +00:05:07.176 --> 00:05:10.436 align:middle +Antes de probarlo, vamos +a utilizar el servicio. + +00:05:10.806 --> 00:05:12.066 align:middle +Dirígete a findAll(). + +00:05:12.986 --> 00:05:18.556 align:middle +Ejecutar un comando de consola manualmente +en tu código PHP es totalmente posible. + +00:05:18.556 --> 00:05:20.876 align:middle +Es un poco raro, ¡pero genial! + +00:05:21.616 --> 00:05:25.676 align:middle +Tenemos que crear un objeto +$output = new BufferedOutput()... + +00:05:26.376 --> 00:05:34.076 align:middle +luego podemos ejecutar el comando diciendo +$this->twigDebugCommand->run(new ArrayInput()) - + +00:05:34.576 --> 00:05:37.476 align:middle +esto es, más o menos, fingir los +argumentos de la línea de comandos - + +00:05:37.646 --> 00:05:41.996 align:middle +pasarle un [] vacío - luego $output. + +00:05:41.996 --> 00:05:45.096 align:middle +Lo que el comando produzca +se fijará en ese objeto. + +00:05:45.776 --> 00:05:49.676 align:middle +Para ver si funciona, basta con dd($output): + +00:05:49.676 --> 00:05:50.496 align:middle +¡Tiempo de prueba! + +00:05:50.876 --> 00:05:51.436 align:middle +Refresca... + +00:05:51.506 --> 00:05:53.396 align:middle +¡y lo tienes! + +00:05:53.396 --> 00:05:54.806 align:middle +Qué divertido es esto + +00:05:55.686 --> 00:06:00.406 align:middle +Muy bien, ahora que esto funciona, +vamos a comentar esta tontería. + +00:06:01.906 --> 00:06:05.956 align:middle +Mantendré nuestro $twigDebugCommand +inyectado sólo como referencia. + +00:06:06.846 --> 00:06:13.396 align:middle +La conclusión clave es la siguiente: la mayoría de +los argumentos a los servicios serán autoconducibles. + +00:06:13.746 --> 00:06:22.076 align:middle +¡Sí! Pero cuando encuentres un argumento que no sea +autoconductible, puedes utilizar el atributo Autowire + +00:06:22.376 --> 00:06:25.406 align:middle +para apuntar al valor o servicio que necesitas. + +00:06:26.356 --> 00:06:28.666 align:middle +Lo siguiente: ¿Recuerdas cuando te dije + +00:06:28.666 --> 00:06:33.176 align:middle +que MixRepository fue el +primer servicio que creamos? + +00:06:33.716 --> 00:06:35.106 align:middle +Pues... Mentí. + +00:06:35.646 --> 00:06:40.186 align:middle +¡Resulta que nuestros controladores +han sido servicios todo este tiempo! diff --git a/sfcasts/ep2-fundamentals/es/parameters.md b/sfcasts/ep2-fundamentals/es/parameters.md new file mode 100644 index 0000000..a1d1c1a --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/parameters.md @@ -0,0 +1,83 @@ +# Parámetros + +Sabemos que existe este concepto de contenedor que contiene todos nuestros servicios... y podemos ver la lista completa de servicios ejecutando + +```terminal +php bin/console debug:container +``` + +## Listado de Parámetros + +Pues bien, resulta que el contenedor guarda otra cosa: rencores. En serio, no esperes gastar una broma al contenedor de servicios y salirte con la tuya. + +Vale, lo que realmente guarda, además de los servicios, son parámetros. Son simples valores de configuración, y podemos verlos ejecutando un comando similar: + +```terminal +php bin/console debug:container --parameters +``` + +Son básicamente variables que puedes leer y referenciar en tu código. En realidad, no tenemos que preocuparnos por la mayoría de ellos. Son establecidas por cosas internas y utilizadas por cosas internas. Pero hay algunas que empiezan por `kernel`que son bastante interesantes, como `kernel.project_dir`, que apunta al directorio de nuestro proyecto. ¡Sí! Si alguna vez necesitas una forma de referirte al directorio de tu aplicación, este parámetro puede ayudarte. + +## Obtención de parámetros de un controlador + +Entonces... ¿cómo utilizamos estos parámetros? Hay dos maneras. En primer lugar, no es muy común, pero puedes obtener un parámetro en tu controlador. Por ejemplo, en `VinylController`, vamos a `dd($this->getParameter())` -que es un método abreviado de `AbstractController` - y luego a `kernel.project_dir`. ¡Incluso obtenemos un bonito autocompletado gracias al plugin Symfony PhpStorm! Y cuando lo probamos... ¡sí! ¡Ahí está! + +## Haciendo referencia a los parámetros con %parameter% + +Ahora... borra eso. Esto funciona, pero la mayoría de las veces, la forma en que utilizarás los parámetros es haciendo referencia a ellos en tus archivos de configuración. Y esto ya lo hemos visto antes! Abre `config/packages/twig.yaml`. ¿Recuerdas ese `default_path`? Eso es hacer referencia al parámetro `kernel.project_dir`. Cuando estés en cualquiera de estos archivos de configuración de `.yaml`y quieras hacer referencia a un parámetro, puedes utilizar esta sintaxis especial: `%`, el nombre del parámetro, y luego otro `%`. + +## Crear un nuevo parámetro + +Abre `cache.yaml`. Vamos a configurar `cache.adapter` como `filesystem` para todos los entornos. Luego, lo anulamos para que sea el adaptador `array` sólo en el entorno dev. Veamos si podemos acortar esto creando un nuevo parámetro. + +¿Cómo se crean los parámetros? En cualquiera de estos archivos, añade una clave raíz llamada`parameters`. Debajo de ella, puedes simplemente... inventar un nombre. Lo llamaré `cache_adapter`, y ponle nuestro valor: `cache.adapter.filesystem`. + +Si tienes una clave raíz `framework`, Symfony pasará toda la configuración a FrameworkBundle. Lo mismo ocurre con la clave `twig` y TwigBundle. + +Pero `parameters` es especial: todo lo que esté por debajo de ella creará un parámetro. + +Así que sí... ahora tenemos un nuevo parámetro `cache.adapter`... que en realidad aún no estamos utilizando. ¡Pero ya podemos verlo! Ejecuta: + +```terminal +php bin/console debug:container --parameters +``` + +Cerca de la parte superior... ahí está - ¡ `cache_adapter`! Para utilizarlo, aquí abajo para `app`, di `%cache_adapter%`. + +Eso es todo. Nota rápida: Te habrás dado cuenta de que a veces utilizo comillas en YAML y a veces no. La mayoría de las veces, en YAML, no es necesario utilizar las comillas... pero siempre se puede. Y si alguna vez no estás seguro de si son necesarias o no, mejor estar seguro y utilizarlas. + +Los parámetros son, de hecho, un ejemplo en el que las comillas son necesarias. Si no lo rodeáramos con comillas, parecería una sintaxis especial de YAML y arrojaría un error. + +De todos modos, en el entorno `dev`, en lugar de decir `framework`, `cache`, y `app`, lo único que tenemos que hacer es anular ese parámetro. Diré `parameters`, luego`cache_adapter`... y lo pondré en `cache.adapter.array`. + +Para ver si eso funciona, gira aquí y ejecuta otro comando de ayuda: + +```terminal +php bin/console debug:config framework cache +``` + +Recuerda que `debug:config` te mostrará cuál es tu configuración actual bajo la clave`framework`, y luego la subclave `cache`. Y aquí puedes ver que `app` está configurado como `cache.adapter.array`, el valor resuelto para el parámetro. + +Comprobemos el valor en el entorno prod... sólo para asegurarnos de que también es correcto. Cuando ejecutes cualquier comando de `bin/console`, ese comando se ejecutará en el mismo entorno en el que se esté ejecutando tu aplicación. Así que cuando ejecutamos `debug:config`, eso se ejecuta en el entorno dev. + +Para ejecutar el comando en el entorno prod, podríamos ir aquí y cambiar`APP_ENV` por `prod` temporalmente... pero hay una forma más fácil. Puedes anular el entorno al ejecutar cualquier comando añadiendo una bandera al final. Por ejemplo: + +```terminal +php bin/console debug:config framework cache --env=prod +``` + +Pero antes de intentarlo, siempre tenemos que borrar nuestra caché primero para ver los cambios en el entorno `prod`. Hazlo ejecutando: + +```terminal +php bin/console cache:clear --env=prod +``` + +Ahora prueba: + +```terminal +php bin/console debug:config framework cache --env=prod +``` + +Y... ¡qué bien! Aparece `cache.adapter.filesystem`. Así pues, el contenedor también contiene parámetros. Este no es un concepto súper importante en Symfony, así que, mientras entiendas cómo funcionan, estás bien. + +Bien, volvamos a la inyección de dependencias. Sabemos que podemos autoinyectar servicios en el constructor de un servicio o en los métodos del controlador. ¿Pero qué pasa si necesitamos pasar algo que no es autoconectable? Por ejemplo, ¿qué pasa si queremos pasar uno de estos parámetros a un servicio? Averigüemos cómo funciona eso a continuación. diff --git a/sfcasts/ep2-fundamentals/es/parameters.vtt b/sfcasts/ep2-fundamentals/es/parameters.vtt new file mode 100644 index 0000000..f1a5895 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/parameters.vtt @@ -0,0 +1,335 @@ +WEBVTT + +00:00:01.016 --> 00:00:05.736 align:middle +Sabemos que existe este concepto de contenedor +que contiene todos nuestros servicios... + +00:00:06.146 --> 00:00:11.886 align:middle +y podemos ver la lista completa de servicios ejecutando: +php bin/console debug:container Pues bien, resulta + +00:00:12.186 --> 00:00:17.276 align:middle +que el contenedor guarda otra cosa: rencores. + +00:00:17.736 --> 00:00:22.636 align:middle +En serio, no esperes gastar una broma al +contenedor de servicios y salirte con la tuya. + +00:00:23.456 --> 00:00:28.546 align:middle +Vale, lo que realmente guarda, además +de los servicios, son parámetros. + +00:00:29.076 --> 00:00:34.196 align:middle +Son simples valores de configuración, y +podemos verlos ejecutando un comando similar: + +00:00:34.786 --> 00:00:36.246 align:middle +php bin/console debug:container + +00:00:36.416 --> 00:00:43.486 align:middle +--parameters Son básicamente variables que +puedes leer y referenciar en tu código. + +00:00:44.176 --> 00:00:46.656 align:middle +En realidad, no tenemos que +preocuparnos por la mayoría de ellos. + +00:00:47.086 --> 00:00:51.706 align:middle +Son establecidas por cosas internas +y utilizadas por cosas internas. + +00:00:52.366 --> 00:00:58.816 align:middle +Pero hay algunas que empiezan por kernel que son +bastante interesantes, como kernel.project_dir, + +00:00:59.346 --> 00:01:01.966 align:middle +que apunta al directorio de nuestro proyecto. + +00:01:02.606 --> 00:01:09.176 align:middle +Sí, si alguna vez necesitas una forma de referirte al +directorio de tu aplicación, este parámetro puede ayudarte. + +00:01:10.086 --> 00:01:12.486 align:middle +Entonces... ¿cómo +utilizamos estos parámetros? + +00:01:13.006 --> 00:01:14.296 align:middle +Hay dos maneras. + +00:01:14.806 --> 00:01:20.146 align:middle +En primer lugar, no es súper común, pero puedes +obtener un parámetro en tu controlador. Por + +00:01:20.876 --> 00:01:26.086 align:middle +ejemplo, en VinylController, vamos +a dd($this->getParameter()) + +00:01:26.586 --> 00:01:28.096 align:middle +-que es un método abreviado + +00:01:28.146 --> 00:01:32.616 align:middle +de AbstractController - y +luego a kernel.project_dir. + +00:01:33.416 --> 00:01:38.256 align:middle +¡Incluso obtenemos un bonito autocompletado +gracias al plugin Symfony PhpStorm! + +00:01:38.896 --> 00:01:40.226 align:middle +Y cuando lo probamos... + +00:01:40.886 --> 00:01:42.086 align:middle +¡sí! ¡Ahí está! + +00:01:42.656 --> 00:01:43.836 align:middle +Ahora... borra eso. + +00:01:44.576 --> 00:01:49.966 align:middle +Esto funciona, pero la mayoría de las veces, +la forma de utilizar los parámetros es + +00:01:49.966 --> 00:01:53.596 align:middle +haciendo referencia a ellos en +tus archivos de configuración. + +00:01:54.096 --> 00:01:55.806 align:middle +¡Y esto ya lo hemos visto antes! + +00:01:56.566 --> 00:01:59.056 align:middle +Abre config/packages/twig.yaml. + +00:01:59.706 --> 00:02:01.606 align:middle +¿Recuerdas ese default_path? + +00:02:02.156 --> 00:02:06.196 align:middle +Eso es hacer referencia al +parámetro kernel.project_dir. + +00:02:06.976 --> 00:02:13.416 align:middle +Cuando estés en cualquiera de estos archivos de configuración +de .yaml y quieras hacer referencia a un parámetro, + +00:02:13.786 --> 00:02:18.946 align:middle +puedes utilizar esta sintaxis +especial: %, el nombre del parámetro, + +00:02:19.176 --> 00:02:22.926 align:middle +y luego otro %. Abre cache.yaml. + +00:02:23.806 --> 00:02:28.366 align:middle +Estamos configurando cache.adapter como +filesystem para todos los entornos. + +00:02:29.046 --> 00:02:33.986 align:middle +Luego, lo estamos anulando para que sea el +adaptador array sólo en el entorno dev. + +00:02:34.746 --> 00:02:38.556 align:middle +Vamos a ver si podemos acortar +esto creando un nuevo parámetro. + +00:02:39.176 --> 00:02:41.176 align:middle +¿Cómo se crean los parámetros? + +00:02:41.706 --> 00:02:45.746 align:middle +En cualquiera de estos archivos, añade +una clave raíz llamada parameters. + +00:02:46.486 --> 00:02:47.926 align:middle +Debajo de ella, puedes simplemente... + +00:02:48.206 --> 00:02:49.156 align:middle +inventar un nombre. + +00:02:49.646 --> 00:02:56.656 align:middle +Lo llamaré cache_adapter, y ponle +nuestro valor: cache.adapter.filesystem. + +00:02:57.686 --> 00:03:03.096 align:middle +Si tienes una clave raíz framework, Symfony +pasará toda la configuración a FrameworkBun dle. + +00:03:03.806 --> 00:03:06.826 align:middle +Lo mismo ocurre con la clave twig y TwigBundle. + +00:03:07.656 --> 00:03:13.726 align:middle +Pero parameters es especial: todo lo que esté +por debajo de ella creará un parámetro. + +00:03:14.516 --> 00:03:18.756 align:middle +Así que sí... ahora tenemos un +nuevo parámetro cache.adapter... + +00:03:19.116 --> 00:03:21.666 align:middle +que aún no estamos utilizando. + +00:03:22.466 --> 00:03:24.846 align:middle +¡Pero ya podemos verlo! + +00:03:25.676 --> 00:03:30.706 align:middle +Ejecuta: php bin/console debug:container +--parameters Cerca de la parte superior... + +00:03:31.216 --> 00:03:33.256 align:middle +ahí está: ¡ cache_adapter! + +00:03:33.956 --> 00:03:40.616 align:middle +Para usarlo, aquí abajo +para app, di %cache_adapter%. + +00:03:41.176 --> 00:03:42.286 align:middle +Eso es todo. + +00:03:43.016 --> 00:03:50.296 align:middle +Nota rápida: Te habrás dado cuenta de que a +veces utilizo comillas en YAML y a veces no. + +00:03:50.916 --> 00:03:54.686 align:middle +La mayoría de las veces, en YAML, no +es necesario utilizar las comillas... + +00:03:55.046 --> 00:03:56.986 align:middle +pero siempre puedes hacerlo. + +00:03:57.746 --> 00:04:03.056 align:middle +Y si alguna vez no estás seguro de si son +necesarias o no, mejor estar seguro y utilizarlas. + +00:04:03.776 --> 00:04:08.236 align:middle +Los parámetros son, de hecho, un ejemplo +en el que las comillas son necesarias. + +00:04:08.866 --> 00:04:11.346 align:middle +Si no lo rodeáramos con com illas, + +00:04:11.836 --> 00:04:15.716 align:middle +parecería una sintaxis especial +de YAML y arrojaría un error. De + +00:04:16.746 --> 00:04:22.016 align:middle +todos modos, en el entorno dev, en +lugar de decir framework, cache, y app, + +00:04:22.566 --> 00:04:25.426 align:middle +lo único que tenemos que +hacer es anular ese parámetro. + +00:04:26.046 --> 00:04:29.826 align:middle +Diré parameters, luego cache_adapter... + +00:04:30.446 --> 00:04:33.416 align:middle +y lo pondré en cache.adapter.array. + +00:04:34.316 --> 00:04:38.276 align:middle +Para ver si eso funciona, gira aquí +y ejecuta otro comando de ayuda: + +00:04:38.276 --> 00:04:43.286 align:middle +php bin/console debug:config +framework cache Recuerda que + +00:04:43.616 --> 00:04:47.826 align:middle +debug:config te mostrará cuál +es tu configuración actual + +00:04:48.036 --> 00:04:51.796 align:middle +bajo la clave framework, +y luego la subclave cache. + +00:04:52.596 --> 00:04:56.756 align:middle +Y aquí puedes ver que app está +configurado como cache.adapter.array, + +00:04:57.146 --> 00:04:59.496 align:middle +el valor resuelto para el parámetro. + +00:05:00.316 --> 00:05:02.966 align:middle +Comprobemos el valor en el entorno prod... + +00:05:03.276 --> 00:05:05.886 align:middle +para asegurarnos de que +también está bien allí. + +00:05:06.756 --> 00:05:10.836 align:middle +Cuando ejecutes cualquier comando de +bin/console, ese comando se ejecutará + +00:05:10.836 --> 00:05:13.696 align:middle +en el mismo entorno en el que se +esté ejecutando tu aplicación. + +00:05:14.286 --> 00:05:18.466 align:middle +Así que cuando ejecutamos debug:config, +eso se ejecuta en el entorno dev. + +00:05:19.156 --> 00:05:23.706 align:middle +Para ejecutar el comando en el +entorno prod, podríamos ir aquí + +00:05:23.706 --> 00:05:27.356 align:middle +y cambiar APP_ENV por prod temporalmente... + +00:05:28.136 --> 00:05:30.116 align:middle +pero hay una forma más fácil. + +00:05:30.836 --> 00:05:36.366 align:middle +Puedes anular el entorno al ejecutar cualquier +comando añadiendo una bandera al final. + +00:05:37.176 --> 00:05:41.486 align:middle +Por ejemplo: php bin/console +debug:config framework cache + +00:05:41.916 --> 00:05:49.156 align:middle +--env=prod Pero antes de intentarlo, siempre +tenemos que borrar nuestra caché primero + +00:05:49.246 --> 00:05:51.366 align:middle +para ver los cambios en el entorno prod. + +00:05:52.146 --> 00:05:55.436 align:middle +Hazlo ejecutando: php bin/console cache:clear + +00:05:55.766 --> 00:06:04.996 align:middle +--env=prod Ahora prueba: php bin/console +debug:config framework cache --env=prod Y... + +00:06:05.486 --> 00:06:06.136 align:middle +¡qué bonito! + +00:06:06.696 --> 00:06:09.816 align:middle +Aparece cache.adapter.filesystem. + +00:06:10.706 --> 00:06:13.626 align:middle +Así pues, el contenedor +también contiene parámetros. + +00:06:14.186 --> 00:06:17.556 align:middle +Este no es un concepto súper +importante en Symfony, así que, + +00:06:17.556 --> 00:06:20.696 align:middle +mientras entiendas cómo +funcionan, estás bien. + +00:06:21.586 --> 00:06:24.676 align:middle +Bien, volvamos a la inyección de dependencias. + +00:06:25.416 --> 00:06:29.056 align:middle +Sabemos que podemos autoinyectar +servicios en el constructor + +00:06:29.056 --> 00:06:32.246 align:middle +de un servicio o en los +métodos del controlador. + +00:06:32.916 --> 00:06:36.846 align:middle +¿Pero qué pasa si necesitamos +pasar algo que no es autoconectable? + +00:06:37.446 --> 00:06:41.756 align:middle +Por ejemplo, ¿qué pasa si queremos pasar +uno de estos parámetros a un servicio? + +00:06:42.316 --> 00:06:44.536 align:middle +Averigüemos cómo funciona eso a continuación diff --git a/sfcasts/ep2-fundamentals/es/prod-environment.md b/sfcasts/ep2-fundamentals/es/prod-environment.md new file mode 100644 index 0000000..b914d52 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/prod-environment.md @@ -0,0 +1,53 @@ +# El entorno "prod" + +Nuestra aplicación se está ejecutando actualmente en el entorno `dev`. Cambiémosla a `prod`... que es el que usarías en producción. Cambia temporalmente `APP_ENV=dev` por`prod`... y luego dirígete a él y actualízalo. ¡Vaya! La barra de herramientas de depuración web ha desaparecido. ¡Eso... tiene sentido! Todo el bundle del perfilador web no está activado en el entorno `prod`. + +También observarás que el volcado de nuestro controlador aparece en la parte superior de la página. El perfilador web normalmente captura eso y lo muestra abajo en la barra de herramientas de depuración web. Pero... como todo ese sistema ya no está habilitado, ahora hace el volcado justo donde lo llamas. + +Y hay muchas otras diferencias, como el registrador, que ahora se comporta de forma diferente gracias a la configuración en `monolog.yaml`. + +## Borrar la caché de prod + +La forma de construir las páginas también ha cambiado. Por ejemplo, Symfony almacena en caché muchos archivos... pero eso no se nota en el entorno `dev`. Eso es porque Symfony es súper inteligente y reconstruye esa caché automáticamente cuando cambiamos ciertos archivos. Sin embargo, en el entorno `prod`, eso no ocurre. + +¡Compruébalo! Abre `templates/base.html.twig`... y cambia el título de la página a `Stirred Vinyl`. Si vuelves y actualizas... ¡mira aquí! No hay cambios! Las propias plantillas Twig se almacenan en la caché. En el entorno `dev`, Symfony reconstruye esa caché por nosotros. ¿Pero en el entorno `prod`? No Tenemos que borrarla manualmente. + +¿Cómo? En tu terminal, ejecuta: + +```terminal +php bin/console cache:clear +``` + +Fíjate que dice que está borrando la caché del entorno prod. Así, al igual que nuestra aplicación se ejecuta siempre en un entorno concreto, los comandos de la consola también se ejecutan en un entorno concreto. Y lee la misma bandera `APP_ENV`. Así que, como tenemos aquí `APP_ENV=prod`, `cache:clear` sabe que debe ejecutarse en el entorno prod y borra la caché para ese entorno. + +Gracias a esto, cuando refrescamos... ahora el título se actualiza. Volveré a cambiar esto por nuestro nombre genial, `Mixed Vinyl`. + +## Cambiar el adaptador de caché sólo para prod + +¡Vamos a probar otra cosa! Abre `config/packages/cache.yaml`. Nuestro servicio de caché utiliza actualmente el `ArrayAdapter`, que es una caché falsa. Eso puede estar bien para el desarrollo, pero no será de mucha ayuda en producción. + +Veamos si podemos volver a cambiar al adaptador del sistema de archivos, pero sólo para el entorno`prod`. ¿Cómo? Aquí abajo, usa `when@prod` y luego repite las mismas claves. Así que `framework`, `cache`, y luego `app`. Pon esto en el adaptador que queremos, que se llama `cache.adapter.filesystem`. + +Va a ser muy fácil ver si esto funciona porque seguimos volcando el servicio de caché en nuestro controlador. Ahora mismo, es un `ArrayAdapter`. Si refrescamos... ¡sorpresa! Sigue siendo un `ArrayAdapter`. ¿Por qué? Porque estamos en el entorno de producción... y prácticamente cada vez que haces un cambio en el entorno `prod`, tienes que reconstruir tu caché. + +Vuelve a tu terminal y ejecuta + +```terminal +php bin console cache:clear +``` + +de nuevo y ahora... lo tienes - ¡ `FilesystemAdapter`! + +Pero... vamos a invertir esta configuración. Copia `cache.adapter.array` y cámbialo por`filesystem`. Lo usaremos por defecto. Luego, en la parte inferior, cambia a `when@dev`, y esto a `cache.adapter.array`. + +¿Por qué hago esto? Bueno, esto literalmente no supone ninguna diferencia en los entornos `dev` y`prod`. Pero si decidimos empezar a escribir pruebas más adelante, que se ejecuten en el entorno de pruebas, con esta nueva configuración, el entorno de pruebas utilizará el mismo servicio de caché que el de producción... lo que probablemente sea más realista y mejor para las pruebas. + +Para asegurarte de que esto sigue funcionando, borra la caché una vez más. Refresca y... ¡funciona! Seguimos teniendo `FilesystemAdapter`. Y... si volvemos al entorno `dev`en `.env`... y refrescamos... ¡sí! La barra de herramientas de depuración de la web ha vuelto, y aquí abajo, ¡volvemos a utilizar `ArrayAdapter`! + +Ahora bien, en realidad, es probable que nunca cambies al entorno `prod` mientras estés desarrollando localmente. Es difícil trabajar con él... ¡y no tiene sentido! ¡El entorno `prod` está realmente pensado para producción! Y así, ejecutarás ese comando `bin/console cache:clear` durante el despliegue... pero probablemente casi nunca en tu máquina local. + +Antes de continuar, entra en `VinylController`, baja a `browse()`, y saca ese `dump()`. + +Bien, ¡comprobación de estado! Primero, todo en Symfony lo hace un servicio. Segundo, los bundles nos dan servicios. Y tercero, podemos controlar cómo se instancian esos servicios a través de las diferentes configuraciones de bundle en `config/packages/`. + +Ahora, vamos a dar un paso importante creando nuestro propio servicio. diff --git a/sfcasts/ep2-fundamentals/es/prod-environment.vtt b/sfcasts/ep2-fundamentals/es/prod-environment.vtt new file mode 100644 index 0000000..28b9fde --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/prod-environment.vtt @@ -0,0 +1,307 @@ +WEBVTT + +00:00:01.016 --> 00:00:04.186 align:middle +Nuestra aplicación se está ejecutando +actualmente en el entorno dev. + +00:00:04.816 --> 00:00:06.606 align:middle +Vamos a cambiarla a prod... + +00:00:07.036 --> 00:00:08.956 align:middle +que es el que utilizarías en producción. + +00:00:09.686 --> 00:00:13.586 align:middle +Cambia temporalmente APP_ENV=dev por prod... + +00:00:14.346 --> 00:00:16.456 align:middle +y luego dirígete a él y actualízalo. + +00:00:17.646 --> 00:00:21.016 align:middle +¡Vaya! La barra de herramientas +de depuración web ha desaparecido. + +00:00:21.476 --> 00:00:23.016 align:middle +Eso... ¡tiene sentido! + +00:00:23.516 --> 00:00:28.976 align:middle +Todo el bundle del perfilador web no +está activado en el entorno prod. + +00:00:29.776 --> 00:00:34.886 align:middle +También observarás que el volcado de nuestro +controlador aparece en la parte superior de la página. + +00:00:35.406 --> 00:00:40.786 align:middle +El perfilador web normalmente captura eso y lo muestra +abajo en la barra de herramientas de depuración web. + +00:00:41.236 --> 00:00:49.096 align:middle +Pero... como todo ese sistema ya no está habilitado, +ahora hace el volcado justo donde lo llamas. + +00:00:49.096 --> 00:00:55.266 align:middle +Y hay muchas otras diferencias, como el registrador, +que ahora se comporta de forma diferente gracias + +00:00:55.266 --> 00:00:57.916 align:middle +a la configuración en monolog.yaml. + +00:00:58.716 --> 00:01:01.866 align:middle +La forma de construir las +páginas también ha cambiado. + +00:01:02.286 --> 00:01:05.976 align:middle +Por ejemplo, Symfony almacena en +caché un montón de archivos... + +00:01:06.506 --> 00:01:09.286 align:middle +pero eso no se nota en el entorno dev. Eso es + +00:01:09.806 --> 00:01:13.486 align:middle +porque Symfony es súper +inteligente y reconstruye + +00:01:13.486 --> 00:01:17.346 align:middle +esa caché automáticamente +cuando cambiamos ciertos archivos. + +00:01:18.076 --> 00:01:22.356 align:middle +Sin embargo, en el entorno prod, eso no ocurre. + +00:01:22.856 --> 00:01:23.306 align:middle +¡Compruébalo! + +00:01:23.716 --> 00:01:26.486 align:middle +Abre templates/base.html.twig... + +00:01:27.116 --> 00:01:31.586 align:middle +y cambia el título de la +página a Stirred Vinyl. + +00:01:32.426 --> 00:01:34.676 align:middle +Si vuelves y actualizas... + +00:01:35.066 --> 00:01:35.886 align:middle +¡mira aquí arriba! + +00:01:36.156 --> 00:01:37.406 align:middle +¡No hay cambios! + +00:01:37.886 --> 00:01:40.916 align:middle +Las propias plantillas Twig +se almacenan en la caché. + +00:01:41.676 --> 00:01:45.736 align:middle +En el entorno dev, Symfony +reconstruye esa caché por nosotros. + +00:01:46.246 --> 00:01:47.886 align:middle +¿Pero en el entorno prod? + +00:01:48.356 --> 00:01:50.846 align:middle +No Tenemos que borrarla manualmente. + +00:01:51.446 --> 00:01:57.726 align:middle +¿Cómo? En tu terminal, ejecuta: php bin/console +cache:clear Fíjate que dice que está borrando + +00:01:57.726 --> 00:02:00.946 align:middle +la caché del entorno prod. + +00:02:01.716 --> 00:02:06.416 align:middle +Así que, al igual que nuestra aplicación +se ejecuta siempre en un entorno específico, + +00:02:06.816 --> 00:02:10.246 align:middle +los comandos de la consola también +se ejecutan en un entorno específico. + +00:02:10.736 --> 00:02:14.366 align:middle +Y lee la misma bandera APP_ENV. + +00:02:15.016 --> 00:02:21.416 align:middle +Así que, como tenemos aquí APP_ENV=prod, +cache:clear sabe que debe ejecutarse + +00:02:21.416 --> 00:02:25.446 align:middle +en el entorno prod y borra +la caché para ese entorno. + +00:02:26.106 --> 00:02:28.296 align:middle +Gracias a esto, cuando refrescamos... + +00:02:28.576 --> 00:02:30.366 align:middle +ahora el título se actualiza. + +00:02:31.166 --> 00:02:34.856 align:middle +Volveré a cambiar esto por +nuestro nombre genial, Mixed Vinyl. + +00:02:35.756 --> 00:02:36.806 align:middle +¡Vamos a probar otra cosa! + +00:02:37.406 --> 00:02:39.926 align:middle +Abre config/packages/cache.yaml. + +00:02:41.006 --> 00:02:46.346 align:middle +Nuestro servicio de caché utiliza actualmente +el ArrayAdapter, que es un caché falso. + +00:02:46.976 --> 00:02:52.416 align:middle +Eso puede estar bien para el desarrollo, +pero no será de mucha ayuda en producción. + +00:02:53.106 --> 00:02:57.056 align:middle +Veamos si podemos volver a cambiar +al adaptador del sistema de archivos, + +00:02:57.516 --> 00:03:00.216 align:middle +pero sólo para el entorno prod. + +00:03:00.916 --> 00:03:07.716 align:middle +¿Cómo? Aquí abajo, utiliza when@prod +y luego repite las mismas claves. + +00:03:08.316 --> 00:03:11.376 align:middle +Así que framework, cache, y luego app. + +00:03:11.906 --> 00:03:17.656 align:middle +Pon esto en el adaptador que queremos, +que se llama cache.adapter.filesystem. Va + +00:03:18.446 --> 00:03:21.306 align:middle +a ser muy fácil ver si esto funciona + +00:03:21.576 --> 00:03:24.486 align:middle +porque seguimos volcando el servicio +de caché en nuestro controlador. + +00:03:25.356 --> 00:03:27.356 align:middle +Ahora mismo, es un ArrayAdapter. + +00:03:27.906 --> 00:03:29.196 align:middle +Si refrescamos... + +00:03:29.716 --> 00:03:32.616 align:middle +¡sorpresa! Sigue siendo un ArrayAdapter. + +00:03:33.246 --> 00:03:36.766 align:middle +¿Por qué? Porque estamos +en el entorno prod... + +00:03:37.216 --> 00:03:39.636 align:middle +y prácticamente cada vez que +haces un cambio en el entorno + +00:03:39.636 --> 00:03:43.236 align:middle +prod , tienes que reconstruir tu caché. + +00:03:43.946 --> 00:03:49.416 align:middle +Vuelve a tu terminal y ejecuta de nuevo +php bin console cache:clear y ahora... + +00:03:50.816 --> 00:03:53.016 align:middle +lo tienes - ¡ FilesystemAdapter! + +00:03:53.776 --> 00:03:56.556 align:middle +Pero... vamos a invertir esta configuración. + +00:03:57.326 --> 00:04:01.646 align:middle +Copia cache.adapter.array +y cámbialo por filesystem. + +00:04:02.076 --> 00:04:04.266 align:middle +Lo usaremos por defecto. + +00:04:04.846 --> 00:04:12.396 align:middle +Luego, en la parte inferior, cambia a +when@dev, y esto a cache.adapter.array. + +00:04:13.076 --> 00:04:15.066 align:middle +¿Por qué hago esto? + +00:04:15.716 --> 00:04:20.876 align:middle +Bueno, esto literalmente no supone ninguna +diferencia en los entornos dev y prod. + +00:04:21.346 --> 00:04:26.996 align:middle +Pero si decidimos empezar a escribir pruebas más +adelante, que se ejecuten en el entorno de pruebas, + +00:04:27.516 --> 00:04:33.586 align:middle +con esta nueva configuración, el entorno de pruebas +utilizará el mismo servicio de caché que el de producción... + +00:04:34.176 --> 00:04:37.516 align:middle +lo que probablemente sea más +realista y mejor para las pruebas. + +00:04:38.386 --> 00:04:42.206 align:middle +Para asegurarte de que esto sigue +funcionando, borra la caché una vez más. + +00:04:43.706 --> 00:04:45.026 align:middle +Refresca y... + +00:04:45.496 --> 00:04:49.256 align:middle +¡funciona! Todavía tenemos FilesystemAdapter. + +00:04:50.126 --> 00:04:54.116 align:middle +Y... si volvemos al entorno dev en .env... + +00:04:54.666 --> 00:04:55.906 align:middle +y refrescamos... + +00:04:56.396 --> 00:05:04.556 align:middle +¡sí! La barra de herramientas de depuración de la web ha +vuelto, y aquí abajo, ¡volvemos a utilizar ArrayAdapter ! + +00:05:05.446 --> 00:05:08.966 align:middle +Ahora bien, en realidad, es +probable que nunca cambies + +00:05:08.966 --> 00:05:12.386 align:middle +al entorno prod mientras estés +desarrollando localmente. + +00:05:12.946 --> 00:05:14.346 align:middle +Es difícil trabajar con él... + +00:05:14.876 --> 00:05:17.126 align:middle +¡y no tiene sentido! + +00:05:17.716 --> 00:05:21.156 align:middle +¡El entorno prod está realmente +pensado para la producción! + +00:05:21.676 --> 00:05:27.726 align:middle +Y así, ejecutarás ese comando bin/console +cache:clear durante el despliegue... + +00:05:28.136 --> 00:05:31.326 align:middle +pero probablemente casi +nunca en tu máquina local. + +00:05:32.166 --> 00:05:38.706 align:middle +Antes de continuar, entra en VinylController, +baja a browse(), y saca ese dump(). + +00:05:39.616 --> 00:05:41.086 align:middle +Bien, ¡comprobación de estado! + +00:05:41.646 --> 00:05:45.016 align:middle +Primero, todo en Symfony lo hace un servicio. + +00:05:45.446 --> 00:05:48.296 align:middle +Segundo, los bundles nos dan servicios. + +00:05:48.746 --> 00:05:55.196 align:middle +Y tercero, podemos controlar cómo se instancian +esos servicios a través de las diferentes + +00:05:55.196 --> 00:05:58.206 align:middle +configuraciones de bundle en config/packages/. + +00:05:58.916 --> 00:06:05.106 align:middle +Ahora, vamos a dar un paso importante +creando nuestro propio servicio diff --git a/sfcasts/ep2-fundamentals/es/secrets-usage.md b/sfcasts/ep2-fundamentals/es/secrets-usage.md new file mode 100644 index 0000000..d0bdeba --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/secrets-usage.md @@ -0,0 +1,87 @@ +# Lectura de secretos frente a variables de entorno + +Acabamos de crear una bóveda de secretos para nuestro entorno `dev`... que contendrá una versión "segura" por defecto de cualquier variable de entorno sensible. Por ejemplo, hemos establecido el valor `GITHUB_TOKEN` como `CHANGEME`. + +Ahora vamos a crear la bóveda del entorno `prod`. Hazlo diciendo: + +```terminal +./bin/console secrets:set GITHUB_TOKEN --env=prod +``` + +Esta vez, coge el valor secreto real de `.env.local` y pégalo aquí. Al igual que antes, como no había ya una bóveda de `prod`, Symfony la creó. Y tiene los mismos cuatro archivos que antes. Aunque, hay una sutil, pero importante diferencia. + +Añade ese nuevo directorio a git: + +```terminal-silent +git add config/secrets/prod +``` + +Y luego ejecuta: + +```terminal +git status +``` + +¡Woh! Sólo se han añadido tres de los cuatro archivos. El cuarto archivo -la clave `decrypt`- es ignorado por Git. Ya tenemos una línea dentro en `.gitignore` para eso. No queremos confirmar la clave de descifrado de `prod` en el repositorio... porque cualquiera que tenga esta clave podrá leer todos nuestros secretos. + +Así, si otro desarrollador baja el proyecto ahora, tendrá la clave de descifrado de `dev`, por lo que no tendrá problemas para leer los valores de la bóveda de `dev`. No tendrá la clave de descifrado de `prod`... ¡pero no pasa nada! ¡El único lugar donde necesitas la clave de descifrado `prod` es en producción! + +Así que con esta configuración, cuando despliegues, en lugar de tener que crear un archivo`.env.local` entero que contenga todos tus secretos, sólo tendrás que preocuparte de introducir este único archivo `prod.decrypt.private.php` en tu código. O, alternativamente, puedes leer esta clave y establecerla en una variable de entorno: puedes consultar la documentación para saber cómo hacerlo. + +## Utilizar la Bóveda de Secretos + +Pero... espera un segundo. ¡En realidad no he explicado cómo se utiliza la bóveda! Sabemos que el entorno `dev` utilizará la bóveda `dev`... y `prod` utilizará`prod`... pero ¿cómo leemos los secretos de la bóveda? + +La respuesta es... ¡ya lo estamos haciendo! Los secretos se convierten en variables de entorno. ¡Es tan sencillo como eso! Así que en `config/packages/framework.yaml`, utilizando esta sintaxis `env`, este `GITHUB_TOKEN` podría ser una variable de entorno real, o podría ser un secreto en nuestra bóveda. + +Para ver si esto funciona, dirígete a `MixRepository` y`dd($this->githubContentClient)`: + +[[[ code('53828c9ece') ]]] + +Muévete, refresca y... veamos si podemos encontrar la cabecera de Autorización en esto. En realidad, hay un truco muy bueno con el volcado. Haz clic en esta zona y mantén pulsado "comando" o "control" + "F" para buscar dentro de ella. Busca la palabra "token" y... ¡oh, eso no está bien! Ese es nuestro verdadero token. Pero... puesto que estamos en el entorno dev, ¿no debería estar leyendo nuestra bóveda dev en la que establecimos el valor falso de `CHANGEME`? ¿Qué está pasando? + +## Los secretos deben convertirse completamente en variables de entorno + +Como he mencionado, los secretos se convierten en variables de entorno. Pero las variables de entorno tienen prioridad sobre los secretos: incluso las variables de entorno definidas en los archivos `.env`. Sí, porque tenemos una variable de entorno `GITHUB_TOKEN` definida en `.env` y`.env.local`, ¡que tiene prioridad sobre el valor de la bóveda! + +Esta es la cuestión. En cuanto elijas convertir un valor de una variable de entorno en un secreto, tienes que dejar de establecerlo como variable de entorno por completo. En otras palabras, borra `GITHUB_TOKEN` en `.env` y también en `.env.local`. + +Ve a actualizar, haz clic de nuevo en esto, usa "comando" + "F", busca "token", y... ¡lo tienes! ¡Vemos "CHANGEME"! Si estuviéramos en el entorno `prod`, leería el valor de la bóveda prod... suponiendo que la clave de desencriptación prod estuviera disponible. + +## El comando secrets:list + +Vale, quita ese `dd()` y actualiza para descubrir que... ¡localmente, todo está roto! ¡Maldita sea! Pero... ¡por supuesto! Ahora está usando ese token falso de la bóveda dev. Funciona bien en producción... pero ¿cómo puedo arreglar mi configuración local para seguir trabajando? + +Podríamos anular temporalmente el valor secreto de `GITHUB_TOKEN` en la bóveda de `dev` ejecutando el comando `secrets:set`. Pero... ¡eso es una tontería! Tendríamos que ser muy cuidadosos para no confirmar el archivo modificado y encriptado. + +Antes de arreglar esto, quiero mostrarte un comando muy útil para la bóveda: + +```terminal +php bin/console secrets:list +``` + +Sí, esto te muestra todos los secretos de nuestra bóveda. ¡Es genial! E incluso puedes pasar `--reveal` para revelar el valor... siempre que tengas la clave `decrypt`. + +Te habrás dado cuenta de que nos da el valor justo aquí... pero luego dice "Valor local" con un espacio en blanco. Hmm... + +Vuelve a ejecutar el comando, pero esta vez añade `--env=prod`. + +```terminal-silent +php bin/console secrets:list --reveal --env=prod +``` + +Y... ¡lo mismo! Esto nos muestra el valor real de `prod`... pero sigue habiendo este punto de "Valor local" sin nada. + +Este "Valor Local" es la clave para arreglar nuestra configuración rota de dev: es una forma de anular un secreto, pero sólo localmente en nuestra única máquina. + +¿Cómo se establece este valor local de anulación? Copia el valor real de `GITHUB_TOKEN`, luego muévete, busca `.env.local` -el mismo archivo en el que hemos estado trabajando- y di`GITHUB_TOKEN=` y pega el valor que acabamos de copiar. + +¡Sí! ¡Localmente, vamos a aprovechar que las variables de entorno "ganan" a los secretos! De vuelta a tu terminal, ejecuta + +```terminal +php bin/console secrets:list --reveal +``` + +de nuevo. ¡Sí! El valor oficial en la bóveda es "CHANGEME"... pero el valor local es nuestro token real que, como sabemos, anulará el secreto y se utilizará. Si volvemos a probar la página... ¡funciona! + +¡Bien, equipo! Estamos... bueno... ¡básicamente hechos! Así que, como recompensa por vuestro duro trabajo en estos temas tan importantes, vamos a celebrarlo utilizando la biblioteca generadora de código de Symfony MakerBundle. diff --git a/sfcasts/ep2-fundamentals/es/secrets-usage.vtt b/sfcasts/ep2-fundamentals/es/secrets-usage.vtt new file mode 100644 index 0000000..4754dd3 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/secrets-usage.vtt @@ -0,0 +1,363 @@ +WEBVTT + +00:00:01.016 --> 00:00:04.876 align:middle +Acabamos de crear una bóveda de +secretos para nuestro entorno dev... + +00:00:05.316 --> 00:00:09.886 align:middle +que contendrá una versión "segura" por defecto +de cualquier variable de entorno sensible. + +00:00:10.436 --> 00:00:14.646 align:middle +Por ejemplo, hemos establecido el +valor GITHUB_TOKEN como CHANGEME. + +00:00:15.316 --> 00:00:18.096 align:middle +Ahora vamos a crear la +bóveda del entorno prod. + +00:00:18.696 --> 00:00:19.546 align:middle +Hazlo diciendo: ./bin/console + +00:00:19.716 --> 00:00:28.036 align:middle +secrets:set GITHUB_TOKEN --env=prod +Esta vez, coge el valor secreto real de + +00:00:28.036 --> 00:00:31.116 align:middle +.env.local y pégalo aquí. + +00:00:31.876 --> 00:00:37.276 align:middle +Igual que antes, como no había ya una +bóveda de prod, Symfony la ha creado. Y + +00:00:37.756 --> 00:00:40.516 align:middle +tiene los mismos cuatro +archivos que antes. Aunque, + +00:00:40.906 --> 00:00:44.726 align:middle +hay una sutil, pero +importante diferencia. Añade + +00:00:45.506 --> 00:00:50.806 align:middle +ese nuevo directorio a git: Y luego +ejecuta: git status ¡Woh! Sólo se + +00:00:51.236 --> 00:00:54.496 align:middle +han añadido tres de los cuatro archivos. El + +00:00:55.086 --> 00:01:00.066 align:middle +cuarto archivo -la clave +decrypt - es ignorado por Git. + +00:01:00.646 --> 00:01:03.776 align:middle +Ya tenemos una línea dentro +en .gitignore para ello. + +00:01:04.316 --> 00:01:08.486 align:middle +No queremos confirmar la clave de descifrado +de prod en el repositorio... porque + +00:01:08.916 --> 00:01:13.556 align:middle +cualquiera que tenga esta clave podrá +leer todos nuestros secretos. Así, + +00:01:14.146 --> 00:01:19.796 align:middle +si otro desarrollador baja el proyecto ahora, +tendrá la clave de descifrado de dev, por lo que + +00:01:20.126 --> 00:01:23.676 align:middle +no tendrá problemas para leer los +valores de la bóveda de dev. No tendrán + +00:01:24.546 --> 00:01:26.946 align:middle +la clave de descifrado de prod... ¡pero + +00:01:27.186 --> 00:01:28.416 align:middle +no pasa nada! ¡ El + +00:01:28.916 --> 00:01:33.486 align:middle +único lugar donde necesitas la clave de +descifrado prod es en producción! Así que + +00:01:33.956 --> 00:01:37.596 align:middle +con esta configuración, cuando +despliegues, en lugar de tener que + +00:01:37.596 --> 00:01:43.916 align:middle +crear un archivo .env.local entero que contenga +todos tus secretos, sólo tendrás que preocuparte de + +00:01:43.916 --> 00:01:49.886 align:middle +introducir este único archivo +prod.decrypt.private.php en tu código. O, + +00:01:50.466 --> 00:01:55.976 align:middle +como alternativa, puedes leer esta clave +y establecerla en una variable de entorno: + +00:01:56.386 --> 00:01:58.516 align:middle +puedes consultar la documentación +para saber cómo hacerlo. Pero... + +00:01:59.356 --> 00:02:00.586 align:middle +espera un segundo. ¡ + +00:02:00.906 --> 00:02:04.436 align:middle +En realidad no he explicado +cómo se utiliza la bóveda! + +00:02:05.146 --> 00:02:08.556 align:middle +Sabemos que el entorno dev +utilizará la bóveda dev... y + +00:02:08.736 --> 00:02:10.736 align:middle +prod utilizará prod... pero + +00:02:11.246 --> 00:02:14.236 align:middle +¿cómo leemos los secretos de la bóveda? La + +00:02:15.046 --> 00:02:16.286 align:middle +respuesta es... + +00:02:16.576 --> 00:02:18.586 align:middle +¡ya lo estamos haciendo! Los secretos + +00:02:19.046 --> 00:02:22.186 align:middle +se convierten en variables de entorno. ¡ Es + +00:02:22.516 --> 00:02:23.686 align:middle +tan sencillo como eso! Así que + +00:02:24.416 --> 00:02:29.666 align:middle +en config/packages/framework.yaml, +utilizando esta sintaxis env, este + +00:02:29.886 --> 00:02:36.366 align:middle +GITHUB_TOKEN podría ser una variable de entorno real, +o podría ser un secreto en nuestra bóveda. Para + +00:02:37.216 --> 00:02:43.466 align:middle +ver si esto funciona, dirígete a MixRepository +y dd($this->githubContentClient): Muévete + +00:02:44.076 --> 00:02:46.596 align:middle +, refresca y... + +00:02:47.226 --> 00:02:50.096 align:middle +veamos si podemos encontrar la cabecera +de Autorización en esto. En realidad, hay + +00:02:50.576 --> 00:02:53.166 align:middle +un truco muy bueno con el volcado. Haz clic + +00:02:53.836 --> 00:02:59.196 align:middle +en esta zona y mantén pulsado "comando" o +"control" + "F" para buscar dentro de ella. + +00:02:59.776 --> 00:03:01.816 align:middle +Busca la palabra "token" y... ¡oh, + +00:03:02.416 --> 00:03:03.906 align:middle +eso no está bien! Esa es + +00:03:04.286 --> 00:03:05.856 align:middle +nuestra ficha real. Pero... + +00:03:06.476 --> 00:03:12.456 align:middle +ya que estamos en el entorno dev, ¿no debería +estar leyendo nuestra bóveda dev en la que + +00:03:12.546 --> 00:03:15.346 align:middle +establecimos el valor falso de +CHANGEME? ¿Qué está pasando + +00:03:15.846 --> 00:03:16.596 align:middle +? Como + +00:03:17.476 --> 00:03:20.726 align:middle +ya he dicho, los secretos se convierten en +variables de entorno. Pero las variables de entorno + +00:03:21.086 --> 00:03:25.916 align:middle +tienen prioridad sobre los secretos: incluso + +00:03:26.556 --> 00:03:30.416 align:middle +las variables de entorno definidas +en los archivos .env. Sí, + +00:03:31.116 --> 00:03:37.176 align:middle +porque tenemos una variable de entorno +GITHUB_TOKEN definida en .env y .env.local, ¡que + +00:03:37.606 --> 00:03:41.446 align:middle +tiene prioridad sobre el valor +de la bóveda! Esta es la + +00:03:42.246 --> 00:03:43.136 align:middle +cuestión. En + +00:03:43.546 --> 00:03:49.006 align:middle +cuanto elijas convertir un valor de una +variable de entorno en un secreto, tienes + +00:03:49.486 --> 00:03:53.886 align:middle +que dejar de establecerlo como +variable de entorno por completo. En + +00:03:54.446 --> 00:03:59.916 align:middle +otras palabras, borra GITHUB_TOKEN en +.env y también en .env.local. Ve a + +00:04:00.686 --> 00:04:06.196 align:middle +actualizar, haz clic de nuevo en esto, +usa "comando" + "F", busca "token", y... + +00:04:06.586 --> 00:04:09.206 align:middle +¡lo tienes! ¡Vemos "CHANGEME"! Si + +00:04:09.946 --> 00:04:14.526 align:middle +estuviéramos en el entorno prod, leería el +valor de la bóveda prod... suponiendo que + +00:04:14.836 --> 00:04:17.656 align:middle +la clave de descifrado prod +estuviera disponible. Vale, + +00:04:18.486 --> 00:04:22.526 align:middle +quita ese dd() y actualiza para +descubrir que... ¡localmente, + +00:04:23.076 --> 00:04:25.656 align:middle +todo está roto! ¡Maldita sea! + +00:04:25.876 --> 00:04:27.306 align:middle +Pero ... ¡por + +00:04:27.526 --> 00:04:28.246 align:middle +supuesto! + +00:04:28.696 --> 00:04:32.316 align:middle +Ahora está usando ese token +falso de la bóveda dev. + +00:04:33.146 --> 00:04:35.286 align:middle +Funciona bien en producción... pero + +00:04:35.616 --> 00:04:39.356 align:middle +¿cómo puedo arreglar mi configuración +local para seguir trabajando? + +00:04:40.116 --> 00:04:45.516 align:middle +Podríamos anular temporalmente el valor +secreto de GITHUB_TOKEN en la bóveda de dev + +00:04:46.016 --> 00:04:48.286 align:middle +ejecutando el comando secrets:set. Pero... + +00:04:49.006 --> 00:04:50.876 align:middle +¡eso es una tontería! Tendríamos + +00:04:51.476 --> 00:04:56.356 align:middle +que ser muy cuidadosos para no confirmar el +archivo modificado y encriptado. Antes de + +00:04:57.146 --> 00:05:01.436 align:middle +arreglar esto, quiero mostrarte un +comando muy útil para la bóveda: php + +00:05:01.436 --> 00:05:08.276 align:middle +bin/console secrets:list Sí, esto te muestra +todos los secretos de nuestra bóveda. ¡ Es + +00:05:08.576 --> 00:05:09.186 align:middle +genial! E + +00:05:09.946 --> 00:05:14.486 align:middle +incluso puedes pasar --reveal +para revelar el valor... + +00:05:14.876 --> 00:05:16.886 align:middle +siempre que tengas la clave decrypt. Te + +00:05:17.736 --> 00:05:21.056 align:middle +habrás dado cuenta de que nos +da el valor justo aquí... pero + +00:05:21.506 --> 00:05:25.096 align:middle +luego dice "Valor local" con +un espacio en blanco. Hmm... + +00:05:25.646 --> 00:05:30.466 align:middle +Vuelve a ejecutar el comando, pero +esta vez añade --env=prod. Y... + +00:05:31.006 --> 00:05:32.586 align:middle +¡lo mismo! Esto + +00:05:32.876 --> 00:05:35.146 align:middle +nos muestra el valor real de prod ... pero + +00:05:35.506 --> 00:05:38.786 align:middle +sigue habiendo este punto de "Valor +local" sin nada. Este "Valor Local + +00:05:39.536 --> 00:05:43.936 align:middle +" es la clave para arreglar nuestra +configuración rota de dev: es + +00:05:44.316 --> 00:05:49.446 align:middle +una forma de anular un secreto, pero sólo +localmente en nuestra única máquina. ¿Cómo se + +00:05:50.206 --> 00:05:53.176 align:middle +establece este valor local de anulación? Copia + +00:05:53.856 --> 00:05:59.046 align:middle +el valor real de GITHUB_TOKEN, +luego muévete, busca .env.local -el + +00:05:59.546 --> 00:06:03.886 align:middle +mismo archivo en el que hemos estado +trabajando- y di GITHUB_TOKEN= y + +00:06:04.046 --> 00:06:06.756 align:middle +pega el valor que acabamos de copiar. ¡Sí! + +00:06:07.486 --> 00:06:12.046 align:middle +Localmente, ¡vamos a aprovechar que + +00:06:12.076 --> 00:06:15.356 align:middle +las variables de entorno "ganan" +a los secretos! De vuelta + +00:06:16.106 --> 00:06:21.296 align:middle +a tu terminal, ejecuta de nuevo php +bin/console secrets:list --reveal . ¡Sí! + +00:06:22.216 --> 00:06:25.786 align:middle +El valor oficial en la +bóveda es "CHANGEME"... pero + +00:06:26.236 --> 00:06:33.596 align:middle +el valor local es nuestro token real que, como +sabemos, anulará el secreto y se utilizará. Si + +00:06:34.486 --> 00:06:35.956 align:middle +volvemos a probar la página... + +00:06:36.286 --> 00:06:38.986 align:middle +¡funciona! ¡Bien, equipo! Ya estamos... + +00:06:39.176 --> 00:06:40.186 align:middle +bueno... ¡básicamente + +00:06:40.426 --> 00:06:41.646 align:middle +hecho! Así que , como + +00:06:42.046 --> 00:06:46.816 align:middle +recompensa por vuestro duro trabajo en +estos temas tan importantes, vamos a + +00:06:47.276 --> 00:06:52.466 align:middle +celebrarlo utilizando la biblioteca +generadora de código de Symfony MakerBundle diff --git a/sfcasts/ep2-fundamentals/es/secrets-vault.md b/sfcasts/ep2-fundamentals/es/secrets-vault.md new file mode 100644 index 0000000..fcbda6a --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/secrets-vault.md @@ -0,0 +1,81 @@ +# La Bóveda de los Secretos + +No quiero adentrarme demasiado en el despliegue, pero vamos a hacer un curso rápido de "Cómo desplegar tu aplicación Symfony 101". Esta es la idea. + +## Despliegue 101 + +Paso 1: Tienes que llevar de alguna manera todo tu código comprometido a tu máquina de producción y luego ejecutar + +```terminal +composer install +``` + +para rellenar el directorio `vendor/`. + +Paso 2: crea de algún modo un archivo `.env.local` con todas tus variables de entorno de producción, que incluirá `APP_ENV=prod`, para que estés en el entorno de producción. + +Y Paso 3: ejecuta + +```terminal +php bin/console cache:clear +``` + +que borrará la caché en el entorno de producción, y luego + +```terminal +php bin/console cache:warmup +``` + +para "calentar" la caché. Puede haber algunos otros comandos, como ejecutar las migraciones de tu base de datos... pero esta es la idea general. Y los documentos de Symfony tienen más detalles. + +Por cierto, en caso de que te lo preguntes, desplegamos a través de https://platform.sh, utilizando la integración en la nube de Symfony... que se encarga de muchas cosas por nosotros. Puedes comprobarlo entrando en https://symfony.com/cloud. También ayuda a apoyar el proyecto Symfony, así que todos salimos ganando. + +## Utiliza variables de entorno reales cuando sea posible + +De todos modos, la parte más complicada del proceso es el paso 2: crear el archivo `.env.local`con todos tus valores de producción, que incluirán cosas como las claves de la API, los detalles de la conexión a la base de datos y más. + +Ahora bien, si tu plataforma de alojamiento te permite almacenar variables de entorno reales directamente dentro de ella, ¡problema resuelto! Si estableces variables de entorno reales, entonces no hay necesidad de gestionar un archivo `.env.local` en absoluto. En cuanto despliegues, Symfony verá y utilizará instantáneamente las variables de entorno reales. Eso es lo que hacemos para Symfonycasts. + +## ¿Crear el .env.local durante el despliegue? + +Pero si eso no es una opción para ti, tendrás que dar de alguna manera a tu sistema de despliegue acceso a tus valores sensibles para que pueda crear el archivo`.env.local`. Pero... como no vamos a consignar ninguno de estos valores en nuestro repositorio, ¿dónde debemos almacenarlos? + +Una opción para manejar los valores sensibles es la bóveda de secretos de Symfony. Es un conjunto de archivos que contienen variables de entorno de forma encriptada. Estos archivos son seguros para enviarlos a tu repositorio... ¡porque están encriptados! + +## Creando la Bóveda dev + +Si quieres almacenar secretos en una bóveda, necesitarás dos: una para el entorno`dev` y otra para el entorno `prod`. Primero vamos a crear estas dos bóvedas... y luego te explicaré cómo leer valores de ellas. + +Empieza creando una para el entorno `dev`. Ejecuta: + +```terminal +php bin/console secrets:set +``` + +Pasa este `GITHUB_TOKEN`, que es el secreto que queremos establecer. A continuación, nos pide nuestro "valor secreto". Como se trata de la bóveda del entorno `dev`, queremos poner algo que sea seguro para que todos lo vean. En un momento explicaré por qué. Diré `CHANGEME`. No puedes verme escribir eso... sólo porque Symfony lo oculta por razones de seguridad. + +Como este es el primer secreto que hemos creado, Symfony creó automáticamente la bóveda de secretos entre bastidores... que es literalmente un conjunto de archivos que viven en `config/secrets/dev/`. Para la bóveda dev, vamos a confirmar todos estos archivos en el repositorio. Vamos a hacerlo. Añade todo el directorio de secretos: + +```terminal-silent +git add config/secrets/dev +``` + +Luego haz el commit con: + +```terminal +git commit -m "adding dev secrets vault" +``` + +## Los archivos de la bóveda de secretos + +He aquí una explicación rápida de los archivos. `dev.list.php` almacena una lista de los valores que viven dentro de la bóveda, `dev.GITHUB_TOKEN.28bd2f.php` almacena el valor cifrado real, y `dev.encrypt.public.php` es la clave criptográfica que permite a los desarrolladores de tu equipo añadir más secretos. De modo que si otro desarrollador se baja del proyecto, tendrá este archivo... para poder añadir más secretos. Por último, `dev.decrypt.private.php` es la clave secreta que nos permite desencriptar y leer los valores de la bóveda. + +En cuanto los archivos de la bóveda estén presentes, Symfony los abrirá automáticamente, descifrará los secretos y los expondrá como variables de entorno Pero, más sobre esto en unos minutos. + +## ¿Acervar la clave de desencriptación dev? + +Pero espera: ¿realmente acabamos de confirmar la clave `decrypt` en el repositorio? Sí, ¡eso normalmente sería un no-no! ¿Por qué te tomarías la molestia de encriptar valores... sólo para almacenar la clave de desencriptación junto a ellos? + +La razón por la que hacemos esto es que se trata de nuestra bóveda de desarrollo, lo que significa que sólo vamos a almacenar valores que sean seguros para que los vean todos los desarrolladores. La bóveda de `dev` sólo se utilizará para el desarrollo local... y queremos que nuestros compañeros de equipo puedan bajar el código y leerlo sin problemas. + +Bien, en este punto tenemos una bóveda `dev` que Symfony utilizará automáticamente en el entorno `dev`. Siguiente: vamos a crear la bóveda prod, que contendrá los valores verdaderamente secretos. Luego aprenderemos la relación entre los secretos de la bóveda y las variables de entorno... así como una forma fácil de visualizar todo esto. diff --git a/sfcasts/ep2-fundamentals/es/secrets-vault.vtt b/sfcasts/ep2-fundamentals/es/secrets-vault.vtt new file mode 100644 index 0000000..baad3b7 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/secrets-vault.vtt @@ -0,0 +1,295 @@ +WEBVTT + +00:00:00.976 --> 00:00:05.086 align:middle +No quiero adentrarme demasiado en el despliegue, +pero vamos a hacer un curso rápido de "Cómo + +00:00:05.086 --> 00:00:08.026 align:middle +desplegar tu aplicación Symfony 101". + +00:00:08.646 --> 00:00:09.616 align:middle +La idea es la siguiente. Paso 1: + +00:00:10.216 --> 00:00:15.676 align:middle +Tienes que llevar de alguna manera todo tu +código comprometido a tu máquina de producción + +00:00:15.936 --> 00:00:20.016 align:middle +y luego ejecutar composer install para +poblar el directorio vendor/. Paso 2: + +00:00:20.676 --> 00:00:28.686 align:middle +crea de algún modo un archivo .env.local con +todas tus variables de entorno de producción, + +00:00:29.206 --> 00:00:34.046 align:middle +que incluirá APP_ENV=prod, para que +estés en el entorno de producción. + +00:00:34.716 --> 00:00:39.926 align:middle +Y Paso 3: ejecuta php bin/console +cache:clear que limpiará la + +00:00:39.926 --> 00:00:45.616 align:middle +caché en el entorno de producción, y luego php +bin/console cache:warmup para "calentar" la caché. + +00:00:46.416 --> 00:00:50.306 align:middle +Puede haber algunos otros comandos, como +ejecutar las migraciones de tu base de datos... + +00:00:50.716 --> 00:00:52.596 align:middle +pero esta es la idea general. + +00:00:52.596 --> 00:00:55.216 align:middle +Y los documentos de Symfony +tienen más detalles. Por + +00:00:55.916 --> 00:01:00.566 align:middle +cierto, en caso de que te lo preguntes, +desplegamos a través de https://platform.sh, + +00:01:00.846 --> 00:01:02.986 align:middle +utilizando la integración +en la nube de Sym fony... + +00:01:03.346 --> 00:01:05.896 align:middle +que se encarga de muchas cosas por nosotros. + +00:01:06.286 --> 00:01:09.426 align:middle +Puedes comprobarlo entrando +en https://symfony.com/cloud. + +00:01:10.086 --> 00:01:13.656 align:middle +También ayuda a apoyar el proyecto +Symfony, así que todos salimos ganando. De + +00:01:14.476 --> 00:01:21.896 align:middle +todos modos, la parte más complicada del proceso +es el paso 2: crear el archivo .env.local con todos + +00:01:21.896 --> 00:01:26.396 align:middle +tus valores de producción, que incluirá +cosas como las claves de la API, + +00:01:26.706 --> 00:01:29.006 align:middle +los detalles de la conexión +a la base de datos y más. + +00:01:29.776 --> 00:01:32.576 align:middle +Ahora bien, si tu plataforma +de alojamiento te permite + +00:01:32.576 --> 00:01:38.066 align:middle +almacenar variables de entorno reales +directamente dentro de ella, ¡problema resuelto! + +00:01:38.686 --> 00:01:46.046 align:middle +Si estableces variables de entorno reales, entonces no hay +necesidad de gestionar un archivo .env.local en absoluto. + +00:01:46.936 --> 00:01:52.856 align:middle +En cuanto despliegues, Symfony verá y utilizará +instantáneamente las variables de entorno reales. + +00:01:53.276 --> 00:01:55.546 align:middle +Eso es lo que hacemos para Symfonycasts. + +00:01:56.206 --> 00:02:02.746 align:middle +Pero si eso no es una opción para ti, tendrás que +dar de alguna manera a tu sistema de despliegue acceso + +00:02:02.806 --> 00:02:08.316 align:middle +a tus valores sensibles para que +pueda crear el archivo .env.local. + +00:02:09.136 --> 00:02:12.616 align:middle +Pero... puesto que no vamos a +consignar ninguno de estos valores + +00:02:12.616 --> 00:02:15.886 align:middle +en nuestro repositorio, +¿dónde debemos almacenarlos? + +00:02:16.586 --> 00:02:20.946 align:middle +Una opción para manejar los valores sensibles +es la bóveda de secretos de Symfony. + +00:02:21.446 --> 00:02:26.646 align:middle +Es un conjunto de archivos que contienen +variables de entorno de forma encriptada. + +00:02:27.316 --> 00:02:29.986 align:middle +Estos archivos son seguros para +enviarlos a tu repositorio... + +00:02:30.376 --> 00:02:31.736 align:middle +¡porque están encriptados! + +00:02:32.496 --> 00:02:37.536 align:middle +Si quieres almacenar secretos en una bóveda, +necesitarás dos: una para el entorno dev + +00:02:37.636 --> 00:02:39.306 align:middle +y otra para el entorno prod. + +00:02:40.086 --> 00:02:43.086 align:middle +Primero vamos a crear estas dos bóvedas... + +00:02:43.306 --> 00:02:46.656 align:middle +y luego te explicaré cómo +leer los valores de ellas. + +00:02:47.356 --> 00:02:49.466 align:middle +Empieza creando una para el entorno dev. + +00:02:49.926 --> 00:02:55.176 align:middle +Ejecuta: php bin/console +secrets:set Pasa este GITHUB_TOKEN, + +00:02:55.656 --> 00:02:57.566 align:middle +que es el secreto que queremos establecer. + +00:02:58.386 --> 00:03:00.786 align:middle +A continuación, nos pide +nuestro "valor secreto". + +00:03:01.436 --> 00:03:04.096 align:middle +Como se trata de la bóveda del entorno dev, + +00:03:04.546 --> 00:03:07.056 align:middle +queremos poner algo que sea +seguro para que todos lo vean. + +00:03:07.646 --> 00:03:09.436 align:middle +En un momento explicaré por qué. + +00:03:10.246 --> 00:03:11.836 align:middle +Diré CHANGEME. + +00:03:12.746 --> 00:03:14.616 align:middle +No puedes verme escribir eso... + +00:03:14.886 --> 00:03:17.836 align:middle +sólo porque Symfony lo oculta +por razones de seguridad. + +00:03:18.776 --> 00:03:21.126 align:middle +Como este es el primer +secreto que creamos, Symfony + +00:03:21.446 --> 00:03:25.256 align:middle +creó automáticamente la bóveda +de secretos entre bastidores... + +00:03:25.506 --> 00:03:31.346 align:middle +que es literalmente un conjunto de +archivos que viven en config/secrets/dev/. + +00:03:32.316 --> 00:03:37.676 align:middle +Para la bóveda dev, vamos a confirmar +todos estos archivos en el repositorio. + +00:03:38.346 --> 00:03:38.896 align:middle +Vamos a hacerlo. + +00:03:39.376 --> 00:03:42.626 align:middle +Añade todo el directorio de +secretos: Luego haz el commit con: + +00:03:43.116 --> 00:03:49.836 align:middle +git commit -m "adding dev secrets vault" Aquí +tienes una explicación rápida de los archivos. + +00:03:50.436 --> 00:03:55.746 align:middle +dev.list.php almacena una lista de los +valores que viven dentro de la bóveda, + +00:03:56.366 --> 00:04:03.196 align:middle +dev.GITHUB_TOKEN.28bd2f.php +almacena el valor cifrado real, + +00:04:03.786 --> 00:04:11.076 align:middle +y dev.encrypt.public.php es la clave +criptográfica que permite a los desarroll adores + +00:04:11.076 --> 00:04:14.126 align:middle +de tu equipo añadir más secretos. + +00:04:14.606 --> 00:04:18.716 align:middle +De modo que si otro desarrollador retira +el proyecto, tendrá este archivo... + +00:04:19.026 --> 00:04:20.856 align:middle +para poder añadir más secretos. + +00:04:21.766 --> 00:04:29.556 align:middle +Por último, dev.decrypt.private.php es la +clave secreta que nos permite desencriptar + +00:04:29.676 --> 00:04:31.846 align:middle +y leer los valores de la bóveda. En + +00:04:32.886 --> 00:04:37.616 align:middle +cuanto los archivos de la bóveda estén +presentes, Symfony los abrirá automáticamente, + +00:04:37.886 --> 00:04:41.926 align:middle +descifrará los secretos y los +expondrá como variables de entorno + +00:04:41.926 --> 00:04:44.346 align:middle +Pero, más sobre esto en unos minutos. + +00:04:45.226 --> 00:04:49.986 align:middle +Pero espera: ¿realmente acabamos de +confirmar la clave decrypt en el repositorio? + +00:04:50.616 --> 00:04:54.486 align:middle +Sí ¡Eso normalmente sería un no-no! + +00:04:55.046 --> 00:04:58.476 align:middle +¿Por qué te tomarías la +molestia de encriptar valores... + +00:04:58.806 --> 00:05:02.456 align:middle +sólo para almacenar la clave +de descifrado junto a ellos? La + +00:05:03.146 --> 00:05:09.616 align:middle +razón por la que hacemos esto es que se trata de nuestra +bóveda de desarrollo, lo que significa que sólo vamos a + +00:05:09.616 --> 00:05:13.136 align:middle +almacenar valores que son seguros para +que los vean todos los desarrolladores. + +00:05:13.776 --> 00:05:16.986 align:middle +La bóveda de dev sólo se utilizará +para el desarrollo local... + +00:05:17.476 --> 00:05:20.786 align:middle +y queremos que nuestros compañeros +de equipo puedan bajar el código + +00:05:21.216 --> 00:05:23.316 align:middle +y leerlo sin problemas. + +00:05:24.216 --> 00:05:26.786 align:middle +Bien, en este punto tenemos una bóveda dev + +00:05:26.786 --> 00:05:29.916 align:middle +que Symfony utilizará +automáticamente en el entorno dev. + +00:05:30.546 --> 00:05:35.916 align:middle +Siguiente: vamos a crear la bóveda prod, que +contendrá los valores verdaderamente secretos. + +00:05:36.616 --> 00:05:41.536 align:middle +Luego aprenderemos la relación entre los secretos +de la bóveda y las variables de entorno... + +00:05:41.936 --> 00:05:45.146 align:middle +así como una forma sencilla +de visualizar todo esto diff --git a/sfcasts/ep2-fundamentals/es/service-config.md b/sfcasts/ep2-fundamentals/es/service-config.md new file mode 100644 index 0000000..819d425 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/service-config.md @@ -0,0 +1,39 @@ +# Configuración manual del servicio en services.yaml + +En tu terminal, ejecuta: + +```terminal +bin/console debug:container --parameters +``` + +Uno de los parámetros de `kernel` se llama `kernel.debug`. Además de los entornos, Symfony tiene este concepto de "modo de depuración". Es verdadero para el entorno `dev` y falso para `prod`. Y, de vez en cuando, ¡es muy útil! + +He aquí nuestro nuevo reto (sobre todo para ver si podemos hacerlo). Dentro de`MixRepository`, quiero averiguar si estamos en modo de depuración. Si el modo de depuración es verdadero, guardaremos el caché durante 5 segundos. Si es falso, quiero almacenar en caché durante 60 segundos + +## ¡Inyección de dependencia! + +Retrocedamos un momento. Supón que estás trabajando dentro de un servicio como`MixRepository`. De repente te das cuenta de que necesitas algún otro servicio como el logger. ¿Qué haces para conseguir el registrador? La respuesta: haces el baile de la inyección de dependencia. Añades un argumento y una propiedad de `private LoggerInterface $logger`... y luego lo utilizas en tu código. Harás esto muchas veces en Symfony. + +Permíteme deshacer eso... porque en realidad no necesitamos el registrador ahora mismo. Pero lo que sí necesitamos es algo parecido. Ahora mismo estamos dentro de un servicio y de repente nos hemos dado cuenta de que necesitamos alguna configuración (la bandera `kernel.debug` ) para hacer nuestro trabajo. ¿Qué hacemos para conseguir esa configuración? Lo mismo Añadirla como argumento a nuestro constructor. Digamos `private bool $isDebug`, y aquí abajo, úsalo: si `$this->isDebug`, caché durante 5 segundos, si no, caché durante 60 segundos. + +## Argumentos no autoinstalables + +Pero... hay una pequeña complicación... y seguro que ya sabes cuál es. Cuando refrescamos la página... ¡vaya! Nos sale un error en `Cannot resolve argument`. Si saltas un poco, dice + +> No se puede autoconectar el servicio `App\Service\MixRepository`: el argumento `$isDebug` del +> método `__construct()` es de tipo `bool`, debes configurar su +> valor explícitamente. + +Eso tiene sentido. El autocableado sólo funciona para los servicios. No puedes tener un argumento bool`$isDebug` y esperar que Symfony se dé cuenta de alguna manera de que queremos el parámetro`kernel.debug`. Puede que sea un mago, pero no tengo un hechizo para eso. Sin embargo, puedo hacer desaparecer un trozo de pastel entero. Con magia. Sin duda. + +## Configurar MixRepository en services.yaml + +¿Cómo solucionamos esto? Abre un archivo que aún no hayamos mirado:`config/services.yaml`. Hasta ahora, no hemos necesitado añadir ninguna configuración para nuestro servicio`MixRepository`. El contenedor vio la clase `MixRepository` en cuanto la creamos... y el autocableado ayudó al contenedor a saber qué argumentos pasar al constructor. Pero ahora que tenemos un argumento no autocable, tenemos que dar una pista al contenedor. Y eso lo hacemos en este archivo. + +Dirígete a la parte inferior y añade el espacio de nombres completo de esta clase:`App\Service\MixRepository`. Debajo de eso, utiliza la palabra `bind`. Y debajo de eso, dale al contenedor una pista para indicarle qué debe pasar al argumento diciendo`$isDebug` set to `%kernel.debug%` + +Estoy utilizando `$isDebug` a propósito. Eso tiene que coincidir exactamente con el nombre del argumento en la clase. Gracias a esto, el contenedor pasará el valor del parámetro`kernel.debug`. + +Y cuando lo probamos... ¡funciona! Los dos argumentos del servicio siguen estando autocableados, pero hemos rellenado el único argumento que faltaba para que el contenedor pueda instanciar nuestro servicio. ¡Muy bien! + +Quiero hablar más del propósito de este archivo y de toda la configuración aquí arriba. Resulta que mucha de la magia que hemos estado viendo relacionada con los servicios y el autocableado se puede explicar con este código. Eso a continuación. diff --git a/sfcasts/ep2-fundamentals/es/service-config.vtt b/sfcasts/ep2-fundamentals/es/service-config.vtt new file mode 100644 index 0000000..9748d29 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/service-config.vtt @@ -0,0 +1,228 @@ +WEBVTT + +00:00:01.076 --> 00:00:03.886 align:middle +En tu terminal, ejecuta: +bin/console debug:container + +00:00:03.886 --> 00:00:09.256 align:middle +--parameters Uno de los parámetros +de kernel se llama kernel.debug. + +00:00:10.106 --> 00:00:14.696 align:middle +Además de los entornos, Symfony tiene +este concepto de "modo de depuración". + +00:00:15.386 --> 00:00:19.056 align:middle +Es verdadero para el entorno +dev y falso para prod. + +00:00:19.476 --> 00:00:21.906 align:middle +Y, de vez en cuando, ¡es muy útil! + +00:00:22.746 --> 00:00:26.476 align:middle +He aquí nuestro nuevo reto (sobre +todo para ver si podemos hacerlo). + +00:00:27.166 --> 00:00:31.666 align:middle +Dentro de MixRepository, quiero averiguar +si estamos en modo de depuración. + +00:00:32.216 --> 00:00:36.246 align:middle +Si el modo de depuración es verdadero, +guardaremos el caché durante 5 segundos. + +00:00:36.486 --> 00:00:41.896 align:middle +Si es falso, quiero almacenar en caché +durante 60 segundos Retrocedamos un minuto. + +00:00:42.546 --> 00:00:46.026 align:middle +Supón que estás trabajando dentro +de un servicio como MixRepository. + +00:00:46.656 --> 00:00:51.926 align:middle +De repente te das cuenta de que necesitas +algún otro servicio como el logger. + +00:00:52.476 --> 00:00:54.326 align:middle +¿Qué haces para obtener el registrador? + +00:00:55.046 --> 00:00:59.116 align:middle +La respuesta: haces el baile de +la inyección de dependencia. + +00:00:59.646 --> 00:01:03.596 align:middle +Añades un argumento y una propiedad +de private LoggerInterface $logger... + +00:01:03.976 --> 00:01:05.756 align:middle +y luego lo utilizas en tu código. + +00:01:06.346 --> 00:01:09.146 align:middle +Harás esto muchas veces en Symfony. + +00:01:09.906 --> 00:01:11.406 align:middle +Permíteme deshacer eso... + +00:01:11.606 --> 00:01:14.986 align:middle +porque en realidad no necesitamos +el registrador ahora mismo. + +00:01:15.516 --> 00:01:17.766 align:middle +Pero lo que sí necesitamos +es algo parecido. Ahora mismo + +00:01:18.406 --> 00:01:23.116 align:middle +estamos dentro de un servicio y +de repente nos hemos dado cuenta + +00:01:23.146 --> 00:01:29.366 align:middle +de que necesitamos alguna configuración (la +bandera kernel.debug ) para hacer nuestro trabajo. + +00:01:30.146 --> 00:01:32.656 align:middle +¿Qué hacemos para +conseguir esa configuración? + +00:01:33.376 --> 00:01:35.216 align:middle +Lo mismo + +00:01:35.686 --> 00:01:37.786 align:middle +Añadirla como argumento a nuestro constructor. + +00:01:38.286 --> 00:01:46.086 align:middle +Digamos private bool $isDebug, y aquí +abajo, úsalo: si $this->isDebug, caché + +00:01:46.136 --> 00:01:49.456 align:middle +durante 5 segundos, si no, +caché durante 60 segundos. + +00:01:50.216 --> 00:01:52.756 align:middle +Pero... hay una pequeña complicación... + +00:01:52.906 --> 00:01:55.736 align:middle +y seguro que ya sabes cuál es. + +00:01:56.406 --> 00:01:57.836 align:middle +Cuando refrescamos la página... + +00:01:58.346 --> 00:02:01.926 align:middle +¡vaya! Obtenemos un error +de Cannot resolve argument. + +00:02:02.576 --> 00:02:07.516 align:middle +Si saltas un poco, dice No se puede autoconectar +el servicio App\Service\MixRepository: el + +00:02:07.656 --> 00:02:11.986 align:middle +argumento $isDebug del método +__construct() es de tipo bool, + +00:02:12.216 --> 00:02:15.416 align:middle +debes configurar su valor explícitamente. + +00:02:16.116 --> 00:02:17.496 align:middle +Esto tiene sentido. + +00:02:18.006 --> 00:02:21.056 align:middle +El autocableado sólo +funciona para los servicios. + +00:02:21.526 --> 00:02:28.656 align:middle +No puedes tener un argumento bool $isDebug y +esperar que Symfony se dé cuenta de alguna manera + +00:02:28.656 --> 00:02:31.356 align:middle +de que queremos el parámetro kernel.debug. + +00:02:32.256 --> 00:02:35.526 align:middle +Puede que sea un mago, pero +no tengo un hechizo para eso. + +00:02:36.126 --> 00:02:39.086 align:middle +Sin embargo, puedo hacer desaparecer +un trozo de pastel entero. + +00:02:39.476 --> 00:02:40.256 align:middle +Con magia. + +00:02:40.666 --> 00:02:41.186 align:middle +Sin duda. + +00:02:41.976 --> 00:02:43.206 align:middle +¿Cómo lo arreglamos? + +00:02:43.756 --> 00:02:48.356 align:middle +Abre un archivo que aún no hayamos +mirado: config/services.yaml. + +00:02:49.246 --> 00:02:55.106 align:middle +Hasta ahora, no hemos necesitado añadir ninguna +configuración para nuestro servicio MixRepository. + +00:02:55.746 --> 00:03:00.476 align:middle +El contenedor vio la clase +MixRepository en cuanto la creamos... + +00:03:00.476 --> 00:03:05.426 align:middle +y el autocableado ayudó al contenedor a saber +qué argumentos debía pasar al constructor. + +00:03:06.186 --> 00:03:11.786 align:middle +Pero ahora que tenemos un argumento no autocable, +tenemos que dar una pista al contenedor. + +00:03:12.166 --> 00:03:14.456 align:middle +Y eso lo hacemos en este archivo. + +00:03:15.106 --> 00:03:17.836 align:middle +Dirígete a la parte inferior y +añade el espacio de nombres completo + +00:03:17.836 --> 00:03:22.386 align:middle +de esta clase: App\Service\MixRepository. + +00:03:23.006 --> 00:03:25.856 align:middle +Debajo de eso, utiliza la palabra bind. + +00:03:26.346 --> 00:03:32.236 align:middle +Y debajo de eso, dale al contenedor una pista +para indicarle qué debe pasar al argumento + +00:03:32.456 --> 00:03:42.146 align:middle +diciendo $isDebug ajustado a %kernel.debug% +Estoy usando $isDebug a propósito. + +00:03:42.576 --> 00:03:47.006 align:middle +Eso tiene que coincidir exactamente con +el nombre del argumento en la clase. + +00:03:47.646 --> 00:03:52.246 align:middle +Gracias a esto, el contenedor pasará +el valor del parámetro kernel.debug. + +00:03:52.886 --> 00:03:54.206 align:middle +Y cuando lo probamos... + +00:03:55.026 --> 00:04:00.026 align:middle +¡funciona! Los dos argumentos del +servicio siguen estando autocableados, + +00:04:00.496 --> 00:04:06.086 align:middle +pero hemos rellenado el único argumento que faltaba +para que el contenedor pueda instanciar nuestro servicio. + +00:04:06.446 --> 00:04:13.456 align:middle +¡Muy bien! Quiero hablar más del propósito de este +archivo y de toda la configuración aquí arriba. Resulta + +00:04:14.016 --> 00:04:18.716 align:middle +que mucha de la magia que hemos estado +viendo relacionada con los servicios + +00:04:18.716 --> 00:04:22.696 align:middle +y el autocableado se puede +explicar con este código. + +00:04:23.176 --> 00:04:24.186 align:middle +Eso a continuación diff --git a/sfcasts/ep2-fundamentals/es/services-yaml.md b/sfcasts/ep2-fundamentals/es/services-yaml.md new file mode 100644 index 0000000..017b434 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/services-yaml.md @@ -0,0 +1,79 @@ +# Todo sobre services.yaml + +Cuando Symfony arranca por primera vez, necesita obtener la lista completa de todos los servicios que deben estar en el contenedor. Esto incluye el ID del servicio, su nombre de clase y todos los argumentos de su constructor. La primera y mayor fuente de servicios son los bundles. Si ejecutas + +```terminal +php bin/console debug:container +``` + +la gran mayoría de estos servicios provienen de bundles. El segundo lugar del que el contenedor obtiene servicios es nuestro código. Y para conocer nuestros servicios, Symfony lee `services.yaml`. + +## La sección especial _defaults + +En el momento en que Symfony comienza a analizar la primera línea de este archivo, nada en nuestro directorio `src/` ha sido registrado como servicio en el contenedor. Esto es realmente importante. Añadir nuestras clases al contenedor es, de hecho, el trabajo de este archivo Y la forma en que lo hace es bastante sorprendente. ¡Hagamos un recorrido! + +Fíjate en que la configuración está bajo una clave `services`. Al igual que `parameters`, ésta es una clave especial. Y, como su nombre indica, todo lo que está bajo ella está destinado a configurar servicios. + +La primera subclave bajo ésta es `_defaults`. `_defaults` es una clave mágica que nos permite definir algunas opciones por defecto que se añadirán a todos los servicios que se registren en este archivo. Así que todos los servicios que registremos a continuación tendrán automáticamente `autowire: true` y `autoconfigure: true`. + +Veamos un ejemplo. Lo más básico que puedes hacer con la clave `services`es... ¡registrar un servicio! Eso es lo que estamos haciendo en la parte inferior. Esto le dice al contenedor que debe haber un servicio `App\Service\MixRepository` en el contenedor y especificamos una opción: `bind`. + +En realidad, los servicios pueden tener un montón de opciones, incluyendo `autowire` y`autoconfigure`. Así que sería totalmente legal decir: `autowire: true` y`autoconfigure: true` aquí. Esto funcionaría perfectamente. Pero gracias a la sección`_defaults`, ¡no son necesarias! El `_defaults` dice: + +> A menos que se haya anulado en un servicio específico, establece `autowire` y +> `autoconfigure` a `true` para todos los servicios de este archivo. + +¿Y qué hace `autowire`? Muy sencillo Le dice al contenedor de Symfony: + +> ¡Oye! Por favor, intenta adivinar los argumentos de mi constructor mirando sus sugerencias de tipo. + +Esta función es bastante impresionante... y por eso está activada automáticamente para todos nuestros servicios. La otra opción - `autoconfigure` - es más sutil y hablaremos de ella más adelante. + +## Registro automático de servicios + +Muy bien, cuando llegamos a la línea `_defaults`, hemos establecido alguna configuración por defecto... pero aún no hemos registrado ningún servicio. Ese es el trabajo de la siguiente sección... y es la clave de todo. Esta sintaxis especial dice + +> Mira dentro del directorio `src/` y registra automáticamente todas +> las clases de PHP como servicio... excepto estas tres cosas. + +Por eso, inmediatamente después de crear la clase `MixRepository`, ¡ya estaba en el contenedor! Y gracias a la sección `_defaults`, cualquier servicio registrado por ésta tendrá automáticamente `autowire: true` y`autoconfigure: true`. ¡Eso es un gran trabajo en equipo! Este mecanismo se denomina "Registro automático de servicios". + +Pero recuerda que cada servicio del contenedor debe tener un ID único. Si vuelves a mirar `debug:container`, la mayoría de los ID de servicio son de tipo serpiente. Permíteme alejar el zoom un poco para que sea más fácil de ver. ¡Mejor! Así, por ejemplo, el servicio `Twig` tiene el ID de `twig` en forma de serpiente. Pero si te desplazas hasta la parte superior de esta lista, nuestro ID de`MixRepository` es... el nombre completo de la clase. + +¡Sí! Cuando utilizas el registro automático de servicios, éste utiliza el nombre de la clase como ID de la clase y del servicio. Esto se hace por simplicidad... pero también para el autocableado. Cuando intentemos autocablear `MixRepository` en nuestro controlador o en cualquier otro lugar, para saber qué servicio pasarnos, Symfony buscará un servicio cuyo ID coincida exactamente con `App\Service\MixRepository`. Así que el auto-registro de servicios no sólo registra nuestras clases como servicios, sino que lo hace de forma que las hace auto-cableables. ¡Eso es increíble! + +## ¿Autorregistro de no servicios? + +De todos modos, después de esta sección aquí, cada clase en `src/` está ahora registrada como servicio en el contenedor. Excepto, bueno... que no queremos que todas las clases de`src/` sean un servicio. + +En realidad hay dos tipos de clases en tu aplicación: las "clases de servicio" que hacen el trabajo, y las "clases modelo" -a veces llamadas "DTO"- cuyo trabajo consiste principalmente en mantener los datos, como una clase `Product` con las propiedades `name` y `price`. Queremos que el contenedor se encargue de instanciar nuestros servicios. Pero en el caso de las clases modelo, las crearemos siempre que las necesitemos, como con `$product = new Product()`. Por lo tanto, éstas no serán servicios en el contenedor. + +En el siguiente tutorial, crearemos clases de entidad Doctrine, que son clases modelo para la base de datos. Éstas vivirán en el directorio `src/Entity/`... y como no están destinadas a ser servicios, ese directorio está excluido. Así que registramos todo en el directorio `src/` como servicio, excepto estas tres cosas. + +Pero... ¡dato curioso! Esta clave `exclude` no es tan importante. Podrías eliminarla y todo seguiría funcionando Si accidentalmente registras como servicio algo que no debe serlo, ¡no te preocupes! Como nunca intentarás autoconectar y utilizar esa clase como un servicio, Symfony se dará cuenta de que no se está utilizando y la eliminará del contenedor. ¡Vaya, qué inteligente! + +## Configuración de servicios personalizados + +Así que todo en `src/` se registra automáticamente como un servicio sin que tengamos que hacer nada ni tocar este archivo. + +Pero... de vez en cuando, necesitarás añadir una configuración extra a un servicio concreto. Eso es lo que ocurrió con `MixRepository` gracias a su argumento no autoadministrable`$isDebug`. + +Para solucionarlo, al final de este archivo, registraremos un nuevo servicio cuyo ID y clase es `App\Service\MixRepository`. En realidad, esto anulará el servicio que se creó durante el registro automático de servicios, ya que ambos ID coincidirán con`App\Service\MixRepository`. Por tanto, estamos definiendo un nuevo servicio. + +Pero gracias a `_defaults`, tiene automáticamente `autowire: true` y`autoconfigure: true`. Entonces añadimos la opción adicional `bind`. + +Así que lo único que tenemos que poner al final de este archivo son los servicios que necesitan una configuración adicional para funcionar. Y... en realidad hay una forma más genial de arreglar los argumentos no autoconfigurables que te mostraré a continuación. + +## ¡Todos los archivos de configuración son iguales! + +Pero antes de llegar a eso, quiero mencionar una cosa más: este archivo,`services.yaml`, se carga mediante el mismo sistema que carga todos los archivos en`config/packages/`. De hecho, no hay ninguna diferencia técnica entre este archivo y digamos... `framework.yaml`. Así es Si quisiéramos, podríamos copiar y borrar el contenido de `services.yaml`, pegarlo en `framework.yaml`, y todo funcionaría exactamente igual. + +Excepto que... tendríamos que, ya sabes, corregir estas rutas ya que estamos un directorio más abajo. ¡Observa! Moveré esto rápidamente y... ¡esto sigue funcionando bien! ¡Genial! Volvamos a ponerlo como estaba y... ya está. + +La única razón por la que tenemos un archivo `service.yaml` es para organizarnos. Es bueno tener un solo archivo para "configurar tus servicios". Lo verdaderamente importante es que toda esta configuración vive bajo la clave `services`. De hecho, cerca de la parte superior de este archivo, verás que hay una clave `parameters` vacía. + +En `cache.yaml`, creamos allí una clave `parameters` para registrar un nuevo parámetro. Realmente depende de nosotros decidir dónde queremos definir este parámetro. Podemos hacerlo en `cache.yaml` o, para mantener todos los parámetros en un solo lugar, podríamos copiar esto y trasladarlo a `services.yaml`. + +En `cache.yaml`, también cogeré el `when@dev`, lo borraré y lo pegaré en`services.yaml`. A nivel técnico, eso no supone ninguna diferencia y nuestra aplicación sigue funcionando. Pero me gusta más esto. Los servicios y los parámetros son una idea global en tu app... así que es bueno organizarlos todos en un solo archivo. + +De acuerdo, la única razón por la que escribimos algún código en la parte inferior de `services.yaml`fue para decirle al contenedor qué debe pasar al argumento no autoadministrable `$isDebug`. Pero, ¿y si te digo que hay una forma más automática de resolver estos argumentos problemáticos? Eso a continuación. diff --git a/sfcasts/ep2-fundamentals/es/services-yaml.vtt b/sfcasts/ep2-fundamentals/es/services-yaml.vtt new file mode 100644 index 0000000..b80ad6d --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/services-yaml.vtt @@ -0,0 +1,510 @@ +WEBVTT + +00:00:01.016 --> 00:00:06.066 align:middle +Cuando Symfony arranca por primera vez, +necesita obtener la lista completa de todos los + +00:00:06.066 --> 00:00:08.426 align:middle +servicios que deben estar en el contenedor. + +00:00:08.916 --> 00:00:15.116 align:middle +Esto incluye el ID del servicio, el nombre de su +clase y todos los argumentos de su constructor. + +00:00:15.816 --> 00:00:19.596 align:middle +La primera y mayor fuente de +servicios son los bundles. + +00:00:20.246 --> 00:00:25.566 align:middle +Si ejecutas php bin/console +debug:container la gran mayoría + +00:00:25.566 --> 00:00:27.886 align:middle +de estos servicios provienen de bundles. + +00:00:28.676 --> 00:00:33.256 align:middle +El segundo lugar del que el contenedor +obtiene servicios es nuestro código. + +00:00:33.806 --> 00:00:38.786 align:middle +Y para conocer nuestros servicios, Symfony lee +services.yaml. En el momento en que Symfony + +00:00:39.636 --> 00:00:43.786 align:middle +comienza a analizar la primera +línea de este archivo, + +00:00:44.146 --> 00:00:48.966 align:middle +nada de nuestro directorio src/ ha sido +registrado como servicio en el contenedor. + +00:00:49.646 --> 00:00:51.236 align:middle +Esto es realmente importante. + +00:00:51.826 --> 00:00:57.596 align:middle +Añadir nuestras clases al contenedor +es, de hecho, el trabajo de este archivo + +00:00:58.126 --> 00:01:00.456 align:middle +Y la forma en que lo hace +es bastante sorprendente. + +00:01:01.056 --> 00:01:02.116 align:middle +¡Hagamos un recorrido! + +00:01:02.856 --> 00:01:05.526 align:middle +Fíjate en que la configuración +está bajo una clave services. + +00:01:06.146 --> 00:01:08.996 align:middle +Al igual que parameters, +ésta es una clave especial. + +00:01:09.416 --> 00:01:15.306 align:middle +Y, como su nombre indica, todo lo que está bajo +ella está destinado a configurar servicios. + +00:01:15.946 --> 00:01:19.316 align:middle +La primera subclave bajo ésta es _defaults. + +00:01:19.976 --> 00:01:27.436 align:middle +_defaults es una clave mágica que nos permite +definir algunas opciones por defecto que se añadirán + +00:01:27.436 --> 00:01:31.786 align:middle +a todos los servicios que se +registren en este archivo. + +00:01:32.426 --> 00:01:38.006 align:middle +Así, todos los servicios que registremos a +continuación tendrán automáticamente autowire: + +00:01:38.006 --> 00:01:40.206 align:middle +true y autoconfigure: true. + +00:01:41.016 --> 00:01:42.356 align:middle +Veamos un ejemplo. + +00:01:42.946 --> 00:01:46.986 align:middle +Lo más básico que puedes hacer +con la clave services es... + +00:01:47.406 --> 00:01:49.066 align:middle +¡registrar un servicio! + +00:01:49.586 --> 00:01:51.286 align:middle +Eso es lo que estamos +haciendo en la parte inferior. + +00:01:51.976 --> 00:01:58.116 align:middle +Esto le dice al contenedor que debe haber +un servicio App\Service\MixRepository + +00:01:58.236 --> 00:02:02.686 align:middle +en el contenedor y +especificamos una opción: bind. + +00:02:03.476 --> 00:02:10.006 align:middle +En realidad, los servicios pueden tener un montón +de opciones, como autowire y autoconfigure. + +00:02:10.776 --> 00:02:17.376 align:middle +Así que sería totalmente legal decir: +autowire: true y autoconfigure: true aquí. + +00:02:17.976 --> 00:02:20.086 align:middle +Esto funcionaría perfectamente. + +00:02:20.646 --> 00:02:24.666 align:middle +Pero gracias a la sección +_defaults, ¡no son necesarias! + +00:02:25.416 --> 00:02:32.376 align:middle +El _defaults dice: A menos que se haya anulado +en un servicio específico, establece autowire + +00:02:32.406 --> 00:02:36.996 align:middle +y autoconfigure en true para todos +los servicios de este archivo. + +00:02:37.786 --> 00:02:40.216 align:middle +¿Y qué hace autowire? + +00:02:40.776 --> 00:02:44.016 align:middle +Muy sencillo Le dice al +contenedor de Symfony ¡Eh! + +00:02:44.436 --> 00:02:49.636 align:middle +Por favor, intenta adivinar los argumentos de +mi constructor mirando sus sugerencias de tipo. + +00:02:50.386 --> 00:02:52.286 align:middle +Esta función es bastante impresionante... + +00:02:52.506 --> 00:02:56.616 align:middle +y por eso está activada automáticamente +en todos nuestros servicios. + +00:02:57.416 --> 00:03:02.056 align:middle +La otra opción - autoconfigure - es más +sutil y hablaremos de ella más adelante. + +00:03:02.986 --> 00:03:06.116 align:middle +Muy bien, cuando lleguemos a +la línea _defaults, habremos + +00:03:06.546 --> 00:03:09.686 align:middle +establecido alguna +configuración por defecto... + +00:03:10.206 --> 00:03:14.386 align:middle +pero aún no hemos registrado ningún servicio. + +00:03:15.076 --> 00:03:17.216 align:middle +Eso a continuación... + +00:03:17.666 --> 00:03:20.656 align:middle +y es la clave de todo. + +00:03:21.406 --> 00:03:25.486 align:middle +Esta sintaxis especial dice Por +favor, mira dentro del directorio src/ + +00:03:25.916 --> 00:03:30.526 align:middle +y registra automáticamente todas +las clases de PHP como un servicio... + +00:03:30.866 --> 00:03:32.856 align:middle +excepto estas tres cosas. + +00:03:33.576 --> 00:03:38.366 align:middle +Por eso, inmediatamente después +de crear la clase MixRepository, + +00:03:38.826 --> 00:03:41.316 align:middle +¡ya estaba en el contenedor! + +00:03:41.986 --> 00:03:46.146 align:middle +Y gracias a la sección _defaults, +cualquier servicio registrado + +00:03:46.146 --> 00:03:51.626 align:middle +por ésta tendrá automáticamente +autowire: true y autoconfigure: true. + +00:03:52.346 --> 00:03:54.596 align:middle +¡Eso es un gran trabajo en equipo! + +00:03:55.406 --> 00:03:59.156 align:middle +Este mecanismo se denomina +"Registro automático de servicios". + +00:04:00.046 --> 00:04:05.216 align:middle +Pero recuerda que cada servicio del +contenedor debe tener un ID único. + +00:04:06.026 --> 00:04:11.456 align:middle +Si vuelves a mirar debug:container, la mayoría +de los ID de servicio son de tipo serpiente. + +00:04:12.146 --> 00:04:14.386 align:middle +Permíteme alejar el zoom un poco +para que sea más fácil de ver. + +00:04:15.046 --> 00:04:21.126 align:middle +¡Mejor! Así, por ejemplo, el servicio Twig +tiene el ID de twig en forma de serpiente. + +00:04:21.756 --> 00:04:26.626 align:middle +Pero si te desplazas hasta la parte superior de +esta lista, nuestro ID de MixRepository es... + +00:04:26.876 --> 00:04:28.746 align:middle +el nombre completo de la clase. + +00:04:29.346 --> 00:04:34.256 align:middle +¡Sí! Cuando utilizas el registro automático +de servicios, éste utiliza el nombre de la + +00:04:34.406 --> 00:04:38.456 align:middle +clase como ID de la clase y del servicio. + +00:04:39.276 --> 00:04:41.286 align:middle +Esto se hace por simplicidad... + +00:04:41.716 --> 00:04:43.896 align:middle +pero también para el autocableado. + +00:04:44.686 --> 00:04:49.616 align:middle +Cuando intentemos autocablear MixRepository en +nuestro controlador o en cualquier otro lugar, + +00:04:50.116 --> 00:04:52.806 align:middle +para saber qué servicio pasarnos, + +00:04:53.216 --> 00:05:00.786 align:middle +Symfony buscará un servicio cuyo ID coincida +exactamente con App\Service\MixRepository. + +00:05:01.466 --> 00:05:06.706 align:middle +Así que el auto-registro de servicios no sólo +registra nuestras clases como servicios, sino que + +00:05:07.106 --> 00:05:10.726 align:middle +lo hace de forma que las hace auto-cableables. + +00:05:11.006 --> 00:05:12.226 align:middle +¡Eso es increíble! De + +00:05:13.136 --> 00:05:16.346 align:middle +todos modos, después de esta +sección, todas las clases + +00:05:16.346 --> 00:05:20.256 align:middle +de src/ están ahora registradas +como servicios en el contenedor. + +00:05:20.956 --> 00:05:21.796 align:middle +Excepto, bueno... + +00:05:22.106 --> 00:05:25.176 align:middle +no queremos que todas las +clases de src/ sean un servicio. + +00:05:25.776 --> 00:05:31.356 align:middle +En realidad hay dos tipos de clases en tu aplicación: +las " clases de servicio " que hacen el trabajo + +00:05:31.836 --> 00:05:38.416 align:middle +, y las "clases modelo" -a veces llamadas "DTO"- cuyo +trabajo consiste principalmente en mantener los datos, + +00:05:38.916 --> 00:05:42.926 align:middle +como una clase Product con +las propiedades name y price. + +00:05:43.446 --> 00:05:47.776 align:middle +Queremos que el contenedor se encargue +de instanciar nuestros servicios. + +00:05:48.316 --> 00:05:52.206 align:middle +Pero en el caso de las clases modelo, las +crearemos siempre que las necesitemos, + +00:05:52.206 --> 00:05:55.096 align:middle +como con $product = new Product(). + +00:05:55.876 --> 00:05:58.786 align:middle +Por lo tanto, éstas no serán +servicios en el contenedor. En el + +00:05:59.516 --> 00:06:03.686 align:middle +siguiente tutorial, crearemos +clases de entidad Doc trine, + +00:06:03.976 --> 00:06:06.316 align:middle +que son clases modelo para la base de datos. + +00:06:07.006 --> 00:06:09.586 align:middle +Éstas vivirán en el directorio src/Entity/... + +00:06:10.046 --> 00:06:14.966 align:middle +y como no están destinadas a ser +servicios, ese directorio está excluido. + +00:06:15.716 --> 00:06:22.116 align:middle +Así que registramos todo en el directorio +src/ como servicio, excepto estas tres cosas. + +00:06:22.776 --> 00:06:24.096 align:middle +Pero... ¡dato curioso! + +00:06:24.506 --> 00:06:27.596 align:middle +Esta clave exclude no es tan importante. + +00:06:28.346 --> 00:06:32.086 align:middle +Podrías eliminarla y +todo seguiría funcionando + +00:06:32.616 --> 00:06:35.756 align:middle +Si accidentalmente registras como servicio algo + +00:06:35.836 --> 00:06:39.076 align:middle +que no debe serlo, ¡no te preocupes! + +00:06:39.676 --> 00:06:43.866 align:middle +Como nunca intentarás autoconectar y +utilizar esa clase como un servicio, + +00:06:44.456 --> 00:06:48.966 align:middle +Symfony se dará cuenta de que no se está +utilizando y la eliminará del contenedor. + +00:06:49.606 --> 00:06:50.666 align:middle +¡Vaya, eso es inteligente! + +00:06:51.456 --> 00:06:56.896 align:middle +Así que todo en src/ se registra automáticamente +como un servicio sin que tengamos que + +00:06:56.896 --> 00:06:59.256 align:middle +hacer nada ni tocar este archivo. + +00:07:00.176 --> 00:07:06.396 align:middle +Pero... ocasionalmente, necesitarás añadir una +configuración extra a un servicio específico. + +00:07:07.046 --> 00:07:13.736 align:middle +Eso es lo que ocurrió con MixRepository gracias +a su argumento no autoadministrable $isDebug. + +00:07:14.316 --> 00:07:20.306 align:middle +Para solucionarlo, al final de este archivo, +registraremos un nuevo servicio cuyo ID + +00:07:20.306 --> 00:07:24.316 align:middle +y clase es App\Service\MixRepository. + +00:07:25.276 --> 00:07:31.456 align:middle +En realidad, esto anulará el servicio que se +creó durante el registro automático de servicios, + +00:07:31.976 --> 00:07:36.846 align:middle +ya que ambos ID coincidirán +con App\Service\MixRepository. + +00:07:37.466 --> 00:07:41.016 align:middle +Por tanto, estamos +definiendo un nuevo servicio. + +00:07:41.826 --> 00:07:47.966 align:middle +Pero gracias a _defaults, tiene automáticamente +autowire: true y autoconfigure: true. + +00:07:48.706 --> 00:07:51.346 align:middle +Entonces añadimos la opción adicional bind. + +00:07:51.986 --> 00:07:55.946 align:middle +Así que lo único que tenemos que poner +al final de este archivo son los servicios + +00:07:55.946 --> 00:07:58.756 align:middle +que necesitan una configuración +adicional para funcionar. + +00:07:59.646 --> 00:08:02.306 align:middle +Y... en realidad hay una forma más genial + +00:08:02.306 --> 00:08:06.096 align:middle +de arreglar los argumentos no autoconfigurables +que te mostraré a continuación. + +00:08:06.946 --> 00:08:12.926 align:middle +Pero antes de llegar a eso, quiero mencionar +una cosa más: este archivo, services.yaml, + +00:08:13.266 --> 00:08:19.556 align:middle +se carga mediante el mismo sistema que carga +todos los archivos en config/packages/. + +00:08:20.146 --> 00:08:24.946 align:middle +De hecho, no hay ninguna diferencia técnica +entre este archivo y, por ejemplo... + +00:08:25.086 --> 00:08:26.326 align:middle +framework.yaml. + +00:08:27.046 --> 00:08:27.446 align:middle +Así es + +00:08:27.646 --> 00:08:32.606 align:middle +Si quisiéramos, podríamos copiar y +borrar el contenido de services.yaml, + +00:08:33.006 --> 00:08:38.086 align:middle +pegarlo en framework.yaml, y todo +funcionaría exactamente igual. + +00:08:38.926 --> 00:08:39.766 align:middle +Excepto que... + +00:08:39.806 --> 00:08:45.176 align:middle +tendríamos que, ya sabes, corregir estas +rutas ya que estamos un directorio más allá. + +00:08:45.976 --> 00:08:48.956 align:middle +¡Observa! Moveré esto muy rápido y... + +00:08:49.436 --> 00:08:51.516 align:middle +¡todavía funciona bien! + +00:08:52.206 --> 00:08:58.406 align:middle +¡Genial! Volvamos a ponerlo como estaba y... + +00:08:58.406 --> 00:08:58.856 align:middle +ya está. + +00:08:59.736 --> 00:09:04.456 align:middle +La única razón por la que tenemos un +archivo service.yaml es para organizarnos. + +00:09:05.056 --> 00:09:09.776 align:middle +Es bueno tener un solo archivo +para "configurar tus servicios". + +00:09:10.576 --> 00:09:16.586 align:middle +Lo verdaderamente importante es que toda esta +configuración vive bajo la clave services. + +00:09:17.406 --> 00:09:22.316 align:middle +De hecho, cerca de la parte superior de este +archivo, verás que hay una clave parameters vacía. + +00:09:23.136 --> 00:09:28.926 align:middle +En cache.yaml, creamos allí una clave +parameters para registrar un nuevo parámetro. + +00:09:29.806 --> 00:09:34.416 align:middle +Realmente depende de nosotros decidir +dónde queremos definir este parámetro. + +00:09:34.946 --> 00:09:42.446 align:middle +Podemos hacerlo en cache.yaml o, para mantener todos +los parámetros en un solo lugar, podríamos copiar esto + +00:09:42.536 --> 00:09:44.916 align:middle +y trasladarlo a services.yaml. + +00:09:45.786 --> 00:09:53.246 align:middle +En cache.yaml, también cogeré el when@dev, +lo borraré y lo pegaré en services.yaml. + +00:09:54.046 --> 00:09:58.796 align:middle +A nivel técnico, eso no supone ninguna diferencia +y nuestra aplicación sigue funcionando. + +00:09:59.376 --> 00:10:00.346 align:middle +Pero me gusta más esto. + +00:10:00.966 --> 00:10:04.676 align:middle +Los servicios y parámetros son +una idea global en tu app... + +00:10:05.136 --> 00:10:07.416 align:middle +así que es bueno organizarlos +todos en un solo archivo. De + +00:10:07.416 --> 00:10:13.636 align:middle +acuerdo, la única razón por la que escribimos algún +código en la parte inferior de services.yaml fue + +00:10:13.636 --> 00:10:19.266 align:middle +para decirle al contenedor qué debe pasar +al argumento no autoadministrable $isDebug. + +00:10:20.136 --> 00:10:25.026 align:middle +Pero, ¿y si te dijera que hay una forma más +automática de resolver estos argumentos problemáticos? + +00:10:25.436 --> 00:10:26.496 align:middle +Eso a continuación diff --git a/sfcasts/ep2-fundamentals/es/time-bundle.md b/sfcasts/ep2-fundamentals/es/time-bundle.md new file mode 100644 index 0000000..2f8bc67 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/time-bundle.md @@ -0,0 +1,65 @@ +# Nuevo Bundle, nuevo servicio: KnpTimeBundle + +En nuestro sitio, puedes crear tu propia mezcla de vinilo. (O podrás hacerlo con el tiempo... ahora mismo, este botón no hace nada). Pero otra gran característica de nuestro sitio es la posibilidad de explorar las mezclas de otros usuarios. + +Ahora que lo estoy viendo, podría ser útil si pudiéramos ver cuándo se creó cada mezcla. + +Si no recuerdas en qué parte de nuestro código se creó esta página, puedes utilizar un truco. Abajo, en la barra de herramientas de depuración de la web, pasa el ratón por encima del código de estado 200. Ah, ¡ja! Esto nos muestra que el controlador detrás de esta página es `VinylController::browse`. + +¡Genial! Ve a abrir `src/Controller/VinylController.php`. Aquí está la acción `browse`: + +[[[ code('1d9b35c7dc') ]]] + +Por cierto, he actualizado un poco el código desde el primer episodio... así que asegúrate de tener una copia fresca si estás codificando conmigo. + +Este método llama a `$this->getMixes()`... que es una función privada que he creado en la parte inferior: + +[[[ code('12af057d07') ]]] + +Esto devuelve una gran matriz de datos falsos que representa las mezclas que vamos a representar en la página. Eventualmente, obtendremos esto de una fuente dinámica, como una base de datos. + +## Impresión de fechas en Twig + +Observa que cada mezcla tiene un campo de fecha `createdAt`. Obtenemos estas mezclas en `browse()`... y las pasamos como una variable `mixes` a `vinyl/browse.html.twig`. Vamos a saltar a esa plantilla. + +Aquí abajo, utilizamos el bucle `for` de Twig para recorrer `mixes`. ¡Es muy sencillo! + +[[[ code('609c972403') ]]] + +Ahora imprimamos también la fecha "creada en". Añade un `|`, otro `` y luego digamos `{{ mix.createdAt }}`. + +Sólo hay un problema. Si miras `createdAt`... es un objeto `DateTime`. Y no puedes imprimir simplemente objetos `DateTime`... obtendrás un gran error que te recordará... que no puedes imprimir simplemente objetos `DateTime`. ¡Qué mundo más cruel! + +Afortunadamente, Twig tiene un práctico filtro `date`. Ya hablamos brevemente de los filtros en el primer episodio: los utilizamos añadiendo un `|` después de algún valor y el nombre del filtro. Este filtro en particular también toma un argumento, que es el formato en el que debe imprimirse la fecha. Para simplificar las cosas, vamos a utilizar `Y-m-d`, o "año-mes-día". + +[[[ code('cc122a1e01') ]]] + +Ve y actualiza y... ¡bien! Ahora podemos ver cuándo se creó cada uno, aunque el formato no es muy atractivo. Podríamos hacer más trabajo para arreglar esto... pero sería mucho más genial si pudiéramos imprimir esto en el formato "hace". + +Probablemente lo hayas visto antes.... como en los comentarios de una entrada de blog... dicen algo así como "publicado hace tres meses" o "publicado hace 10 minutos". + +Así que... la pregunta es: ¿Cómo podemos convertir un objeto `DateTime` en ese bonito formato "hace"? Bueno, eso me suena a trabajo y, como he dicho antes, el trabajo en Symfony lo hace un servicio. Así que la verdadera pregunta es: ¿Existe un servicio en Symfony que pueda convertir los objetos `DateTime` al formato "ago"? La respuesta es... no. Pero hay un bundle de terceros que puede darnos ese servicio. + +## Instalación de KnpTimeBundle + +Ve a https://github.com/KnpLabs/KnpTimeBundle. Si miras la documentación de este bundle, verás que nos proporciona un servicio que puede hacer esa conversión. Así que... ¡vamos a instalarlo! + +Desplázate hasta la línea `composer require`, cópiala, gira a nuestro terminal y pégala. + +```terminal-silent +composer require knplabs/knp-time-bundle +``` + +¡Genial! Esto agarró `knplabs/knp-time-bundle`... así como `symfony/translation`: el componente de traducción de Symfony, que es una dependencia de `KnpTimeBundle`. Cerca de la parte inferior, también configuró dos recetas. Veamos qué hacen. Ejecuta: + +```terminal +git status +``` + +¡Impresionante! Cada vez que instales un paquete de terceros, Composer siempre modificará tus archivos `composer.json` y `composer.lock`. Esto también ha actualizado el archivo`config/bundles.php`: + +[[[ code('35747b5619') ]]] + +Eso es porque acabamos de instalar un bundle - `KnpTimeBundle` - y su receta se encargó de ello automáticamente. También parece que la receta de traducción añadió un archivo de configuración y un directorio `translations/`. El traductor es necesario para utilizar KnpTimeBundle... pero no necesitaremos trabajar con él directamente. + +Entonces... ¿qué nos ha aportado la instalación de este nuevo bundle? Servicios, por supuesto ¡Busquemos y utilicemos esos a continuación! \ No newline at end of file diff --git a/sfcasts/ep2-fundamentals/es/time-bundle.vtt b/sfcasts/ep2-fundamentals/es/time-bundle.vtt new file mode 100644 index 0000000..df78cd4 --- /dev/null +++ b/sfcasts/ep2-fundamentals/es/time-bundle.vtt @@ -0,0 +1,263 @@ +WEBVTT + +00:00:01.026 --> 00:00:04.616 align:middle +En nuestro sitio, puedes crear +tu propia mezcla de vinilo. + +00:00:04.816 --> 00:00:07.686 align:middle +(O eventualmente podrás hacerlo... + +00:00:07.976 --> 00:00:10.156 align:middle +ahora mismo, este botón no hace nada). + +00:00:10.916 --> 00:00:16.306 align:middle +Pero otra gran característica de nuestro sitio es la +posibilidad de explorar las mezclas de otros usuarios. + +00:00:16.946 --> 00:00:23.226 align:middle +Ahora que lo estoy viendo, podría ser útil si +pudiéramos ver cuándo se creó cada mezcla. + +00:00:24.176 --> 00:00:29.266 align:middle +Si no recuerdas en qué parte de nuestro código +se creó esta página, puedes utilizar un truco. + +00:00:29.976 --> 00:00:34.256 align:middle +Abajo, en la barra de herramientas de depuración de la +web, pasa el ratón por encima del código de estado 200. + +00:00:34.876 --> 00:00:42.006 align:middle +Ah, ¡ja! Esto nos muestra que el controlador +detrás de esta página es VinylController::browse. + +00:00:42.756 --> 00:00:47.366 align:middle +¡Genial! Ve a abrir +src/Controller/VinylController.php. + +00:00:47.366 --> 00:00:48.986 align:middle +Aquí está la acción browse: + +00:00:49.746 --> 00:00:54.256 align:middle +Por cierto, he actualizado un poco el +código desde el primer episodio... + +00:00:54.456 --> 00:00:58.986 align:middle +así que asegúrate de tener una copia +fresca si estás codificando conmigo. + +00:00:59.846 --> 00:01:02.406 align:middle +Este método llama a $this->getMixes()... + +00:01:02.746 --> 00:01:05.756 align:middle +que es una función privada que +he creado en la parte inferior: + +00:01:06.376 --> 00:01:08.846 align:middle +Esto devuelve una gran matriz de datos falsos + +00:01:09.216 --> 00:01:12.766 align:middle +que representa las mezclas que +vamos a representar en la página. + +00:01:13.386 --> 00:01:17.856 align:middle +Al final, lo obtendremos de una fuente +dinámica, como una base de datos. + +00:01:18.676 --> 00:01:22.036 align:middle +Observa que cada mezcla tiene +un campo de fecha createdAt. + +00:01:22.776 --> 00:01:24.886 align:middle +Obtenemos estas mezclas en browse()... + +00:01:25.356 --> 00:01:31.046 align:middle +y las pasamos como una variable +mixes a vinyl/browse.html.twig. + +00:01:31.706 --> 00:01:33.516 align:middle +Vamos a entrar en esa plantilla. + +00:01:34.346 --> 00:01:38.886 align:middle +Aquí abajo, utilizamos el bucle +for de Twig para recorrer mixes. + +00:01:39.276 --> 00:01:40.196 align:middle +¡Es muy sencillo! + +00:01:40.946 --> 00:01:43.766 align:middle +Ahora imprimamos también la fecha "creada en". + +00:01:44.366 --> 00:01:49.906 align:middle +Añade un |, otro y +luego digamos {{ mix.createdAt }}. + +00:01:50.546 --> 00:01:52.186 align:middle +Sólo hay un problema. + +00:01:52.766 --> 00:01:54.096 align:middle +Si miras createdAt... + +00:01:54.626 --> 00:01:56.676 align:middle +es un objeto DateTime. + +00:01:56.676 --> 00:01:59.946 align:middle +Y no puedes imprimir los objetos DateTime... + +00:02:00.406 --> 00:02:02.836 align:middle +obtendrás un gran error que te recordará... + +00:02:03.006 --> 00:02:05.806 align:middle +que no puedes imprimir +simplemente objetos DateTime. + +00:02:06.136 --> 00:02:07.186 align:middle +¡Qué mundo más cruel! + +00:02:08.146 --> 00:02:11.176 align:middle +Afortunadamente, Twig tiene +un práctico filtro date. + +00:02:11.886 --> 00:02:16.926 align:middle +Ya hablamos brevemente de los filtros en el +primer episodio: los utilizamos añadiendo un | + +00:02:16.926 --> 00:02:20.626 align:middle +después de algún valor +y el nombre del filtro. + +00:02:21.146 --> 00:02:27.056 align:middle +Este filtro en particular también toma un argumento, +que es el formato en el que debe imprimirse la fecha. + +00:02:27.896 --> 00:02:33.626 align:middle +Para simplificar las cosas, vamos a +utilizar Y-m-d, o "año-mes-día". + +00:02:34.476 --> 00:02:36.426 align:middle +Dirígete y actualiza y... + +00:02:36.936 --> 00:02:44.206 align:middle +¡bien! Ahora podemos ver cuándo se creó cada +uno, aunque el formato no es muy atractivo. + +00:02:45.176 --> 00:02:48.036 align:middle +Podríamos hacer más +trabajo para arreglar esto... + +00:02:48.506 --> 00:02:53.946 align:middle +pero sería mucho mejor si pudiéramos +imprimir esto en el formato "hace". + +00:02:54.586 --> 00:02:55.926 align:middle +Probablemente lo hayas visto antes.... + +00:02:56.266 --> 00:02:58.526 align:middle +como en los comentarios +de una entrada del blog... + +00:02:59.106 --> 00:03:04.216 align:middle +dicen algo así como "publicado hace tres +meses" o "publicado hace 10 minutos". + +00:03:05.166 --> 00:03:12.526 align:middle +Así que... la pregunta es: ¿Cómo podemos convertir +un objeto DateTime en ese bonito formato "hace"? + +00:03:13.306 --> 00:03:22.076 align:middle +Bueno, eso me suena a trabajo y, como he dicho +antes, el trabajo en Symfony lo hace un servicio. + +00:03:22.566 --> 00:03:26.976 align:middle +Así que la verdadera pregunta es: +¿Existe un servicio en Symfony + +00:03:27.146 --> 00:03:30.476 align:middle +que pueda convertir los objetos +DateTime al formato "ago"? + +00:03:31.116 --> 00:03:32.346 align:middle +La respuesta es... + +00:03:32.426 --> 00:03:37.826 align:middle +no. Pero hay un bundle de terceros +que puede darnos ese servicio. + +00:03:38.776 --> 00:03:42.656 align:middle +Ve a https://github.com/KnpLabs/KnpTimeBundle. + +00:03:43.436 --> 00:03:45.446 align:middle +Si miras la documentación de este bundle, + +00:03:45.756 --> 00:03:49.826 align:middle +verás que nos da un servicio +que puede hacer esa conversión. + +00:03:50.246 --> 00:03:52.416 align:middle +Así que... ¡vamos a instalarlo! + +00:03:53.046 --> 00:03:58.816 align:middle +Desplázate hasta la línea composer require, +cópiala, gira a nuestro terminal y pégala. + +00:04:00.576 --> 00:04:04.056 align:middle +¡Genial! Esto agarró +knplabs/knp-time-bundle... + +00:04:04.426 --> 00:04:09.466 align:middle +así como symfony/translation: El +componente de traducción de Symfony, + +00:04:09.956 --> 00:04:13.016 align:middle +que es una dependencia de KnpTimeBundle. + +00:04:13.866 --> 00:04:18.116 align:middle +Cerca de la parte inferior, +también configuró dos recetas. + +00:04:18.876 --> 00:04:19.796 align:middle +Veamos lo que hicieron. + +00:04:20.316 --> 00:04:22.986 align:middle +Ejecuta: git status +¡Impresionante! Cada vez que + +00:04:23.486 --> 00:04:26.016 align:middle +instales un paquete de terceros, Composer + +00:04:26.276 --> 00:04:31.946 align:middle +siempre modificará tus archivos +composer.json y composer.lock. + +00:04:32.516 --> 00:04:36.556 align:middle +Esto también actualizó el +archivo config/bundles.php: Eso es + +00:04:37.476 --> 00:04:41.436 align:middle +porque acabamos de instalar +un bundle - KnpTimeBundle - + +00:04:41.906 --> 00:04:45.016 align:middle +y su receta se encargó +de ello automáticamente. + +00:04:45.846 --> 00:04:51.786 align:middle +También parece que la receta de traducción añadió un +archivo de configuración y un directorio translations/. + +00:04:52.716 --> 00:04:55.716 align:middle +El traductor es necesario +para utilizar KnpTimeBundle... + +00:04:55.926 --> 00:04:58.216 align:middle +pero no necesitaremos +trabajar con él directamente. + +00:04:59.176 --> 00:05:03.066 align:middle +Entonces... ¿qué nos ha aportado +la instalación de este nuevo bundle? + +00:05:03.646 --> 00:05:04.966 align:middle +Servicios, por supuesto + +00:05:05.476 --> 00:05:07.916 align:middle +¡Busquemos y utilicemos esos a continuación! diff --git a/sfcasts/ep2-fundamentals/global-bind.md b/sfcasts/ep2-fundamentals/global-bind.md new file mode 100644 index 0000000..8356255 --- /dev/null +++ b/sfcasts/ep2-fundamentals/global-bind.md @@ -0,0 +1,74 @@ +# Bind Arguments Globally + +In practice, you rarely need to do anything inside of `services.yaml`. Most of the +time, when you add an argument to the constructor of a service, it's autowireable. +So you add the argument, give it a type-hint... and *keep* coding! + +[[[ code('c687e6b377') ]]] + +But the `$isDebug` argument is *not* autowireable... since it's not a service. And +*that* forced us to *completely* override the service so we could specify that +*one* argument with `bind`. It works but... that was... kind of a lot of typing +to do such a small thing! + +[[[ code('d26ed5a862') ]]] + +# Moving bind to `_defaults` + +So here's a *different* solution. Copy that `bind` key, delete the service entirely, +and up, under `_defaults`, paste: + +[[[ code('b8c74cd912') ]]] + +When we move over and try this... the page *still* works! How cool is that? And, +it makes sense. This section will automatically register `MixRepository` as a +service... and then anything under `_defaults` will be applied *to* that service. +So the end result is exactly what we had before. + +I *love* doing this! It allows me to set up project-wide conventions. Now that we +have this, we could add an `$isDebug` argument to the constructor of *any* service +and it will instantly work. + +## Binding with Type_hints + +By the way, if you want, you can *also* include the *type* with the bind. + +So this would now *only* work if we use the `bool` type-hint with the argument: + +[[[ code('c3d834b446') ]]] + +If we used `string`, for example, Symfony would *not* try to pass in that value. + +## The Autowire Attribute + +So the global bind is *awesome*. But starting in Symfony 6.1, there's *another* +way to specify a non-autowireable argument. Comment out the global `bind`. I *do* +still like doing this... but let's try the new way: + +[[[ code('a28f96d46a') ]]] + +If we refresh now, we get an error because Symfony doesn't know what to pass to +the `$isDebug` argument. To fix that, go into `MixRepository` and, above the argument +(or before the argument if you're not using multiple lines), add a PHP 8 attribute +called `Autowire`. Normally, PHP 8 attributes will auto-complete, but this *isn't* +auto-completing for me. That's actually due to a bug in PhpStorm. To get around this, +I'm going to type out `Autowire`... then go to the top and start adding the `use` +statement for this manually, which *does* give us an option to auto-complete. Hit +"tab" and... *tah dah*! If you want to make them alphabetical, you can move it around. + +You may also notice that it's underlined with a message: + +> Attribute cannot be applied to a property [...] + +Again, PhpStorm is a bit confused because this is both a property *and* an argument. + +Anyway, go ahead and pass this an argument `%kernel.debug%`: + +[[[ code('080c22c394') ]]] + +Refresh now and... got it! Pretty cool, right? + +Next: most of the time when you autowire an argument like `HttpClientInterface`, +there's only *one* service in the container that implements that interface. But what +if there were *multiple* HTTP clients in our container? How could we choose the +one we want? It's time to talk about *named autowiring*. diff --git a/sfcasts/ep2-fundamentals/http-client.md b/sfcasts/ep2-fundamentals/http-client.md new file mode 100644 index 0000000..f373e30 --- /dev/null +++ b/sfcasts/ep2-fundamentals/http-client.md @@ -0,0 +1,122 @@ +# The HTTP Client Service + +We don't have a database yet... and we'll save that for a *future* tutorial. But +to make things a bit more fun, I've created a GitHub repository - +https://github.com/SymfonyCasts/vinyl-mixes - with a `mixes.json` file that holds +a *fake* database of vinyl mixes. Let's make an HTTP request from our Symfony +app *to* this file and use *that* as our temporary database. + +So... how *can* we make HTTP requests in Symfony? Well, making an HTTP request is +*work*, and - say it with me now - "Work is done by a service". So the next question +is: Is there already a service in our app that can make HTTP requests? + +Let's find out! Spin over to your terminal and run: + +```terminal +php bin/console debug:autowiring http +``` + +to search the services for "http". We *do* get a bunch of results, but... nothing +that looks like an HTTP client. And, that's correct. There is *not* currently any +service in our app that can make HTTP requests. + +## Installing the HTTPClient Component + +*But*, we can install *another* package to give us one. At your terminal, type: + +```terminal +composer require symfony/http-client +``` + +*But*, before we run that, I want to show you *where* this command comes from. Search +for "symfony http client". One of the top results is Symfony.com's documentation +that teaches about an HTTP Client component. Remember: Symfony is a collection +of *many* different libraries, called components. And *this* one helps us make +HTTP requests! + +Near the top, you'll see a section called "Installation", and *there's* the line +from our terminal! + +*Anyways*, if we run that... cool! Once it finishes, try that `debug:autowiring` +command again: + +```terminal-silent +php bin/console debug:autowiring http +``` + +And... here it is! Right at the bottom: `HttpClientInterface`, which + +> Provides flexible methods for requesting HTTP resources synchronously or +> asynchronously. + +## The Super Smart FrameworkBundle + +Woo! We just got a new service! *That* means that we must have just installed a +new bundle, right? Because... bundles give us services? Well... go check out +`config/bundles.php`: + +[[[ code('31a0dbdbc7') ]]] + +Woh! There's *no* new bundle here! Try running + +```terminal +git status +``` + +Yea... that *only* installed a normal PHP package. Inside `composer.json`, here's +the new package... But it's *just* a "library": not a *bundle*. + +[[[ code('d946582375') ]]] + +So, *normally*, if you install "just" a PHP library, it gives you PHP classes, but +it doesn't hook into Symfony to give you new *services*. What we just saw is a +special trick that many of the Symfony components use. The *main* bundle in our +app is `framework-bundle`. In fact, when we started our project, this was the +*only* bundle we had. `framework-bundle` is *smart*. When you install a new +Symfony component - like the HTTP Client component - that bundle *notices* the +new library and automatically adds the services for it. + +So the new service comes from `framework-bundle`... which adds that as soon as it +detects that the `http-client` package is installed. + +## Using the HttpClientInterface service + +Anyways, time to use the new service. The type we need is `HttpClientInterface`. +Head over to `VinylController.php` and, up here in the `browse()` action, +autowire `HttpClientInterface` and let's name it `$httpClient`: + +[[[ code('f67b91f6c3') ]]] + +Then, instead of calling `$this->getMixes()`, say `$response = $httpClient->`. +This lists all of its methods... we *probably* want `request()`. Pass this `GET`... +and then I'll paste the URL: you can copy this from the code block on this page. +It's a direct link to the content of the `mixes.json` file: + +[[[ code('f33fdc893f') ]]] + +Cool! So we make the request and it returns a response containing the `mixes.json` +data that we see here. Fortunately, this data has all of the same keys as the +old data we were using down here... so we should be able to swap it in super easily. +To get the mix data from the response, we can say `$mixes = $response->toArray()`: + +[[[ code('b6660098c4') ]]] + +a handy method that JSON decodes the data for us! + +Moment of truth! Move over, refresh and... it works! We now have *six* mixes on +the page. And... super cool! A new icon showed up on the web debug toolbar: "Total +requests: 1". The HTTP Client service hooks *into* the web debug toolbar to add +this, which is pretty awesome. If we click it, we can see info about the request +and the response. I *love* that. + +To celebrate this working, spin back over and remove the hardcoded `getMixes()` +method: + +[[[ code('fe38cc18e5') ]]] + +The only problem I can think of now is that, every single time someone visits our +page, we're making an HTTP request to GitHub's API... and HTTP requests are slow! +To make matters worse, once our site becomes super popular - which won't take +long - GitHub's API will probably start rate limiting us. + +To fix this, let's leverage *another* Symfony service: the cache service. diff --git a/sfcasts/ep2-fundamentals/maker-command.md b/sfcasts/ep2-fundamentals/maker-command.md new file mode 100644 index 0000000..dd61af8 --- /dev/null +++ b/sfcasts/ep2-fundamentals/maker-command.md @@ -0,0 +1,99 @@ +# MakerBundle & Autoconfiguration + +Congrats, team! We are *done* with the heavy stuff in this tutorial! So it's time +for a victory lap. Let's install one of my *favorite* Symfony bundles: MakerBundle. +Find your terminal and run: + +```terminal +composer require maker --dev +``` + +In this case, I'm using the `--dev` flag because this is a code generation utility +that we only need locally, *not* on production. + +This bundle, of course, provides services. But these services aren't really meant +for us to use *directly*. Instead, all of the services from this bundle power a +bunch of new `bin/console` commands. Run + +```terminal +php bin/console +``` + +and look for the `make` section. Ooh. There's a ton of stuff here for setting up +security, generating doctrine entities for the database (which we'll do in the next +tutorial), making a CRUD, and much more. + +## Generating a new Command Class + +Let's try one: how about we try to build our *own* new custom console command +that will appear in this list. To do that, run: + +```terminal +php bin/console make:command +``` + +This will interactively ask you for the name of the command. Let's say +`app:talk-to-me`. You don't *have* to, but it's pretty common to prefix your custom +commands with `app:`. And... done! + +That created exactly *one* new file: `src/Command/TalkToMeCommand.php`. Let's go +open that up: + +[[[ code('01fe532ac9') ]]] + +Cool! On top, you can see that the name and description of the +command are done in a PHP attribute! Then, down in this `configure()` method, +which we'll talk about more in a minute, we can configure arguments and options +that can be passed from the command line. + +When we *run* the command, `execute()` will be called... where we can print things +out to the screen or read options and arguments. + +Perhaps the *best* thing about this class is that... it *already* works. Check it +out! Back at your terminal, run; + + +```terminal +php bin/console app:talk-to-me +``` + +And... it's *alive*! It doesn't *do* much, but this output is coming from down +here. Woo! + +## Autoconfiguration: Auto Discovering "Plugins" + +But wait... how *did* Symfony instantly see our new `Command` class and know to +start using it? Is it because it lives in the `src/Command/` directory... and +Symfony scans for classes that live here? Nope! We could rename this directory to +`ThereAreDefinitelyNoCommandsInHere`... and Symfony would *still* see the command. + +The way this works is *much* cooler. Open up `config/services.yaml` and +look at the `_defaults` section: + +[[[ code('99a7530291') ]]] + +We talked about what `autowire: true` means, but I didn't explain the purpose of +`autoconfigure: true`. Because this is below `_defaults`, autoconfiguration *is* +active on all of our services, including our new `TalkToMeCommand` service. +When `autoconfiguration` is enabled, it basically tells Symfony: + +> Hey, please look at the base class or interface of each service, and if it +> *looks* like a class should be a console command... or an event subscriber... +> or any other class that hooks into a part of Symfony, please automatically +> integrate the service *into* that system. Okay, thanks. Bye! + +Yep! Symfony sees that our class extends `Command` and thinks: + +> Hmm, I may not be a self-aware AI... but I bet this is a command. I better notify +> the console system about it! + +I *love* autoconfiguration. It means that we can create a PHP class, extend +whatever base class or implement whatever interface needed for the "thing" that +we're building, and... it will just *work*. + +Internally, if you want all the nerdy details, autoconfiguration adds a *tag* to +your service, like `console.command`, which is what ultimately helps it get noticed +by the console system. + +All right, now that our command is working, let's have some fun and customize it +*next*. diff --git a/sfcasts/ep2-fundamentals/metadata.yml b/sfcasts/ep2-fundamentals/metadata.yml new file mode 100644 index 0000000..b669da8 --- /dev/null +++ b/sfcasts/ep2-fundamentals/metadata.yml @@ -0,0 +1,26 @@ +chapters: + - bundles + - time-bundle + - bundle-services + - http-client + - cache-service + - bundle-config + - cache-config + - debug-container + - environments + - prod-environment + - create-service + - dependency-injection + - parameters + - service-config + - services-yaml + - global-bind + - named-autowiring + - non-autowireable-services + - controllers-services + - environment-variables + - secrets-vault + - secrets-usage + - maker-command + - custom-command + - command-extra diff --git a/sfcasts/ep2-fundamentals/named-autowiring.md b/sfcasts/ep2-fundamentals/named-autowiring.md new file mode 100644 index 0000000..6b4d4b5 --- /dev/null +++ b/sfcasts/ep2-fundamentals/named-autowiring.md @@ -0,0 +1,82 @@ +# Named Autowiring & Scoped HTTP Clients + +In `MixRepository`, it would be cool if we didn't need to specify the host name +when we make the HTTP request. Like, it'd be great if that were preconfigured +and we only needed to include the path. *Also*, pretty soon, we're going to configure +an access token that will be used when we make requests to the GitHub API. We could +pass that access token manually here in our service, but how cool would it be if +the HttpClient service came preconfigured to always include the access token? + +So, *does* Symfony have a way for us to, sort of, "preconfigure" the HttpClient +service? It *does*! It's called "scoped clients": a feature of HttpClient where +you can create multiple HttpClient services, each preconfigured *differently*. + +## Creating a Scoped Client + +Here's how it works. Open up `config/packages/framework.yaml`. To create a scoped +client, under the `framework` key, add `http_client` followed by `scoped_clients`. +Now, give your scoped client a name, like `githubContentClient`... since we're using +a part of their API that returns the content of files. Also add `base_uri`, go copy +the host name over here... and paste: + +[[[ code('ebc7952f44') ]]] + +Remember: the purpose of these config files is to *change* the services in the +container. The end result of this new code is that a *second* HttpClient service +will be added to the container. We'll see that in a minute. And, by the way, there's +no way that you could just *guess* that you need `http_client` and `scoped_clients` +keys to make this work. Configuration is the kind of thing where you really need +to rely on the documentation. + +Anyways, now that we've preconfigured this client, we should be able to go into +`MixRepository` and make a request directly to the path: + +[[[ code('2f3d36b908') ]]] + +But if we head over and refresh... ah... + +> Invalid URL: scheme is missing [...]. Did you forget to add "http(s)://"? + +I didn't *think* we forgot... since we configured it via the `base_uri` option... +but apparently that didn't work. And you may have guessed why. Find your terminal +and run: + +```terminal +php bin/console debug:autowiring client +``` + +There are now *two* HttpClient services in the container: The normal, +non-configured one and the one that *we* just configured. Apparently, in +`MixRepository`, Symfony is still passing us the *unconfigured* HttpClient service. + +How can I be sure? Well, think back to how autowiring works. Symfony looks at the +type-hint of our argument, which is +`Symfony\Contracts\HttpClient\HttpClientInterface`, and then looks in the container +to find a service whose ID is an exact match. It's *that* simple + +## Fetching the Named Version of a Service + +So... if there are multiple services with the same "type" in our container, is only +the *main* one autowireable? Fortunately, no! We can use something called "named +autowiring"... and it's already showing us how. If we type-hint an argument with +`HttpClientInterface` *and* *name* the argument `$githubContentClient`, Symfony +will pass us the second one. + +Let's try it: change the argument from `$httpClient` to `$githubContentClient`: + +[[[ code('319f5e30f8') ]]] + +and now... it doesn't work. Whoops... + +> Undefined property: `MixRepository::$httpClient` + +That's... just me being careless. When I changed the argument name, it changed +the property name. So... we need to adjust the code below: + +[[[ code('2764098093') ]]] + +And now... it's alive! We just autowired a *specific* HttpClientInterface service! + +Next, let's tackle another tricky problem with autowiring by learning how to fetch +one of the *many* services in our container that is totally *not* available for +autowiring. diff --git a/sfcasts/ep2-fundamentals/non-autowireable-services.md b/sfcasts/ep2-fundamentals/non-autowireable-services.md new file mode 100644 index 0000000..f224b74 --- /dev/null +++ b/sfcasts/ep2-fundamentals/non-autowireable-services.md @@ -0,0 +1,122 @@ +# Non-Autowireable Services + +Run: + +```terminal +php bin/console debug:container +``` + +And... I'll make this a bit smaller so that everything shows up on one line. As we +know, this command shows *all* of the services in our container... but only a *small* +number of these are autowireable. We know that because a service is autowireable +*only* if its ID, which is this over here, is a class or interface name. + +So at first, it might look like the Twig service is *not* autowireable. After all, +its ID - `twig` - is definitely *not* a class or interface. But if you scroll up +to the top... let's see... yep! There's another service in the container whose ID +is `Twig\Environment`, which is an *alias* to the service `twig`. This is a little +trick Symfony does to make services autowireable. If we type-hint an argument +with `Twig\Environment`, we get the `twig` service. + +However, most of services in this list do *not* have an alias like that. So they +are *not* autowireable. And, that's *usually* fine. If a service *isn't* autowireable, +it's probably because you'll never need to use it. *But* let's pretend that we *do* +want to use one of these. + +Check this one out! It's called `twig.command.debug`. Open up another tab. +Earlier, we ran: + +```terminal +php bin/console debug:twig +``` + +This shows us *all* of the functions and filters from Twig... which is nice! +Well, surprise! This command *comes* from the `twig.command.debug` service! Because, +"everything in Symfony is done by a service" - even console commands. + +As a challenge, let's see if we can inject this service into `MixRepository`, +execute it, and dump its output. + +## Dependency Injection: Adding the new Argument + +First things first. In `MixRepository`, we just discovered that, in order to do our +work, we need access to another service. What do we do? The answer: *Dependency +injection*, which is that fancy word for adding another construct argument and +setting it onto a property, which we can do all at once with +`private $twigDebugCommand`: + +[[[ code('db9765d0f6') ]]] + +If we stopped right now and refreshed... no surprise! We get an error. Symfony has +no idea what to pass for that argument. + +What if we added the *type* for this class? Back over in our terminal, we can see +that this service is an instance of `DebugCommand`. Over here, let's add that +type-hint: `DebugCommand`... we want the one from `Symfony\Bridge\Twig\Command`. +Hit "tab" to autocomplete that: + +[[[ code('1d003f5f47') ]]] + +And then... refresh. *Still* an error! Okay, we *should* add the type-hint because +we're good programmers. But... no matter how hard we try, this is *not* an autowireable +service. So, how do we fix this? + +## Binding the Argument in YAML + +There are two main ways. I'll show you the *old* way first, which I'm *mostly* doing +because you'll see it in documentation and blog posts all over the place. In +`config/services.yaml`, just like we did earlier for the `$isDebug` argument, +override our service entirely. Say `App\Service\MixRepository`, and add a +`bind` key. Then, we're going to hint what to pass to the `$twigDebugCommand` argument. + +The *only* tricky thing is figuring out what *value* to set. For example, if I go +and copy the service ID - `twig.command.debug` - and paste that here... that's *not* +going to work! That's literally going to pass that *string*. If you refresh, +yup! + +> Argument 4 must be of type `DebugCommand`, string given. + +We need to tell Symfony to pass the *service* that has this ID. In these YAML +files, there's a special syntax to do just that: *prefix* the service ID with the +`@` symbol: + +[[[ code('447697206a') ]]] + +As *soon* as we do that... the fact that this doesn't explode means it's working! + +## The Autowire Attribute + +*But*... let's remove this. Because I want to show you the *new* way do this... +which leverages that same fancy `Autowire` attribute. + +Up here, say `#[Autowire()]`, but instead of just passing a string, say +`service: 'twig.command.debug'`: + +[[[ code('b2b20613e3') ]]] + +## Using the new Argument + +I love that! Before we try this, let's actually *use* the service. Head down to +`findAll()`. Executing a console command *manually* in your PHP code is totally +possible. It's a little weird, but cool! We need to create an +`$output = new BufferedOutput()` object... then we can execute the command by saying +`$this->twigDebugCommand->run(new ArrayInput())` - this is, sort of, *faking* +the command-line arguments - pass that an empty `[]` - then `$output`. Whatever +the command *outputs* will be set onto that object. + +To see if it's working, just `dd($output)`: + +[[[ code('d77a8d195f') ]]] + +Testing time! Refresh... and got it! How fun is that? + +All right, now that this is working, let's comment out this silliness. I'll keep our +`$twigDebugCommand` injected just for reference. + +The *key* takeaway is this: *most* arguments to services will be autowireable. Yay! +But when you hit an argument that is *not* autowireable, you can use the `Autowire` +attribute to point to the value or service you need. + +Next: Remember when I told you that `MixRepository` was the first service we ever +created? Well... I lied. It turns out that our *controllers* have been services this +whole time! diff --git a/sfcasts/ep2-fundamentals/parameters.md b/sfcasts/ep2-fundamentals/parameters.md new file mode 100644 index 0000000..3434fa9 --- /dev/null +++ b/sfcasts/ep2-fundamentals/parameters.md @@ -0,0 +1,141 @@ +# Parameters + +We know there's this *container* concept that holds all of our services... and we +can see the full list of services by running: + +```terminal +php bin/console debug:container +``` + +## Listing Parameters + +Well, it turns out that the container holds one *other* thing: grudges. Seriously, +don't expect to pull a prank on the service container and get away with it. + +Ok, what it *really* holds, in addition to services, is *parameters*. These +are simple configuration values, and we can see them by running a similar command: + +```terminal +php bin/console debug:container --parameters +``` + +These are basically variables that you can read and reference in your code. We +don't need to worry about most of these, actually. They're *set* by internal things +and *used* by internal things. But there *are* a few that start with `kernel` +that are pretty interesting, like `kernel.project_dir`, which points to the +*directory* of our project. Yep! If you ever need a way to refer to the directory +of your app, *this* parameter can help. + +## Fetching Parameters from a Controller + +So... how do we *use* these parameters? There are two ways. First, it's not super +common, but you *can* fetch a parameter in your controller. For example, +in `VinylController`, let's `dd($this->getParameter())` - which is a shortcut +method from `AbstractController` - and then `kernel.project_dir`. We even get some +nice auto-completion thanks to the Symfony PhpStorm plugin! + +[[[ code('ef8f6d1274') ]]] + +And when we try it... yep! There it is! + +## Referencing Parameters with %parameter% + +Now... delete that. This *works*, but most of the time, the way you'll use parameters +is by referencing them in your *configuration files*. And we've seen this before! +Open up `config/packages/twig.yaml`: + +[[[ code('852f378720') ]]] + +Remember that `default_path`? That's referencing the `kernel.project_dir` parameter. +When you're in *any* of these `.yaml` configuration files and you want to reference +a parameter, you can use this special syntax: `%`, the name of the parameter, +then another `%`. + +## Creating a new Parameter + +Open up `cache.yaml`. We're setting `cache.adapter` to `filesystem` for *all* +environments. Then, we're overriding it to be the `array` adapter in the dev +environment only. Let's see if we can shorten this by creating a *new* parameter. + +How *do* we create parameters? In any of these files, add a root key called +`parameters`. Below that, you can just... invent a name. I'll call it `cache_adapter`, +and set that to our value: `cache.adapter.filesystem`: + +[[[ code('88f00b7bd8') ]]] + +If you have a root `framework` key, Symfony will pass all of the config to +FrameworkBundle. The same is true with the `twig` key and TwigBundle. + +But `parameters` is special: anything under this will create a *parameter*. + +So yea... we now have a new `cache.adapter` parameter... that we're not actually +*using* yet. But we can already *see* it! Run: + +```terminal +php bin/console debug:container --parameters +``` + +Near the top... there it is - `cache_adapter`! To use this, down here for `app`, +say `%cache_adapter%`: + +[[[ code('9060cf5eae') ]]] + +That's *it*. Quick note: You may have noticed that *sometimes* I use quotes in +YAML and *sometimes* I *don't*. Mostly, in YAML, you don't need to use quotes... +but you always *can*. And if you're ever not sure if they're needed or not, better +to be safe and *use* them. + +Parameters *are* actually one example where quotes are *required*. If we didn't +surround this with quotes, it would look like a special YAML syntax and throw +an error. + +Anyway, in the `dev` environment, instead of saying `framework`, `cache`, and `app`, +all we need to do is override that parameter. I'll say `parameters`, then +`cache_adapter`... and set it to `cache.adapter.array`: + +[[[ code('e3fea12cdb') ]]] + +To see if that's working, spin over here and run another helper command: + +```terminal +php bin/console debug:config framework cache +``` + +Remember, `debug:config` will show you what your *current* configuration is under the +`framework` key, and then the `cache` sub-key. And you can see here that `app` is +set to `cache.adapter.array` - the resolved value for the parameter. + +Let's check the value in the prod environment... just to make sure it's right there +too. When you run any `bin/console` command, that command will execute in the same +environment your app is running in. So when we ran `debug:config`, that ran in the +*dev* environment. + +To run the command in the *prod* environment, we *could* go over here and change +`APP_ENV` to `prod` temporarily... but there's an *easier* way. You can *override* +the environment when running any command by adding a flag at the end. For example: + +```terminal +php bin/console debug:config framework cache --env=prod +``` + +But before we try that, we always need to clear our cache first to see changes in +the `prod` environment. Do *that* by running: + +```terminal +php bin/console cache:clear --env=prod +``` + +*Now* try: + +```terminal +php bin/console debug:config framework cache --env=prod +``` + +And... beautiful! It shows `cache.adapter.filesystem`. So, the container *also* holds +parameters. This isn't a *super* important concept in Symfony, so, as long as you +understand how they work, you're good. + +Ok, let's turn back to dependency injection. We know that we can autowire services +into the constructor of a service or into controller methods. But what if we need +to pass something that's *not* autowireable? Like, what if we wanted to pass one +of these *parameters* to a service? Let's find out how that works *next*. diff --git a/sfcasts/ep2-fundamentals/prod-environment.md b/sfcasts/ep2-fundamentals/prod-environment.md new file mode 100644 index 0000000..2f0bf91 --- /dev/null +++ b/sfcasts/ep2-fundamentals/prod-environment.md @@ -0,0 +1,113 @@ +# The "prod" Environment + +Our app is currently running in the `dev` environment. Let's switch it to `prod`... +which is what you would use on production. Temporarily change `APP_ENV=dev` to +`prod`: + +[[[ code('0ad4cd6444') ]]] + +then head over and refresh. Whoa! The web debug toolbar is *gone*. +That... makes sense! The entire web profiler bundle is *not* enabled in the `prod` +environment. + +You'll also notice that the dump from our controller appears on the *top* +of the page. The web profiler normally *captures* that and displays it down on the +web debug toolbar. But... since that whole system isn't enabled anymore, it now dumps +right where you call it. + +And there are a lot of other differences, like the logger, which now behaves +differently thanks to the configuration in `monolog.yaml`. + +## Clearing the prod Cache + +The way pages are built has also changed. For example, Symfony caches a lot of files... +but you don't notice that in the `dev` environment. That's because Symfony is super +smart and rebuilds that cache automatically when we change certain files. *However*, +in the `prod` environment, that doesn't happen. + +Check it out! Open up `templates/base.html.twig`... and change the title on the +page to `Stirred Vinyl`. If you go back over and refresh... look up here! *No change*! +The Twig templates *themselves* are cached. In the `dev` environment, Symfony rebuilds +that cache *for* us. But in the `prod` environment? Nope! We need to clear it +manually. + +How? At your terminal, run: + +```terminal +php bin/console cache:clear +``` + +Notice it says that it's clearing the cache for the *prod* environment. So, just +like how our *app* always runs in a specific environment, the console commands +*also* run in a specific environment. And, it reads that same `APP_ENV` flag. So +because we have `APP_ENV=prod` here, `cache:clear` knew that it should run in the +*prod* environment and clear the cache for *that* environment. + +Thanks to this, when we refresh... *now* the title updates. I'll change this +back to our *cool* name, `Mixed Vinyl`. + +## Changing the Cache Adapter for prod Only + +Let's try something else! Open up `config/packages/cache.yaml`. Our cache service +currently uses the `ArrayAdapter`, which is a fake cache. That might be cool +for development, but it won't be much help on production: + +[[[ code('8b4dd40fad') ]]] + +Let's see if we can switch that back to the filesystem adapter, but *only* for the +`prod` environment. How? Down here, use `when@prod` and then repeat the same keys. +So `framework`, `cache`, and then `app`. Set this to the adapter we want, which is +called `cache.adapter.filesystem`: + +[[[ code('38e1943114') ]]] + +It's going to be *really* easy to see if this works because we're still dumping the +cache service in our controller. Right now, it's an `ArrayAdapter`. If we refresh... +surprise! It's *still* an `ArrayAdapter`. Why? Because we're in the *prod* +environment... and pretty much any time you make a change in the `prod` environment, +you need to rebuild your cache. + +Go back to your terminal and run + +```terminal +php bin console cache:clear +``` + +again and now... got it - `FilesystemAdapter`! + +But... let's *reverse* this config. Copy `cache.adapter.array` and change it to +`filesystem`. We'll use *that* by default. Then at the bottom, change to `when@dev`, +and *this* to `cache.adapter.array`: + +[[[ code('9554ab0ea8') ]]] + +Why am I doing that? Well, that literally makes zero difference in the `dev` and +`prod` environments. *But* if we decide to start writing tests later, which run in +the *test* environment, with this new config, the test environment will use the same +cache service as production... which is probably more realistic and better for testing. + +To make sure this still works, clear the cache one more time. Refresh and... it +does! We still have `FilesystemAdapter`. And... if we switch back to the `dev` +environment in `.env`: + +[[[ code('02a3fa35f7') ]]] + +and refresh... yes! The web debug toolbar is back, and down +here, we are once again using `ArrayAdapter`! + +Now, in reality, you probably won't ever switch to the `prod` environment while +you're developing locally. It's hard to work with... and there's just no point! +The `prod` environment is really meant for production! And so, you *will* run +that `bin/console cache:clear` command during deployment... but probably almost +never on your local machine. + +Before we go on, head into `VinylController`, go down to `browse()`, and take +out that `dump()`: + +[[[ code('51d407c154') ]]] + +Okay, status check! First, *everything* in Symfony is done by a service. Second, +bundles *give* us services. And third, we can control *how* those services are +instantiated via the different bundle configuration in `config/packages/`. + +*Now*, let's go one important step further by creating our *own* service. diff --git a/sfcasts/ep2-fundamentals/secrets-usage.md b/sfcasts/ep2-fundamentals/secrets-usage.md new file mode 100644 index 0000000..dc5f445 --- /dev/null +++ b/sfcasts/ep2-fundamentals/secrets-usage.md @@ -0,0 +1,137 @@ +# Reading Secrets vs Env Vars + +We *just* created a secrets vault for our `dev` environment... which will contain +a default "safe" version of any sensitive environment variables. For example, +we set the `GITHUB_TOKEN` value to `CHANGEME`. + +*Now* let's create the `prod` environment vault. Do that by saying: + +```terminal +./bin/console secrets:set GITHUB_TOKEN --env=prod +``` + +This time, grab the *real* secret value from `.env.local` and paste it here. +Just like before, since there wasn't a `prod` vault already, Symfony *created* +it. And it's got the *same* four files as before. Though, there *is* one subtle, +but important difference. + +Add that new directory to git: + +```terminal-silent +git add config/secrets/prod +``` + +Then run: + +```terminal +git status +``` + +Woh! Only *three* of the four files were added. The *fourth* file - the `decrypt` +key - is *ignored* by Git. We already have a line inside in `.gitignore` for that. +We do *not* want to commit the `prod` decrypt key to the repository... because +anyone that has this key will be able to read *all* of our secrets. + +So, if another developer pulls down the project now, they *will* have the `dev` +decrypt key, so they'll have no problems reading values from the `dev` vault. +They won't have the `prod` decrypt key... but no big deal! The only place where +you need the `prod` decrypt key is on production! + +So with this setup, when you deploy, instead of needing to create an entire +`.env.local` file containing *all* of your secrets, you just need to worry about +getting this *one* `prod.decrypt.private.php` file up into your code. Or, +alternatively, you can read this key and set it on an environment variable: +you can check the docs for details on how. + +## Using The Secrets Vault + +But... wait a second. I haven't really explained *how* the vault is used! We know +that the `dev` environment will use the `dev` vault... and `prod` will use +`prod`... but how do we *read* secrets out of the vault? + +The answer is... we already *are*! Secrets *become* environment variables. It's +as simple as that! So in `config/packages/framework.yaml`, by using this `env` +syntax, this `GITHUB_TOKEN` could be a *real* environment variable, or it could +be a *secret* in our vault. + +To see if this is working, head to `MixRepository` and +`dd($this->githubContentClient)`: + +[[[ code('53828c9ece') ]]] + +Move over, refresh, and... let's see if we can find the Authorization header in +this. Actually, there's a really cool trick with dump. Click on this area and +hold "command" or "control" + "F" to search inside of it. Search for the word +"token" and... oh, that's not right! That's our *real* token. But... since +we're in the *dev* environment, shouldn't it be reading our *dev* vault where we +set the fake `CHANGEME` value? What's going on? + +## Secrets Must Fully Be Converted Away from Env Vars + +As I mentioned, secrets become environment variables. *But* environment variables +take *precedence* over secrets: even environment variables defined in the `.env` +files. Yup, because we have a `GITHUB_TOKEN` env var set in `.env` and +`.env.local`, *that* is taking precedence over the value in the vault! + +Here's the point. As soon as you choose to convert a value from an environment +variable into a secret, you need to *stop* setting it as an environment +variable *completely*. In other words, delete `GITHUB_TOKEN` in `.env` and *also* +in `.env.local`. + +Go refresh, click on this again, use "command" + "F", search for "token", and... +got it! We see "CHANGEME"! If we were in the `prod` environment, it would read the +value from the prod vault... assuming the prod decrypt key was available. + +## The secrets:list Command + +Ok, remove that `dd()` and refresh to discover that... locally, everything is +broken! Dang! But... of course! It's now using that *fake* token from the *dev* +vault. It *would* work ok on production... but how can I fix my local setup so +I can keep working? + +We *could* temporarily override the `GITHUB_TOKEN` secret value in the `dev` vault +by running the `secrets:set` command. But... that's lame! We would need to be +extra careful to not commit the modified, encrypted file. + +Before we fix this, I want to show you a really handy command for the vault: + +```terminal +php bin/console secrets:list +``` + +Yup, this shows you all of the secrets in our vault. Pretty cool! And you can even +pass `--reveal` to *reveal* the value... as long as you have the `decrypt` key. + +You may have noticed that it *gives* us the value right here... but then says +"Local Value" with a blank space. Hmm... + +Re-run the command, but this time add `--env=prod`. + +```terminal-silent +php bin/console secrets:list --reveal --env=prod +``` + +And... same thing! This shows us the *real* `prod` value... but there's still +this "Local Value" spot with nothing. + +This "Local Value" is the key to fixing our broken dev setup: it's a way to +override a secret, but only *locally* on our one machine. + +How do you *set* this local override value? Copy the real `GITHUB_TOKEN` value, +then move over, find `.env.local` - the same file we've been working in - and say +`GITHUB_TOKEN=` and paste the value we just copied. + +Yup! Locally, we're going to take advantage of the fact that environment variables +"win" over secrets! Back at your terminal, run + +```terminal +php bin/console secrets:list --reveal +``` + +again. Yes! The *official* value in the vault is "CHANGEME"... but the *local* +value is our *real* token which, as we know, will *override* the secret and be +used. If we try the page again... it works! + +Okay, team! We're... well... *basically* done! So as a reward for your hard work +on these *super* important topics, let's celebrate by using Symfony's code +generator library: MakerBundle. diff --git a/sfcasts/ep2-fundamentals/secrets-vault.md b/sfcasts/ep2-fundamentals/secrets-vault.md new file mode 100644 index 0000000..b86a895 --- /dev/null +++ b/sfcasts/ep2-fundamentals/secrets-vault.md @@ -0,0 +1,126 @@ +# The Secrets Vault + +I don't want to get *too* far into deployment, but let's do a quick "How To Deploy +Your Symfony App 101" course. Here's the idea. + +## Deployment 101 + +Step 1: You need to *somehow* get all of your committed code onto your production +machine and then run + +```terminal +composer install +``` + +to populate the `vendor/` directory. + +Step 2: Somehow create a `.env.local` file with all of your production environment +variables, which will include `APP_ENV=prod`, so that you're in the prod +environment. + +And Step 3: run + +```terminal +php bin/console cache:clear +``` + +which will clear the cache in the production environment, and then + +```terminal +php bin/console cache:warmup +``` + +to "warm up" the cache. There may be a few other commands, like running your +database migrations... but this is the general idea. And the Symfony docs have +more details. + +By the way, in case you're wondering, we deploy via https://platform.sh, using +Symfony's Cloud integration... which handles *a lot* of stuff for us. You can +check it out by going to https://symfony.com/cloud. It also helps support the +Symfony project, so it's a win-win. + +## Use Real Environment Variables When Possible + +Anyway, the trickiest part of the process is Step 2 - creating the `.env.local` +file with all of your production values, which will include things like API keys, +your database connection details and more. + +Now, *if* your hosting platform allows you to store *real* environment variables +directly inside of it, problem solved! If you set *real* env vars, then there is +no need to manage a `.env.local` file at all. As soon as you deploy, Symfony will +instantly see and use the real env vars. That's what we do for Symfonycasts. + +## Creating .env.local During Deploy? + +*But* if that's *not* an option for you, you'll need to somehow give your +deployment system access to your sensitive values so that it can create the +`.env.local` file. But... since we're not committing any of these values to our +repository, where *should* we store them? + +One option for handling sensitive values is Symfony's *secrets vault*. It's a +set of files that contain environment variables in an *encrypted form*. These +files are *safe* to commit to your repository... because they're encrypted! + +## Creating the dev Vault + +If you want to store secrets in a vault, you'll need two of them: one for the +`dev` environment and one for the `prod` environment. We're going to *create* +these two vaults first... *then* I'll explain how to read values out of them. + +Start by creating one for the `dev` environment. Run: + +```terminal +php bin/console secrets:set +``` + +Pass this `GITHUB_TOKEN`, which is the secret we want to set. It then asks for our +"secret value". Since this is the vault for the `dev` environment, we want to +put something that's safe for everyone to see. I'll explain why in a moment. I'll +say `CHANGEME`. You can't see me type that... only because Symfony hides it for +security reasons. + +Since this is the *first* secret we've created, Symfony automatically created the +secrets vault behind the scenes... which is literally a set of files that live +in `config/secrets/dev/`. For the *dev* vault, we're going to commit *all* of +these files to the repository. Let's do that. Add the entire secrets directory: + +```terminal-silent +git add config/secrets/dev +``` + +Then commit with: + + +```terminal +git commit -m "adding dev secrets vault" +``` + +## The Secrets Vault Files + +Here's a quick explanation of the files. `dev.list.php` stores a list of *which* +values live inside the vault, `dev.GITHUB_TOKEN.28bd2f.php` stores the actual +encrypted value, and `dev.encrypt.public.php` is the cryptographic key that allows +developers on your team to add *more* secrets. So if another developer +pulled down the project, they'll have this file... so they can add more secrets. +Finally, `dev.decrypt.private.php` is the secret key that allows us to *decrypt* +and *read* the values in the vault. + +As *soon* as the vault files are present, Symfony will automatically open them, +decrypt the secrets, and expose them as environment variables! But, more on that +in a few minutes. + +## Storing the dev Decrypt Key? + +But wait: did we really just *commit* the `decrypt` key to the repository? Yes! +That would *normally* be a no-no! Why would you go to the trouble of encrypting +values... just to store the decryption key right next to them? + +The reason we're doing *exactly* that is that this is our *dev* vault, which means +we're only going to store values that are safe for *all* developers to look at. +The `dev` vault will only be used local development... and we want our teammates +to be able to pull down the code and read those without any trouble. + +Ok, at this point we have a `dev` vault that Symfony will automatically use in +the `dev` environment. Next: let's create the *prod* vault, which will hold the +*truly* secret values. We'll then learn relationship between vault secrets and +environment variables... as well as an easy way to visualize all of this. diff --git a/sfcasts/ep2-fundamentals/service-config.md b/sfcasts/ep2-fundamentals/service-config.md new file mode 100644 index 0000000..414ae1d --- /dev/null +++ b/sfcasts/ep2-fundamentals/service-config.md @@ -0,0 +1,82 @@ +# Manual Service Config in services.yaml + +At your terminal, run: + +```terminal +bin/console debug:container --parameters +``` + +One of the `kernel` parameters is called `kernel.debug`. In addition to environments, +Symfony has this concept of "debug mode". It's *true* for the `dev` environment +and *false* for `prod`. And, occasionally, it comes in handy! + +Here's our new *challenge* (mostly to see if we can do it). Inside of +`MixRepository`, I want to figure out if we're in debug mode. If debug mode is *true*, +we will cache for *5 seconds*. If it's *false*, I want to cache for *60 seconds*: + +[[[ code('94a9fd6760') ]]] + +## Dependency Injection! + +Let's back up for a minute. Suppose you're working inside of a service like +`MixRepository`. Suddenly you realize that you need some *other* service like +the logger. What do you do to get the logger? The answer: you do the dependency +injection dance. You add a `private LoggerInterface $logger` argument and property... +then you use it down in your code. You'll do this *tons* of times in Symfony. + +Let me undo that... because we don't actually need the logger right now. But what +we *do* need is similar. Right now we're inside of a service and we've suddenly +realized that we need some configuration (the `kernel.debug` flag) to do our work. +What do we do to get that config? The *same* thing! Add that as an argument to our +constructor. Say `private bool $isDebug`, and down here, use it: if `$this->isDebug`, +cache for 5 seconds, else cache for 60 seconds. + +## Non-Autowireable Arguments + +But... there's a slight complication... and I bet you already know what it is. When +we refresh the page... yikes! We get a `Cannot resolve argument` error. If you skip +a bit, it says: + +> Cannot autowire service `App\Service\MixRepository`: argument `$isDebug` of +> method `__construct()` is type-hinted `bool`, you should configure its +> value explicitly. + +That makes sense. Autowiring only works for services. You can't have a bool +`$isDebug` argument and expect Symfony to somehow realize that we want the +`kernel.debug` parameter. I might be a wizard, but I don't have a spell for that. +I *can* make a whole slice of pie disappear, though. With magic. Definitely. + +## Configuring MixRepository in services.yaml + +How do we fix this? Open a file that we haven't looked at yet: +`config/services.yaml`: + +[[[ code('542aee2141') ]]] + +So far, we haven't needed to add any configuration for our +`MixRepository` service. The container saw the `MixRepository` class as soon as we +created it... and autowiring helped the container know which arguments to pass to +the constructor. But now that we have a non-autowireable argument, we need to give +the container a *hint*. And we do that in this file. + +Head down to the bottom and add the full namespace of this class: +`App\Service\MixRepository`: + +[[[ code('8e53833539') ]]] + +Below that, use the word `bind`. And below *that*, give the container a *hint* +to tell it what to pass to the argument by saying `$isDebug` set to `%kernel.debug%`: + +[[[ code('b6a4ed289a') ]]] + +I'm using `$isDebug` on purpose. That needs to *exactly* match the name of +the argument in the class. Thanks to this, the container will pass the +`kernel.debug` parameter value. + +And when we try it... it works! The two service arguments are still autowired, but +we filled in the one *missing* argument so that the container can instantiate our +service. Nice! + +I want to talk more about the purpose of this file and all of the configuration up +here. It turns out that a lot of the magic we've been seeing related to services +and autowiring can be explained by this code. That's *next*. diff --git a/sfcasts/ep2-fundamentals/services-yaml.md b/sfcasts/ep2-fundamentals/services-yaml.md new file mode 100644 index 0000000..6be5c4a --- /dev/null +++ b/sfcasts/ep2-fundamentals/services-yaml.md @@ -0,0 +1,170 @@ +# All About services.yaml + +When Symfony first boots up, it needs to get the full list of all of the services +that should be in the container. That includes the service ID, its class name, and +all of its constructor arguments. The first and biggest source of services are +*bundles*. If you run + +```terminal +php bin/console debug:container +``` + +the vast majority of these services come from bundles. The *second* place the +container gets services from is *our* code. And to learn about our services, Symfony +reads `services.yaml`. + +## The Special \_defaults Section + +At the moment that Symfony starts parsing the first line of this file, *nothing* +in our `src/` directory has been registered as a service in the container. This is +really important. Adding our classes to the container is, in fact, the *job* of +this file! And the way it does it is pretty amazing. Let's take a tour! + +Notice that the config is under a `services` key. Like `parameters`, this +is a special key. And, like its name suggests, anything under this is meant to +configure *services*. + +The first *sub-key* under this is `_defaults`. `_defaults` is a magic key +that allows us to define some *default* options that will be added to *all* services +that are registered in this file. So *every* service that we register below will +*automatically* have `autowire: true` and `autoconfigure: true`: + +[[[ code('a83c31b72c') ]]] + +Let's look at an example. The most *basic* thing you can do under the `services` +key is... register a service! That's what we're doing at the bottom. This tells +the container that there should be an `App\Service\MixRepository` service in the +container *and* we specified one option: `bind`. + +[[[ code('11d276a2d8')]]] + +Services can actually have a *bunch* of options, including `autowire` and +`autoconfigure`. So it would be *totally* legal to say, `autowire: true` and +`autoconfigure: true` right here. This would work *just* fine. But thanks to the +`_defaults` section, those aren't needed! The `_defaults` says: + +> Unless it's been overridden on a specific service, set `autowire` and +> `autoconfigure` to `true` for all services in this file. + +And what does `autowire` *do*? Simple! It tells Symfony's container: + +> Hey! Please try to guess my constructor arguments by looking at their type-hints. + +This feature is pretty awesome... which is why it's automatically turned on for all +of our services. The other option - `autoconfigure` - is more subtle and we'll talk +about it later. + +## Service Auto-Registration + +All right, by the time we get to the `_defaults` line, we've established some default +configuration... but we *haven't* actually registered any services yet. That's the +job of the next section... and it's the key to *everything*: + +[[[ code('dee9765bcf') ]]] + +This special syntax says + +> Please look inside the `src/` directory and automatically register *all* +> PHP classes as a service... except for these three things. + +This is why, *immediately* after we created the `MixRepository` class, it was +*already* in the container! And thanks to the `_defaults` section, any services +registered by this will *automatically* have `autowire: true` and +`autoconfigure: true`. That's some serious team work! This mechanism is called +"Service Auto-Registration". + +But remember, every service in the container needs to have a unique ID. If you look +back at `debug:container`, most of the service IDs are snake case. Let me zoom out +a bit so it's easier to see. Better! So, for example, the `Twig` service has the +snake case `twig` ID. But if you scroll up to the top of this list, our +`MixRepository` ID is... the full *class* name. + +Yep! When you use Service Auto-Registration, it uses the class name as *both* the +class *and* the service *ID*. This is done for simplicity... but *also* for +autowiring. When we try to autowire `MixRepository` into our controller or anywhere +else, to figure out which service to pass us, Symfony will look for a service +whose ID exactly matches `App\Service\MixRepository`. So Service +Auto-Registration not only registers our classes as services, it does it in a way +that makes them *autowireable*. That's awesome! + +## Auto-Registration of Non-Services? + +Anyway, after this section here, *every* class in `src/` is now registered +as a service in the container. Except, well... we don't want *every* class in +`src/` to be a service. + +There are really two types of classes in your app: "Service classes" that do work, +and "model classes" - sometimes called "DTOs" - whose job is mostly to hold data - +like a `Product` class with `name` and `price` properties. We want the container +to handle instantiating our services. But for model classes, *we* will create them +whenever we need them - like with `$product = new Product()`. So, these will *not* +be services in the container. + +In the next tutorial, we'll create Doctrine entity classes, which are model classes +for the database. These will live in the `src/Entity/` directory... and since +they're not meant to be services, that directory is excluded. So we register +*everything* in the `src/` directory as a service, *except* for these three things. + +But.. fun fact! This `exclude` key is *not* that important. Heck, you could delete +it and everything would *still* work! If you accidentally register something as a +service that isn't *meant* to be a service, *no worries*! Since you'll never try +to autowire and *use* that class like a service, Symfony will realize it's not +being used and remove it from the container. Dang, that is smart! + +## Custom Service Configuration + +So everything in `src/` is automatically registered as a service without us needing +to do anything or touch this file. + +*But*... occasionally, you'll need to add *extra* config to a *specific* service. +That's what happened with `MixRepository` thanks to its non-autowireable +`$isDebug` argument. + +To fix that, at the bottom of this file, we're registering a new service whose ID +and class is `App\Service\MixRepository`. This will actually *override* the service +that was created during Service Auto-Registration, since both IDs will match +`App\Service\MixRepository`. So, we're defining a *brand new* service. + +But thanks to `_defaults`, it automatically has `autowire: true` and +`autoconfigure: true`. Then we add the additional `bind` option. + +So the only thing we need to put at the bottom of this file are services that need +*additional* configuration to work. And... there's actually a *cooler* way to fix +non-autowireable arguments that I'll show you next. + +## All Configuration Files are Equals! + +But before we get to that, I want to mention one more thing: this file, +`services.yaml`, is loaded via the *same* system that loads all of the files in +`config/packages/`. In fact, there's no technical difference between this file and +say... `framework.yaml`. That's right! If we wanted to, we could copy and delete +the contents of `services.yaml`, paste them into `framework.yaml`, and everything +would work *exactly* the same. + +*Except* that... we would need to, y'know, just correct these paths since we're one +directory deeper. Watch! I'll move this around real quick and... this still works +just fine! Cool! Let's put that back the way it was and... there we go. + +The only reason we have a `service.yaml` file is for organization. It feels good to +have *one* file to "configure your services". The truly important thing is that all +of this config lives under the `services` key. In fact, near the top of this file, +you'll notice there's an empty `parameters` key. + +In `cache.yaml`, we created a `parameters` key *there* to register a new parameter. +It's really up to us to decide *where* we want to define this parameter. We can do it +in `cache.yaml` or, to keep all parameters in one spot, we could copy this and move +it over to `services.yaml`. + +In `cache.yaml`, I'll also grab the `when@dev`, delete that, and paste it into +`services.yaml`: + +[[[ code('5ffe3e85e5') ]]] + +On a technical level, that makes no difference and our app *still* +works. But I like this better. Services and parameters are a global idea in your +app... so it's nice to organize them all in one file. + +All right, the only reason we wrote any code at the bottom of `services.yaml` +was to tell the container what to pass to the non-autowireable `$isDebug` argument. +But what if I told you there's a more *automatic* way to solve these problematic +arguments? That's *next*. diff --git a/sfcasts/ep2-fundamentals/time-bundle.md b/sfcasts/ep2-fundamentals/time-bundle.md new file mode 100644 index 0000000..83b5b99 --- /dev/null +++ b/sfcasts/ep2-fundamentals/time-bundle.md @@ -0,0 +1,101 @@ +# New Bundle, New Service: KnpTimeBundle + +On our site, you can create your *own* vinyl mix. (Or you'll *eventually* be able +to do this... right now, this button doesn't do anything). But another +great feature of our site is the ability to browse *other* user's mixes. + +Now that I'm looking at this, it might be useful if we could see *when* each mix +was created. + +If you don't remember *where* in our code this page was built, you can use a trick. +Down on the web debug toolbar, hover over the 200 status code. Ah, ha! This shows +us that the controller behind this page is `VinylController::browse`. + +Cool! Go open up `src/Controller/VinylController.php`. *Here* is the `browse` action: + +[[[ code('1d9b35c7dc') ]]] + +By the way, I *did* update the code a *little* bit since episode one... so make sure +you've got a fresh copy if you're coding along with me. + +This method calls `$this->getMixes()`... which is a private function I created down +at the bottom: + +[[[ code('12af057d07') ]]] + +This returns a *big* array of fake data that represents the mixes +we're going to render on the page. Eventually, we'll get this from a *dynamic* source, +like a database. + +## Printing Dates in Twig + +Notice that each mix has a `createdAt` date field. We get these mixes up +in `browse()`... and pass them as a `mixes` variable into `vinyl/browse.html.twig`. +Let's jump into that template. + +Down here, we use Twig's `for` loop to loop over `mixes`. Simple enough! + +[[[ code('609c972403') ]]] + +Let's *also* now print the "created at" date. Add a `|`, another `` and then +say `{{ mix.createdAt }}`. + +There's just one problem. If you look at `createdAt`... it's a `DateTime` object. +And you can't just *print* `DateTime` objects... you'll get a big error reminding +you... that you can't just print `DateTime` objects. Cruel world! + +Fortunately, Twig has a handy `date` filter. We talked about filters briefly +in the first episode: we using them by adding a `|` after some value and then the +name of the filter. This *particular* filter also takes an argument, which is the +*format* the date should be printed. To keep things simple, let's use `Y-m-d`, or +"year-month-day". + +[[[ code('cc122a1e01') ]]] + +Head over and refresh and... okay! We can now see *when* each was created, though +the format isn't very attractive. We *could* do more work to spruce this up... +but it would be *way* cooler if we could print this out in the "ago" format. + +You've probably seen it before.... like for comments on a blog post... they say +something like "posted three months ago" or "posted 10 minutes ago". + +So... the question is: How *can* we convert a `DateTime` object into that nice +"ago" format? Well, that sounds like *work* to me and, as I said earlier, *work* +in Symfony is done by a service. So the *real* question is: Is there a *service* +in Symfony that can convert `DateTime` objects to the "ago" format? The answer +is... no. But there *is* a third party bundle that can give us that service. + +## Installing KnpTimeBundle + +Go to https://github.com/KnpLabs/KnpTimeBundle. If you look at this bundle's +documentation, you'll see that it gives us a service that can do that conversion. +So... let's get it installed! + +Scroll to the `composer require` line, copy that, spin over to our terminal, +and paste. + +```terminal-silent +composer require knplabs/knp-time-bundle +``` + +Cool! This grabbed `knplabs/knp-time-bundle`... as well as `symfony/translation`: +Symfony's translation component, which is a dependency of `KnpTimeBundle`. Near +the bottom, it *also* configured two recipes. Let's see what those did. Run: + +```terminal +git status +``` + +Awesome! Any time you install a third party package, Composer will *always* +modify your `composer.json` and `composer.lock` files. This *also* updated the +`config/bundles.php` file: + +[[[ code('35747b5619') ]]] + +That's because we just installed a bundle - `KnpTimeBundle` - and its recipe +handled that automatically. It also looks like the translation recipe added +a config file and a `translations/` directory. The translator *is* needed +to use KnpTimeBundle... but we won't need to work with it directly. + +So... what did installing this new bundle give us? Services of course! Let's +find and use those next! diff --git a/sfcasts/ep3-doctrine/add-property.md b/sfcasts/ep3-doctrine/add-property.md new file mode 100644 index 0000000..901a235 --- /dev/null +++ b/sfcasts/ep3-doctrine/add-property.md @@ -0,0 +1,119 @@ +# Adding new Properties + +In our `VinylMix` entity, I forgot to add a property earlier: `votes`. We're going +to keep track of the number of up votes or down votes that a particular mix has. + +## Modifying with make:entity + +Ok... so how can we add a *new* property to an entity? Well, we can *absolutely* +do it by hand: all we need to do is create the property and the getter and setter +methods. *But*, a much easier way is to head *back* to our favorite `make:entity` +command: + +```terminal-silent +php bin/console make:entity +``` + +This is used to *create* entities, but we can also use it to *update* them. +Type `VinylMix` as the class name and... it sees that it exists! Add +a new property: `votes`... make it an `integer`, say "no" to nullable.. +then hit "enter" to finish. + +The end result? Our class has a new property... and getter and setter methods below. + +[[[ code('c4d23cc903') ]]] + +## Generating a Second Migration + +Ok, let's think. We have a `vinyl_mix` table in the database... but it does *not* +yet have the new `votes` column. We need to *alter* the table to add it. How can +we do that? The exact same way as before: with a migration! At your terminal, run: + +```terminal +symfony console make:migration +``` + +Then go check out the new class. + +[[[ code('4d17ed3986') ]]] + +This is amazing! Inside the `up()` method, it says + +> ALTER TABLE vinyl_mix ADD votes INT NOT NULL + +So it saw our `VinylMix` entity, checked out the `vinyl_mix` table in the database, +and generated a *diff* between them. It realized that, in order to make the database +look like our entity, it needed to alter the table and add that `votes` column. +That's simply *amazing*. + +Back over at the terminal, if you run + +```terminal +symfony console doctrine:migrations:list +``` + +you'll see that it recognizes *both* migrations and it knows that it has *not* +executed the second one. To do that, run: + +```terminal +symfony console doctrine:migrations:migrate +``` + +Doctrine is smart enough to *skip* the first and execute the *second*. Nice! + +When you deploy to production, all you need to do is run `doctrine:migrations:migrate` +each time. It will handle executing any and all migrations that the *production* +database hasn't yet executed. + +## Giving Properties Default Values + +Ok, one more quick thing while we're here. Inside of `VinylMix`, the new `votes` +property defaults to `null`. But when we create a new `VinylMix`, it would make +a lot of sense to default the votes to *zero*. So let's change this to `= 0`. + +Cool! And if we do that, the property in PHP no longer needs to allow `null`... +so remove the `?`. Because we're initializing to an integer, this property will +*always* be an `int`: it will never be null. + +[[[ code('cb3daa06fb') ]]] + +But... I wonder... because I made this change, do I need to alter anything in my +database? The answer is *no*. I can prove it by running a helpful command: + +```terminal +symfony console doctrine:schema:update --dump-sql +``` + +This is very similar to the `make:migration` command... but instead of generating +a file with the SQL, it just prints out the SQL needed to bring your database up +to date. In this case, it shows that our database is *already* in sync with our +entity. + +The point is: if we initialize the value of a property in PHP... that's *just* a +PHP change. It doesn't change the column in the database or give the *column* +a default value, which is totally fine. + +## Auto-Setting createdAt + +Let's initialize one other field: `$createdAt`. It would be *amazing* if something +automatically set this property whenever we created a new `VinylMix` object... instead +of us needing to set it manually. + +Whelp, we can do that by creating a good, old-fashioned PHP `__construct()` method. +Inside, say `$this->createdAt = new \DateTimeImmutable()`, which will default to +*right now*. + +[[[ code('9527d880e7') ]]] + +That's it! And... we don't need the `= null` anymore since it will be initialized +down here... and we also don't need the `?`, because it will *always* be a +`DateTimeImmutable` object. + +[[[ code('0598821470') ]]] + +Nice! Thanks to this, the `$createdAt` property will automatically be set +*every time* we instantiate our object. And that's just a PHP change: it doesn't +change the column in the database. + +All right, we have a `VinylMix` entity *and* the corresponding table. Next, let's +instantiate a `VinylMix` object and *save* it to the database. diff --git a/sfcasts/ep3-doctrine/console.md b/sfcasts/ep3-doctrine/console.md new file mode 100644 index 0000000..6666bff --- /dev/null +++ b/sfcasts/ep3-doctrine/console.md @@ -0,0 +1,75 @@ +# The "symfony console" Command & server_version + +Doctrine is now configured to talk to our database, which lives inside a Docker +container. That's thanks to the fact that the Symfony dev server exposes +this `DATABASE_URL` environment variable, which *points* to the container. For +me, the container is accessible on port 50739. + +Now let's make sure the actual database has been created. But first, in `index.php`, +remove the `dd()`... then close that file. + +Spin over to your terminal and run: + +```terminal +php bin/console +``` + +This prints *every* `bin/console` command that's available *including* a bunch of +*new* ones that start with the word `doctrine`. Ooh. Most of these aren't very +important and we'll walk through the ones that *are* along the way. + +## bin/console doctrine:database:create + +For example, one is called `doctrine:database:create`. Cool, let's try it: + +```terminal +php bin/console doctrine:database:create +``` + +And... error! Look closely: it's trying to connect to port 5432. But our environment +variable is pointing to port 50739! It's as if it's using the `DATABASE_URL` +value from our `.env` file instead of the *real* one that's set by the Symfony binary. + +And, in fact, that's *exactly* what's happening. And, it makes sense! When we refresh +the page in our browser, that's processed *through* the `symfony` binary, which gives +it the opportunity to add the environment variable. + +But when we run a `bin/console` command - where `console` is just a PHP file that +lives in a `bin/` directory, the `symfony` binary is *never* used as part of that +process. This means it never has the opportunity to add the environment variable. +And so, Symfony falls back to using the value from `.env`. + +To fix this, whenever we run a `bin/console` command that needs the Docker environment +variables, instead of running `bin/console`, run `symfony console`: + +```terminal-silent +symfony console doctrine:database:create +``` + +That's literally a shortcut to running `bin/console`: it's no different. But the +fact that we're executing it *through* the `symfony` binary gives it the opportunity +to add the environment variables. + +When we try this... yes! We *do* get an error because apparently the database already +exists, but it *did* successfully connect and talk to the database. + +## Configuring the server_version + +Ok, there's one last bit of configuration that we need to set. Open +`config/packages/doctrine.yaml`. This file came from the recipe. Find +`server_version` and un-comment it. + +[[[ code('e49193f024') ]]] + +This value "13" is referring to the version of my database engine. Since I'm +using Postgres version 13, I need 13 here. If you're using MySQL, you might need +8 or 5.7. + +This helps Doctrine determine which features your database does or doesn't support... +since a newer version of a database might support features that an older version +doesn't. It's not a particularly interesting piece of configuration, we just need +to make sure it's set. + +Ok team: all the boring setup is *done*. Next: let's create our first entity class! +Entities are the most *foundational* concept in Doctrine and the *key* to talking +to our first database table. diff --git a/sfcasts/ep3-doctrine/docker-compose.md b/sfcasts/ep3-doctrine/docker-compose.md new file mode 100644 index 0000000..f0bda62 --- /dev/null +++ b/sfcasts/ep3-doctrine/docker-compose.md @@ -0,0 +1,151 @@ +# docker-compose & Exposed Ports + +We need to get a database running: MySQL, Postgresql, whatever. If you already have +one running, awesome! All you need to do is copy your `DATABASE_URL` environment +variable, open or create a `.env.local` file, paste, then change it to match whatever +your local setup is using. If you decide to do this, feel free to skip ahead to +the end of chapter 4 where we configure the `server_version`. + +## Docker Just for the Database + +For me, I do *not* have a database running locally on my system... and I'm *not* +going to install one. Instead, I want to use Docker. And, we're going to use Docker +in an interesting way. I *do* have PHP installed locally: + +```terminal-silent +php -v +``` + +So I *won't* use Docker to create a container specifically for PHP. Instead I'm going +to use Docker simply to help boot up any *services* my app needs locally. And right +now, I need a database service. Thanks to some magic between Docker and the Symfony +binary, this is going to be *super* easy. + +To start, remember when the Doctrine recipe asked us if we wanted Docker +configuration? Because we said yes, the recipe gave us `docker-compose.yml` and +`docker-compose.override.yml` files. When Docker boots, it will read *both* of +these... and they're split into two pieces just in case you want to *also* +use Docker to deploy to production. But we're not going to worry about that: we +just want to use Docker to make life easier for local development. + +[[[ code('6190b9c37f') ]]] + +[[[ code('4a4d66d50e') ]]] + +These files say that they will boot a single Postgres database container +with a user called `symfony` and password `ChangeMe`: + +***TIP +The username changed from `symfony` to `app` in the newest recipe version. +*** + +It will also expose port 5432 of the container - that's Postgres's normal port - to our +*host* machine on a *random* port. This means that we're going to be able to talk to the +Postgresql Docker container as *if* it were running on our local machine... as long as +we know the random port that Docker chose. We'll see how that works in a minute. + +By the way, if you want to use MySQL instead of Postgres, you absolutely can. +Feel free to update these files... or delete both of them and run: + +```terminal +php bin/console make:docker:database +``` + +to generate a new compose file for MySQL or MariaDB. I'm going to stick with Postgres +because it's awesome. + +At this point, we're going to start Docker and learn a bit about how to communicate +with the database that lives inside. If you're pretty comfortable with Docker, feel +free to skip to the next chapter. + +## Starting the Container + +Anyways, let's get our container running. First, make sure you have Docker actually +installed on your machine: I won't show that because it varies by operating system. +Then, find your terminal and run: + +```terminal +docker-compose up -d +``` + +The `-d` means "run in the background as a daemon". The first time you run this, +it'll probably download a bunch of stuff. But eventually, our container should +start! + +## Communicating with the Container + +Cool! But now what? How can we *talk* to the container? Run a command called: + +```terminal +docker-compose ps +``` + +This shows info about all the containers currently running... just one for us. The +really important thing is that port 5432 in the container is connected to port +50700 on my host machine. This means that if we talk to this port, we will actually +be talking to that Postgres database. Oh, and this port is random: it'll be different +on your machine... and it'll even change each time we stop and start our container. +More on that soon. + +But now that we know about port 50700, we can *use* that to connect to the database. +For example, because I'm using Postgres, I could run: + +```terminal-silent +psql --user=symfony --port=50700 --host=127.0.0.1 --password app +``` + +That means: connect to Postgres at 127.0.0.1 port 50700 using user `symfony` and +talking to the `app` database. All of this is configured in the `docker-compose.yml` +file. Copy the `ChangeMe` password because that last flag tells Postgres to ask +for that password. Paste and... we're in! + +If you're using MySQL, we can do this same thing with a `mysql` command. + +But, this only works if we have that `psql` command installed on our *local* +machine. So let's try a different command. Run: + +```terminal +docker-compose ps +``` + +again. The container is called `database`, which comes from our `docker-compose.yml` +file. So we can change the previous command to: + +```terminal +docker-compose exec database psql --username symfony --password app +``` + +This time, we're executing the `psql` command *inside* the container, so we don't +need to install it locally. Type `ChangeMe` for the password and... we're back in! + +The point is: just by running `docker-compose up`, we have a Postgres database +container that we can talk to! + +## Stopping the Container + +Btw, when you're ready to stop the container later, you can run: + +```terminal +docker-compose stop +``` + +That basically turns the container off. Or you can run the more common: + +```terminal +docker-compose down +``` + + +which turns off the containers and removes them. To start back up, it's the same: + +```terminal +docker-compose up -d +``` + +But notice that when we run `docker-compose ps` again, the port on my host machine +is a *different* random port! So, in theory, we could configure the `DATABASE_URL` +variable to point to our Postgres database, including using the correct port. But +that random port that keeps changing is going to be annoying! + +Fortunately, there's a trick for this! It turns our, our app is *already* configured, +without us doing anything! That's next. diff --git a/sfcasts/ep3-doctrine/docker-env-vars.md b/sfcasts/ep3-doctrine/docker-env-vars.md new file mode 100644 index 0000000..134895b --- /dev/null +++ b/sfcasts/ep3-doctrine/docker-env-vars.md @@ -0,0 +1,84 @@ +# Docker & Environment Variables + +We now have a Postgres database running inside of a Docker container. We can see it +by running: + +```terminal +docker-compose ps +``` + +This also tells us that if we want to *talk* to this database, we can connect to port +`50739` on our local machine. That will be a different port for you, because it's +randomly chosen when we start Docker. + +We also learned that we can talk to the database directly via: + +```terminal +docker-compose exec database psql --user symfony --password app +``` + +To get our actual *application* to point to the database that's running on this +port, we could go into `.env` or `.env.local` and customize `DATABASE_URL` +accordingly: with user `symfony` password `ChangeMe`... and with whatever your port +currently is. Though... we *would* need to *update* that port each time we start +and stop Docker. + +## Symfony Binary & Docker Env Vars + +Thankfully, we don't need to do *any* of that because, surprise, the `DATABASE_URL` +environment variable is *already* being correctly set! When we set up our project, +we started a local dev server using the Symfony binary. + +Just as a reminder, I'm going to run: + +```terminal +symfony server:stop +``` + +to stop that server. And then restart it with: + +```terminal +symfony serve -d +``` + +I'm mentioning this because the `symfony` binary has a *pretty* awesome Docker +superpower. + +Watch: when you refresh now... and hover over the bottom right corner of the web +debug toolbar, it says "Env Vars: From Docker". + +In short, the Symfony binary *noticed* that Docker was running and exposed some +new environment variables pointing to the database! I'll show you. Open up `public/index.php`. + +[[[ code('debfa901f6') ]]] + +We don't normally care about this file... but it's a great +spot to dump some info *right* when our app starts booting. Inside the callback, +`dd()` the `$_SERVER` superglobal. That variable contains a *lot* of information, +*including* any environment variables. + +Ok, spin over and refresh. Big list! Search for `DATABASE_URL` and... there it is! +But that is *not* the value that we have in our `.env` file: the port is *not* +what we have there. Nope, it's the *correct* port needed to talk to the Docker +container! + +Yup, the Symfony binary detects that Docker is running and sets a *real* +`DATABASE_URL` environment variable that *points* to that container. And remember, +since this is a *real* environment variable, it will win over any value placed +in the `.env` or `.env.local` files. + +The point is: *just* by starting Docker, everything is already set up: we didn't +need to touch *any* config files. That's pretty cool. + +By the way, if you want to see all the environment variables the Symfony binary +is setting, you can run: + +```terminal +symfony var:export --multiline +``` + +But the most important one by far is `DATABASE_URL`. + +Ok: Doctrine is configured! Next, let's create the database itself via a `bin/console` +command. When we do that, we'll learn a trick for doing this *with* the environment +variables from the Symfony binary. diff --git a/sfcasts/ep3-doctrine/entity.md b/sfcasts/ep3-doctrine/entity.md new file mode 100644 index 0000000..ea1de2e --- /dev/null +++ b/sfcasts/ep3-doctrine/entity.md @@ -0,0 +1,136 @@ +# Entity Class + +One of the coolest, but maybe most *surprising* things about Doctrine, is that it +wants you to pretend like the database doesn't exist! Yea, instead of thinking +about tables and columns, Doctrine wants us to think about objects and properties. + +For example, let's say that we want to save some product data. The way we do that +with Doctrine is by creating a `Product` class with *properties* that hold the data. +Then you instantiate a `Product` object, set data onto it and politely ask Doctrine +to save it for you. *We* don't have to worry about *how* Doctrine does that. + +But, of course, behind the scenes Doctrine *is* talking to a database. It will +INSERT the data from the `Product` object into a `product` table where each property +is mapped to a column. This is called an Object Relational Mapper, or *ORM*. + +Later, when we want to get that data back, we don't think about "querying" that table +and its columns. Nope, we simply ask Doctrine to find the object that we had earlier. +Of course, it *will* query the table... then recreate the object with the data. +But that's not a detail *we* think about: we ask for the `Product` object, and it +gives it to us. Doctrine handles all of the saving and querying *automatically*. + +## Generating the Entity with make:entity + +*Anyways*, when we use an ORM like Doctrine, if we want to save something to +the database, we need to create a class that *models* the thing we want to save, like +a `Product` class. In Doctrine, these classes are given a special name: *entities*. +Though, they're really just normal PHP classes. And while you *can* create these +entity classes by hand, there's a MakerBundle command that makes life *much* nicer. + +Spin over to your terminal and run: + +```terminal +php bin/console make:entity +``` + +In this case, we don't have to run `symfony console make:entity` because this +command will *not* talk to the database: it *just* generates code. But, if you're +ever not sure, using `symfony console` is always safe. + +Okay, we want to create a class to store all of the vinyl mixes in our system. So +let's create a new class called `VinylMix`. Then answer `no` for broadcasting +entity updates: that's an extra feature related to Symfony Turbo. + +Ok, here's the important part: it asks which properties we want. We're going +to add *several*. Start with one called `title`. Next it asks which *type* this +field is. Hit `?` to see the full list. + +These are *Doctrine* types... and each one will map to a different column type +in your database, depending on which database you're using, like MySQL or +Postgres. The basic types are on top like `string`, `text` - which can hold +*more* than a string) - `boolean`, `integer` and `float`. Then relationship fields - +we'll talk about those in the next tutorial - some special fields, like storing +JSON and date fields. + +For `title`, use `string`, which can hold up to 255 characters. I'll keep the default +length... then it asks us if the field can be null in the database. I'll answer `no`. +This means that the column *cannot* be null. In other words, the column will be +*required* in the database. + +And... one field done! Let's add a few more. We need a `description`, and make this +a `text` type. `string` maxes out at 255 characters, `text` can hold a ton more. +This time, I'll say `yes` to making it nullable. So this will be an *optional* +column in the database. Another one down! + +For the next property, call it `trackCount`. It will be an `integer` and will +be *not* null. Then add `genre`, as a `string`, length 255... and also not null +so that it's required in the database. + +*Finally*, add a `createdAt` field so we can know when each vinyl mix was +originally created. This time, because the field name ends in "At", the command +suggests a `datetime_immutable` type. Hit "enter" to use that, and also make this +*not* null in the database. + +We don't need to add any more properties right now so hit "enter" one more time to +exit the command. + +Done! What did this do? Well first, I can tell you that this did *not* talk to +or change our database at *all*. Nope, it simply generated two classes. The first +is `src/Entity/VinylMix.php`. The *second* is `src/Repository/VinylMixRepository.php`. +Ignore the `Repository` one for now... we'll talk about its purpose in a few minutes. + +[[[ code('33b9a4adde') ]]] + +## Checking out the Entity Class & Attributes + +Go open up the `VinylMix.php` entity. Say hello to... a... wow, pretty normal, boring +PHP class! It generated a `private` property for each field we added, +plus an extra `id` property. The command also added a getter and setter method for +each of these. So... this is basically just a class that holds data... and we can access +and set that data via the getter and setter methods + +The *only* thing that makes this class special are the attributes. The `ORM\Entity` +above the class tells Doctrine: + +> Hey! I want to be able to save objects of this class to the database. This +> is an *entity*. + +Then, above each property, we use `ORM\Column` to tell Doctrine that we want to +save this property as a *column* in the table. This also communicates other options +like the *length* of the column and whether or not it should be *nullable*. +`nullable: false` is the default... so the command only generated `nullable: true` +on the *one* property that needs it. + +The other thing `ORM\Column` controls is the field *type*. That's set via this `type` +option. As I mentioned, this doesn't refer directly to a MySQL or Postgres type... +its a *Doctrine* type that will then *map* to something specific based on our +database. + +## Field Type Guessing + +But, interesting: the `type` option only shows up on the `$description` field. +The reason for that is *really* cool... and new! Doctrine is smart. It looks at the +type on your *property* and *guesses* the field type from that. So when you have +a `string` property type, Doctrine assumes that you want that to be *its* `string` +type. You *could* write `Types::STRING` inside `ORM\Column`... but that would be +totally redundant. + +We *do* need it for the `description` field, however... because we want to use the +`TEXT` type, *not* the `STRING` type. But in every *other* situation, it works. +Doctrine guesses the correct type from the `?int` property type... and the same thing +happens down here for the `?\DateTimeImmutable` type. + +## Table and Column Naming + +In addition to controlling things about each column, we can *also* control the *name* +of the table by adding an `ORM\Table` above the class with name set to, for example, +`vinyl_mix`. But, *surprise*! We don't need to do that! Why? Because Doctrine is +really good at generating great names. It generates the table name by transforming +the class into snake case. So even *without* `ORM\Table`, this will be the name +of the table. The same applies to properties. `$trackCount` will map to a +`track_count` column. Doctrine handles all of this for us: we don't need to +think about our table or column names at all. + +At this point, we've run `make:entity` and it generated an entity class for us. Yay! +But... we don't actually *have* a `vinyl_mix` table in our database yet. How do we +create one? With the magic of *database migrations*. That's next. diff --git a/sfcasts/ep3-doctrine/es/add-property.md b/sfcasts/ep3-doctrine/es/add-property.md new file mode 100644 index 0000000..fc62083 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/add-property.md @@ -0,0 +1,85 @@ +# Añadir nuevas propiedades + +En nuestra entidad `VinylMix`, olvidé añadir antes una propiedad: `votes`. Vamos a llevar la cuenta del número de votos a favor o en contra que tiene una determinada mezcla. + +## Modificación con make:entity + +Bien... ¿cómo podemos añadir una nueva propiedad a una entidad? Bueno, podemos hacerlo a mano: todo lo que tenemos que hacer es crear la propiedad y los métodos getter y setter. Pero, una forma mucho más fácil es volver a nuestro comando favorito `make:entity`: + +```terminal-silent +php bin/console make:entity +``` + +Éste se utiliza para crear entidades, pero también podemos utilizarlo para actualizarlas. Escribe `VinylMix` como nombre de la clase y... ¡ve que existe! Añade una nueva propiedad: `votes`... conviértela en `integer`, di "no" a anulable... y pulsa "intro" para terminar. + +¿El resultado final? Nuestra clase tiene una nueva propiedad... y métodos getter y setter a continuación. + +[[[ code('c4d23cc903') ]]] + +## Generar una segunda migración + +Bien, pensemos. Tenemos una tabla `vinyl_mix` en la base de datos... pero aún no tiene la nueva columna `votes`. Tenemos que modificar la tabla para añadirla. ¿Cómo podemos hacerlo? Exactamente igual que antes: ¡con una migración! En tu terminal, ejecuta: + +```terminal +symfony console make:migration +``` + +Luego ve a ver la nueva clase. + +[[[ code('4d17ed3986') ]]] + +¡Esto es increíble! Dentro del método `up()`, dice + +> ALTER TABLE vinyl_mix ADD votes INT NOT NULL + +Así que vio nuestra entidad `VinylMix`, comprobó la tabla `vinyl_mix` en la base de datos y generó una diferencia entre ellas. Se dio cuenta de que, para que la base de datos se pareciera a nuestra entidad, tenía que alterar la tabla y añadir esa columna `votes`. Eso es simplemente increíble. + +De vuelta al terminal, si ejecutas + +```terminal +symfony console doctrine:migrations:list +``` + +verás que reconoce ambas migraciones y sabe que no ha ejecutado la segunda. Para ello, ejecuta: + +```terminal +symfony console doctrine:migrations:migrate +``` + +Doctrine es lo suficientemente inteligente como para saltarse la primera y ejecutar la segunda. ¡Qué bien! + +Cuando despliegues a producción, todo lo que tienes que hacer es ejecutar `doctrine:migrations:migrate`cada vez. Se encargará de ejecutar todas y cada una de las migraciones que la base de datos de producción aún no haya ejecutado. + +## Dar valores por defecto a las propiedades + +Bien, una cosa más rápida mientras estamos aquí. Dentro de `VinylMix`, la nueva propiedad `votes`tiene como valor por defecto `null`. Pero cuando creamos un nuevo `VinylMix`, tendría mucho sentido poner los votos por defecto a cero. Así que cambiemos esto a `= 0`. + +¡Genial! Y si hacemos eso, la propiedad en PHP ya no necesita permitir `null`... así que elimina el `?`. Como estamos inicializando a un entero, esta propiedad siempre será un `int`: nunca será nulo. + +[[[ code('cb3daa06fb') ]]] + +Pero... Me pregunto... porque he hecho este cambio, ¿tengo que modificar algo en mi base de datos? La respuesta es no. Puedo probarlo ejecutando un comando muy útil: + +```terminal +symfony console doctrine:schema:update --dump-sql +``` + +Es muy parecido al comando `make:migration`... pero en lugar de generar un archivo con el SQL, sólo imprime el SQL necesario para actualizar tu base de datos. En este caso, muestra que nuestra base de datos ya está sincronizada con nuestra entidad. + +La cuestión es: si inicializamos el valor de una propiedad en PHP... eso es sólo un cambio en PHP. No cambia la columna en la base de datos ni le da un valor por defecto, lo cual está totalmente bien. + +## Autoconfiguración de createdAt + +Vamos a inicializar otro campo: `$createdAt`. Sería increíble que algo estableciera automáticamente esta propiedad cada vez que creamos un nuevo objeto `VinylMix`... en lugar de tener que establecerla nosotros manualmente. + +Podemos hacerlo creando un método PHP `__construct()` a la vieja usanza. Dentro, digamos `$this->createdAt = new \DateTimeImmutable()`, que por defecto será ahora mismo. + +[[[ code('9527d880e7') ]]] + +Y ya está Y... ya no necesitamos el `= null` ya que se inicializará aquí abajo... y tampoco necesitamos el `?`, porque siempre será un objeto`DateTimeImmutable`. + +[[[ code('0598821470') ]]] + +¡Muy bien! Gracias a esto, la propiedad `$createdAt` se establecerá automáticamente cada vez que instanciemos nuestro objeto. Y eso es sólo un cambio de PHP: no cambia la columna en la base de datos. + +Muy bien, tenemos una entidad `VinylMix` y la tabla correspondiente. A continuación, vamos a instanciar un objeto `VinylMix` y guardarlo en la base de datos. diff --git a/sfcasts/ep3-doctrine/es/add-property.vtt b/sfcasts/ep3-doctrine/es/add-property.vtt new file mode 100644 index 0000000..bca06bc --- /dev/null +++ b/sfcasts/ep3-doctrine/es/add-property.vtt @@ -0,0 +1,255 @@ +WEBVTT + +00:00:01.026 --> 00:00:06.476 align:middle +En nuestra entidad VinylMix, olvidé +añadir antes una propiedad: votes. + +00:00:07.076 --> 00:00:12.636 align:middle +Vamos a llevar la cuenta del número de votos a +favor o en contra que tiene una determinada mezcla. + +00:00:13.496 --> 00:00:17.556 align:middle +Bien... ¿cómo podemos añadir +una nueva propiedad a una entidad? + +00:00:18.316 --> 00:00:23.456 align:middle +Bueno, podemos hacerlo a mano: todo lo que +tenemos que hacer es crear la propiedad + +00:00:23.636 --> 00:00:25.636 align:middle +y los métodos getter y setter. + +00:00:26.246 --> 00:00:31.596 align:middle +Pero, una forma mucho más fácil es volver +a nuestro comando favorito make:entity: + +00:00:32.506 --> 00:00:37.326 align:middle +Éste se utiliza para crear entidades, pero +también podemos utilizarlo para actualizarlas. + +00:00:38.146 --> 00:00:40.566 align:middle +Escribe VinylMix como nombre de la clase y... + +00:00:41.056 --> 00:00:43.306 align:middle +¡vea que existe! + +00:00:44.016 --> 00:00:45.776 align:middle +Añade una nueva propiedad: votes... + +00:00:46.486 --> 00:00:49.776 align:middle +haz que sea un integer, di "no" a anulable.. + +00:00:50.306 --> 00:00:51.956 align:middle +y pulsa "enter" para terminar. + +00:00:52.576 --> 00:00:53.976 align:middle +¿El resultado final? + +00:00:54.516 --> 00:00:56.106 align:middle +Nuestra clase tiene una nueva propiedad... + +00:00:56.576 --> 00:00:58.426 align:middle +y métodos getter y setter a continuación. + +00:00:59.416 --> 00:01:01.056 align:middle +Bien, pensemos. + +00:01:01.616 --> 00:01:05.576 align:middle +Tenemos una tabla vinyl_mix +en la base de datos... + +00:01:05.946 --> 00:01:10.356 align:middle +pero aún no tiene la nueva columna votes. + +00:01:11.106 --> 00:01:13.486 align:middle +Tenemos que modificar la tabla para añadirla. + +00:01:14.176 --> 00:01:15.586 align:middle +¿Cómo podemos hacerlo? + +00:01:16.116 --> 00:01:19.856 align:middle +Exactamente igual que +antes: ¡con una migración! + +00:01:20.436 --> 00:01:25.886 align:middle +En tu terminal, ejecuta: symfony console +make:migration Luego ve a ver la nueva clase. + +00:01:27.006 --> 00:01:28.856 align:middle +¡Esto es increíble! + +00:01:29.316 --> 00:01:35.496 align:middle +Dentro del método up(), dice ALTER +TABLE vinyl_mix ADD votes INT NOT NULL + +00:01:36.086 --> 00:01:42.606 align:middle +Así que vio nuestra entidad VinylMix, +comprobó la tabla vinyl_mix en la base de datos + +00:01:43.026 --> 00:01:45.786 align:middle +y generó una diferencia entre ellas. + +00:01:46.416 --> 00:01:51.266 align:middle +Se dio cuenta de que, para que la base +de datos se pareciera a nuestra entidad, + +00:01:51.896 --> 00:01:55.786 align:middle +tenía que alterar la tabla +y añadir esa columna votes. + +00:01:56.506 --> 00:01:58.846 align:middle +Eso es sencillamente asombroso. De + +00:01:59.736 --> 00:02:05.836 align:middle +vuelta al terminal, si ejecutas symfony +console doctrine:migrations:list verás + +00:02:05.836 --> 00:02:12.156 align:middle +que reconoce ambas migraciones y +sabe que no ha ejecutado la segunda. + +00:02:13.006 --> 00:02:19.026 align:middle +Para ello, ejecuta: symfony console doctrine:migrations:migrate +Doctrine es lo suficientemente inteligente como + +00:02:19.026 --> 00:02:22.826 align:middle +para saltarse la primera y ejecutar la segunda. + +00:02:23.176 --> 00:02:26.016 align:middle +¡Qué bien! Cuando despliegues a producción, + +00:02:26.376 --> 00:02:30.086 align:middle +todo lo que tienes que hacer es ejecutar +doctrine:migrations:migrate cada vez. Se + +00:02:30.686 --> 00:02:33.946 align:middle +encargará de ejecutar todas +y cada una de las migraciones + +00:02:34.006 --> 00:02:37.836 align:middle +que la base de datos de +producción aún no haya ejecutado. + +00:02:38.756 --> 00:02:41.186 align:middle +Vale, una cosa más rápida +mientras estamos aquí. + +00:02:41.776 --> 00:02:46.686 align:middle +Dentro de VinylMix, la nueva propiedad +votes tiene por defecto null. + +00:02:47.326 --> 00:02:53.486 align:middle +Pero cuando creamos un nuevo VinylMix, tendría +mucho sentido poner por defecto los votos a cero. + +00:02:54.006 --> 00:02:56.526 align:middle +Así que cambiemos esto a = 0. + +00:02:57.246 --> 00:03:04.586 align:middle +¡Genial! Y si hacemos eso, la propiedad +en PHP ya no necesita permitir null... + +00:03:04.916 --> 00:03:10.016 align:middle +así que elimina el ?. Como +estamos inicializando a un entero, + +00:03:10.446 --> 00:03:14.856 align:middle +esta propiedad siempre será +un int: nunca será nula. + +00:03:15.736 --> 00:03:17.026 align:middle +Pero... Me pregunto... + +00:03:17.636 --> 00:03:21.986 align:middle +como he hecho este cambio, ¿tengo que +modificar algo en mi base de datos? + +00:03:22.676 --> 00:03:24.426 align:middle +La respuesta es no. + +00:03:25.066 --> 00:03:30.766 align:middle +Puedo comprobarlo ejecutando un comando muy +útil: symfony console doctrine:schema:update + +00:03:30.936 --> 00:03:36.966 align:middle +--dump-sql Es muy parecido +al comando make:migration... + +00:03:37.466 --> 00:03:43.156 align:middle +pero en lugar de generar un archivo con +el SQL, sólo imprime el SQL necesario + +00:03:43.216 --> 00:03:44.646 align:middle +para actualizar tu base de datos. + +00:03:44.646 --> 00:03:50.106 align:middle +En este caso, muestra que nuestra base de datos +ya está sincronizada con nuestra entidad. + +00:03:50.916 --> 00:03:55.346 align:middle +La cuestión es: si inicializamos +el valor de una propiedad en PHP... + +00:03:55.346 --> 00:03:57.476 align:middle +eso es sólo un cambio en PHP. + +00:03:58.086 --> 00:04:00.406 align:middle +No cambia la columna en la base de datos + +00:04:00.826 --> 00:04:04.986 align:middle +ni le da un valor por defecto, +lo cual está totalmente bien. + +00:04:05.906 --> 00:04:08.836 align:middle +Vamos a inicializar otro campo: $createdAt. + +00:04:09.516 --> 00:04:16.396 align:middle +Sería increíble que algo estableciera automáticamente +esta propiedad cada vez que creamos un nuevo + +00:04:16.396 --> 00:04:17.886 align:middle +objetoVinylMix... + +00:04:18.376 --> 00:04:21.046 align:middle +en lugar de tener que establecerla manualmente. + +00:04:21.706 --> 00:04:28.566 align:middle +Podemos hacerlo creando un método +PHP __construct() a la vieja usanza. + +00:04:29.206 --> 00:04:34.156 align:middle +Dentro, digamos $this->createdAt += new \DateTimeImmutable(), + +00:04:34.536 --> 00:04:36.856 align:middle +que por defecto será ahora mismo. + +00:04:37.676 --> 00:04:38.236 align:middle +Y ya está + +00:04:38.866 --> 00:04:44.616 align:middle +Y... ya no necesitamos el = null ya +que se inicializará aquí abajo... + +00:04:45.006 --> 00:04:51.786 align:middle +y tampoco necesitamos el ?, porque +siempre será un objeto DateTimeImmutable. + +00:04:52.446 --> 00:04:59.436 align:middle +¡Muy bien! Gracias a esto, la propiedad $createdAt +se establecerá automáticamente cada vez que + +00:04:59.436 --> 00:05:01.016 align:middle +instanciemos nuestro objeto. + +00:05:01.636 --> 00:05:06.616 align:middle +Y eso es sólo un cambio en PHP: no +cambia la columna en la base de datos. + +00:05:07.546 --> 00:05:12.356 align:middle +Muy bien, tenemos una entidad +VinylMix y la tabla correspondiente. + +00:05:13.016 --> 00:05:18.396 align:middle +A continuación, vamos a instanciar un objeto +VinylMix y guardarlo en la base de datos diff --git a/sfcasts/ep3-doctrine/es/console.md b/sfcasts/ep3-doctrine/es/console.md new file mode 100644 index 0000000..b157d08 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/console.md @@ -0,0 +1,47 @@ +# El comando "symfony console" y server_version + +Doctrine está ahora configurado para hablar con nuestra base de datos, que vive dentro de un contenedor Docker. Esto es gracias a que el servidor Symfony dev expone esta variable de entorno `DATABASE_URL`, que apunta al contenedor. En mi caso, el contenedor es accesible en el puerto 50739. + +Ahora vamos a asegurarnos de que la base de datos real se ha creado. Pero primero, en `index.php`, elimina el `dd()`... y luego cierra ese archivo. + +Ve a tu terminal y ejecuta: + +```terminal +php bin/console +``` + +Esto imprime todos los comandos de `bin/console` que están disponibles, incluyendo un montón de nuevos comandos que empiezan con la palabra `doctrine`. Ooh. La mayoría de ellos no son muy importantes y ya iremos viendo los que sí lo son. + +## bin/console doctrine:database:create + +Por ejemplo, uno se llama `doctrine:database:create`. Genial, vamos a probarlo: + +```terminal +php bin/console doctrine:database:create +``` + +Y... ¡error! Fíjate bien: está intentando conectarse al puerto 5432. ¡Pero nuestra variable de entorno apunta al puerto 50739! Es como si utilizara el valor `DATABASE_URL`de nuestro archivo `.env` en lugar del real que establece el binario de Symfony. + +Y, de hecho, eso es exactamente lo que está ocurriendo. Y, ¡tiene sentido! Cuando actualizamos la página en nuestro navegador, eso se procesa a través del binario `symfony`, que le da la oportunidad de añadir la variable de entorno. + +Pero cuando ejecutamos un comando `bin/console` -donde `console` es sólo un archivo PHP que vive en un directorio `bin/`, el binario `symfony` nunca se utiliza como parte de ese proceso. Esto significa que nunca tiene la oportunidad de añadir la variable de entorno, por lo que Symfony vuelve a utilizar el valor de `.env`. + +Para solucionar esto, siempre que ejecutemos un comando `bin/console` que necesite las variables de entorno de Docker, en lugar de ejecutar `bin/console`, ejecuta `symfony console`: + +```terminal-silent +symfony console doctrine:database:create +``` + +Eso es literalmente un atajo para ejecutar `bin/console`: no es diferente. Pero el hecho de que lo estemos ejecutando a través del binario `symfony` le da la oportunidad de añadir las variables de entorno. + +Cuando probamos esto... ¡sí! Obtenemos un error porque aparentemente la base de datos ya existe, pero se conectó con éxito y habló con la base de datos. + +## Configurar la versión del servidor + +Bien, hay una última parte de la configuración que debemos establecer. Abre`config/packages/doctrine.yaml`. Este archivo viene de la receta. Busca`server_version` y desactívalo. + +Este valor "13" se refiere a la versión de mi motor de base de datos. Como estoy usando la versión 13 de Postgres, necesito el 13 aquí. Si utilizas MySQL, puede que necesites la 8 o la 5.7. + +Esto ayuda a Doctrine a determinar qué características soporta tu base de datos o no... ya que una versión más reciente de una base de datos podría soportar características que una versión más antigua no soporta. No es una pieza de configuración especialmente interesante, sólo tenemos que asegurarnos de que está configurada. + +Ok equipo: toda la configuración aburrida está hecha. Lo siguiente: ¡creamos nuestra primera clase de entidad! Las entidades son el concepto más fundamental de Doctrine y la clave para hablar con nuestra primera tabla de la base de datos. diff --git a/sfcasts/ep3-doctrine/es/console.vtt b/sfcasts/ep3-doctrine/es/console.vtt new file mode 100644 index 0000000..a969384 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/console.vtt @@ -0,0 +1,185 @@ +WEBVTT + +00:00:01.016 --> 00:00:05.986 align:middle +Doctrine está ahora configurado para hablar con nuestra +base de datos, que vive dentro de un contenedor Docker. + +00:00:06.516 --> 00:00:12.946 align:middle +Esto es gracias a que el servidor de desarrollo +Symfony expone esta variable de entorno DATABASE_URL + +00:00:12.946 --> 00:00:15.526 align:middle +, que apunta al contenedor. + +00:00:16.176 --> 00:00:20.456 align:middle +En mi caso, el contenedor es +accesible en el puerto 50739. + +00:00:21.196 --> 00:00:24.956 align:middle +Ahora vamos a asegurarnos de que +la base de datos real se ha creado. + +00:00:25.466 --> 00:00:28.916 align:middle +Pero primero, en index.php, elimina el dd()... + +00:00:29.476 --> 00:00:30.686 align:middle +y cierra ese archivo. + +00:00:31.506 --> 00:00:33.096 align:middle +Ve a tu terminal y ejecuta: + +00:00:33.096 --> 00:00:40.936 align:middle +php bin/console Esto imprime todos los comandos de +bin/console que están disponibles, incluyendo un montón + +00:00:40.936 --> 00:00:44.386 align:middle +de nuevos comandos que empiezan +con la palabra doctrine. + +00:00:44.516 --> 00:00:49.326 align:middle +Ooh. La mayoría de ellos no +son muy importantes y ya iremos + +00:00:49.326 --> 00:00:51.386 align:middle +viendo los que sí lo son. + +00:00:52.206 --> 00:00:56.526 align:middle +Por ejemplo, uno se llama +doctrine:database:create. + +00:00:57.246 --> 00:01:02.436 align:middle +Genial, vamos a probarlo: php +bin/console doctrine:database:create Y... + +00:01:02.826 --> 00:01:10.136 align:middle +¡error! Fíjate bien: está +intentando conectarse al puerto 5432. + +00:01:10.746 --> 00:01:16.026 align:middle +¡Pero nuestra variable de +entorno apunta al puerto 50739! Es + +00:01:16.846 --> 00:01:23.746 align:middle +como si utilizara el valor DATABASE_URL +de nuestro archivo .env en lugar + +00:01:23.746 --> 00:01:27.386 align:middle +del real que establece el binario de Symfony. + +00:01:28.216 --> 00:01:31.756 align:middle +Y, de hecho, eso es exactamente +lo que está ocurriendo. + +00:01:32.316 --> 00:01:33.506 align:middle +Y, ¡tiene sentido! + +00:01:34.146 --> 00:01:39.786 align:middle +Cuando refrescamos la página en nuestro navegador, +eso se procesa a través del binario symfony, + +00:01:40.386 --> 00:01:43.816 align:middle +que le da la oportunidad de +añadir la variable de entorno. + +00:01:43.816 --> 00:01:51.076 align:middle +Pero cuando ejecutamos un comando bin/console +-donde console es sólo un archivo PHP que vive + +00:01:51.076 --> 00:01:57.546 align:middle +en un directorio bin/, el binario symfony +nunca se utiliza como parte de ese proceso. + +00:01:58.176 --> 00:02:02.886 align:middle +Esto significa que nunca tiene la oportunidad +de añadir la variable de entorno. + +00:02:03.446 --> 00:02:07.526 align:middle +Así, Symfony vuelve a +utilizar el valor de .env. + +00:02:08.486 --> 00:02:11.766 align:middle +Para solucionarlo, siempre que +ejecutemos un comando bin/console + +00:02:11.906 --> 00:02:16.406 align:middle +que necesite las variables de entorno de +Docker, en lugar de ejecutar bin/console, + +00:02:16.676 --> 00:02:24.136 align:middle +ejecuta symfony console: Eso es literalmente un +atajo para ejecutar bin/console: no es diferente. + +00:02:25.076 --> 00:02:31.786 align:middle +Pero el hecho de que lo estemos ejecutando a +través del binario symfony le da la oportunidad + +00:02:31.846 --> 00:02:33.886 align:middle +de añadir las variables de entorno. + +00:02:34.676 --> 00:02:35.986 align:middle +Cuando probamos esto... + +00:02:36.506 --> 00:02:42.536 align:middle +¡sí! Obtenemos un error porque +aparentemente la base de datos ya existe, + +00:02:42.916 --> 00:02:46.306 align:middle +pero se conectó con éxito y +habló con la base de datos. + +00:02:47.176 --> 00:02:51.196 align:middle +Bien, hay una última parte de la +configuración que tenemos que establecer. + +00:02:51.846 --> 00:02:54.886 align:middle +Abre config/packages/doctrine.yaml. + +00:02:55.606 --> 00:02:57.616 align:middle +Este archivo procede de la receta. + +00:02:58.246 --> 00:03:02.266 align:middle +Busca server_version y desactívalo. + +00:03:02.946 --> 00:03:08.646 align:middle +Este valor "13" se refiere a la +versión de mi motor de base de datos. + +00:03:09.476 --> 00:03:14.176 align:middle +Como estoy usando la versión 13 +de Postgres, necesito el 13 aquí. + +00:03:14.706 --> 00:03:19.266 align:middle +Si utilizas MySQL, puede +que necesites la 8 o la 5.7. + +00:03:20.246 --> 00:03:24.746 align:middle +Esto ayuda a Doctrine a determinar qué +características soporta o no tu base de datos... + +00:03:25.216 --> 00:03:30.796 align:middle +ya que una versión más reciente de una base de datos puede +admitir funciones que una versión más antigua no admite. + +00:03:31.416 --> 00:03:35.286 align:middle +No es una pieza de configuración +especialmente interesante, + +00:03:35.576 --> 00:03:37.896 align:middle +sólo tenemos que asegurarnos +de que está configurada. + +00:03:38.916 --> 00:03:42.486 align:middle +Ok equipo: toda la configuración +aburrida está hecha. + +00:03:43.146 --> 00:03:46.456 align:middle +Siguiente: ¡creemos nuestra +primera clase de entidad! + +00:03:46.956 --> 00:03:51.946 align:middle +Las entidades son el concepto más +fundamental de Doctrine y la clave + +00:03:52.046 --> 00:03:54.656 align:middle +para hablar con nuestra primera +tabla de la base de datos diff --git a/sfcasts/ep3-doctrine/es/docker-compose.md b/sfcasts/ep3-doctrine/es/docker-compose.md new file mode 100644 index 0000000..a032545 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/docker-compose.md @@ -0,0 +1,97 @@ +# docker-compose y puertos expuestos + +Necesitamos poner en marcha una base de datos: MySQL, Postgresql, lo que sea. Si ya tienes una en marcha, ¡genial! Todo lo que tienes que hacer es copiar tu variable de entorno `DATABASE_URL`, abrir o crear un archivo `.env.local`, pegarlo, y luego cambiarlo para que coincida con lo que esté usando tu configuración local. Si decides hacer esto, no dudes en saltar al final del capítulo 4, donde configuramos el `server_version`. + +## Docker sólo para la base de datos + +En mi caso, no tengo una base de datos funcionando localmente en mi sistema... y no voy a instalar una. En su lugar, quiero utilizar Docker. Y vamos a utilizar Docker de una forma interesante. Tengo PHP instalado localmente: + +```terminal-silent +php -v +``` + +Así que no voy a usar Docker para crear un contenedor específicamente para PHP. En su lugar, voy a utilizar Docker simplemente para ayudar a arrancar cualquier servicio que mi aplicación necesite localmente. Y en este momento, necesito un servicio de base de datos. Gracias a cierta magia entre Docker y el binario de Symfony, esto va a ser súper fácil. + +Para empezar, ¿recuerdas cuando la receta de Doctrine nos preguntó si queríamos la configuración de Docker? Como dijimos que sí, la receta nos dio los archivos `docker-compose.yml` y`docker-compose.override.yml`. Cuando Docker arranque, leerá ambos... y están divididos en dos partes por si quieres usar también Docker para desplegar en producción. Pero no vamos a preocuparnos por eso: sólo queremos usar Docker para facilitar la vida en el desarrollo local. + +Estos archivos dicen que arrancarán un único contenedor de base de datos Postgres con un usuario llamado `symfony` y una contraseña `ChangeMe`. También expondrá el puerto 5432 del contenedor -que es el puerto normal de Postgres- a nuestra máquina anfitriona en un puerto aleatorio. Esto significa que vamos a poder hablar con el contenedor Docker de Postgresql como si se estuviera ejecutando en nuestra máquina local... siempre que conozcamos el puerto aleatorio que Docker ha elegido. Veremos cómo funciona en un minuto. + +Por cierto, si quieres utilizar MySQL en lugar de Postgres, puedes hacerlo. Puedes actualizar estos archivos... o eliminar ambos y ejecutar + +```terminal +php bin/console make:docker:database +``` + +para generar un nuevo archivo de composición para MySQL o MariaDB. Yo me voy a quedar con Postgres porque es increíble. + +Llegados a este punto, vamos a poner en marcha Docker y aprender un poco sobre cómo comunicarse con la base de datos que vive dentro. Si te sientes bastante cómodo con Docker, no dudes en pasar al siguiente capítulo. + +## Iniciar el contenedor + +De todas formas, vamos a poner en marcha nuestro contenedor. Primero, asegúrate de que tienes Docker realmente instalado en tu máquina: No lo mostraré porque varía según el sistema operativo. Luego, busca tu terminal y ejecuta: + +```terminal +docker-compose up -d +``` + +El `-d` significa "ejecutar en segundo plano como un demonio". La primera vez que lo ejecute, probablemente descargará un montón de cosas. Pero al final, ¡tu contenedor debería arrancar! + +## Comunicarse con el contenedor + +¡Genial! ¿Pero ahora qué? ¿Cómo podemos hablar con el contenedor? Ejecuta un comando llamado: + +```terminal +docker-compose ps +``` + +Esto muestra información sobre todos los contenedores que se están ejecutando actualmente... sólo uno para nosotros. Lo realmente importante es que el puerto 5432 del contenedor está conectado al puerto 50700 de mi máquina anfitriona. Esto significa que si hablamos con este puerto, estaremos hablando realmente con esa base de datos Postgres. Ah, y este puerto es aleatorio: será diferente en tu máquina... e incluso cambiará cada vez que paremos y arranquemos nuestro contenedor. Pronto hablaremos de ello. + +Pero ahora que conocemos el puerto 50700, podemos utilizarlo para conectarnos a la base de datos. Por ejemplo, como estoy utilizando Postgres, podría ejecutar: + +```terminal-silent +psql --user=symfony --port=50700 --host=127.0.0.1 --password app +``` + +Esto significa: conectar con Postgres en el puerto 127.0.0.1 50700 utilizando el usuario `symfony` y hablando con la base de datos `app`. Todo esto está configurado en el archivo `docker-compose.yml`. Copia la contraseña de `ChangeMe` porque esa última bandera le dice a Postgres que te pida esa contraseña. Pégala y... ¡estamos dentro! + +Si utilizas MySQL, podemos hacer esto mismo con un comando `mysql`. + +Pero esto sólo funciona si tenemos ese comando `psql` instalado en nuestra máquina local. Así que vamos a probar con otro comando. Ejecuta + +```terminal +docker-compose ps +``` + +de nuevo. El contenedor se llama `database`, que proviene de nuestro archivo `docker-compose.yml`. Así podremos cambiar el comando anterior por: + +```terminal +docker-compose exec database psql --username symfony --password app +``` + +Esta vez, estamos ejecutando el comando `psql` dentro del contenedor, por lo que no necesitamos instalarlo localmente. Escribe `ChangeMe` como contraseña y... ¡vamos a estar dentro! + +La cuestión es: ¡sólo con ejecutar `docker-compose up`, tenemos un contenedor de base de datos Postgres con el que podemos hablar! + +## Detener el contenedor + +Por cierto, cuando estés preparado para detener el contenedor más adelante, puedes ejecutarlo: + +```terminal +docker-compose stop +``` + +Que básicamente apaga el contenedor. O puedes ejecutar el más común + +```terminal +docker-compose down +``` + +que apaga los contenedores y los elimina. Para volver a arrancar, es lo mismo: + +```terminal +docker-compose up -d +``` + +Pero fíjate en que cuando volvemos a ejecutar `docker-compose ps`, ¡el puerto de mi máquina anfitriona es un puerto aleatorio diferente! Así que, en teoría, podríamos configurar la variable `DATABASE_URL`para que apunte a nuestra base de datos Postgres, incluyendo el uso del puerto correcto. ¡Pero ese puerto aleatorio que sigue cambiando va a ser molesto! + +Afortunadamente, ¡hay un truco para esto! Resulta que nuestra aplicación ya está configurada, ¡sin que nosotros hagamos nada! Eso a continuación. diff --git a/sfcasts/ep3-doctrine/es/docker-compose.vtt b/sfcasts/ep3-doctrine/es/docker-compose.vtt new file mode 100644 index 0000000..b6f674b --- /dev/null +++ b/sfcasts/ep3-doctrine/es/docker-compose.vtt @@ -0,0 +1,330 @@ +WEBVTT + +00:00:01.016 --> 00:00:05.616 align:middle +Necesitamos poner en marcha una +base de datos: MySQL, Postgresql, + +00:00:06.246 --> 00:00:09.086 align:middle +lo que sea. Si ya tienes una en marcha + +00:00:09.446 --> 00:00:13.516 align:middle +, ¡genial! Todo lo que +tienes que hacer es copiar + +00:00:13.916 --> 00:00:18.126 align:middle +tu variable de entorno DATABASE_URL, abrir o + +00:00:18.676 --> 00:00:21.986 align:middle +crear un archivo .env.local, pegarlo, +y luego cambiarlo para que coincida + +00:00:22.806 --> 00:00:27.506 align:middle +con lo que esté usando tu configuración local. +Si decides hacer esto, no dudes en saltar al + +00:00:27.886 --> 00:00:29.876 align:middle +final del capítulo 4, donde + +00:00:30.596 --> 00:00:34.746 align:middle +configuramos el server_version. En mi caso, +no tengo una base de datos funcionando + +00:00:34.976 --> 00:00:37.316 align:middle +localmente en mi sistema... y no voy + +00:00:37.776 --> 00:00:40.156 align:middle +a instalar una. En su lugar, + +00:00:40.676 --> 00:00:43.326 align:middle +quiero utilizar Docker. Y +vamos a utilizar Docker de una + +00:00:43.996 --> 00:00:49.046 align:middle +forma interesante. Tengo PHP instalado +localmente: Así que no voy a usar Docker + +00:00:49.046 --> 00:00:51.546 align:middle +para crear un contenedor específicamente + +00:00:51.546 --> 00:01:00.116 align:middle +para PHP. En su lugar, voy a utilizar Docker simplemente +para ayudar a arrancar cualquier servicio que + +00:01:00.656 --> 00:01:03.366 align:middle +mi aplicación necesite +localmente. Y en este momento, + +00:01:03.986 --> 00:01:10.486 align:middle +necesito un servicio de base de datos. Gracias +a cierta magia entre Docker y el binario de + +00:01:11.216 --> 00:01:16.716 align:middle +Symfony, esto va a ser súper fácil. Para +empezar, ¿recuerdas cuando la receta de Doctrine + +00:01:17.356 --> 00:01:21.496 align:middle +nos preguntó si queríamos +la configuración de Docker? + +00:01:21.676 --> 00:01:25.056 align:middle +Como dijimos que + +00:01:25.716 --> 00:01:29.016 align:middle +sí, la receta nos dio los archivos +docker-compose.yml ydocker-compose.override.yml. + +00:01:29.346 --> 00:01:33.286 align:middle +Cuando Docker arranque, leerá ambos... +y están divididos en dos partes + +00:01:33.286 --> 00:01:36.356 align:middle +por si quieres usar también +Docker para desplegar + +00:01:37.006 --> 00:01:40.596 align:middle +en producción. Pero no vamos a preocuparnos +por eso: sólo queremos usar Docker para + +00:01:40.596 --> 00:01:43.256 align:middle +facilitar la vida en el desarrollo local. + +00:01:43.946 --> 00:01:48.606 align:middle +Estos archivos dicen que arrancarán +un único contenedor de base de datos + +00:01:48.946 --> 00:01:52.786 align:middle +Postgres con un usuario llamado symfony y una + +00:01:53.776 --> 00:02:01.136 align:middle +contraseña ChangeMe. También expondrá el puerto +5432 del contenedor -que es el puerto normal de + +00:02:01.646 --> 00:02:05.336 align:middle +Postgres- a nuestra máquina +anfitriona en un puerto + +00:02:06.036 --> 00:02:11.076 align:middle +aleatorio. Esto significa que vamos a poder +hablar con el contenedor Docker de Postgresql como + +00:02:11.396 --> 00:02:14.026 align:middle +si se estuviera ejecutando en +nuestra máquina local... siempre + +00:02:14.456 --> 00:02:18.116 align:middle +que conozcamos el puerto aleatorio +que Docker ha elegido. Veremos cómo + +00:02:18.586 --> 00:02:20.166 align:middle +funciona en un minuto. Por cierto, si quieres + +00:02:21.016 --> 00:02:26.316 align:middle +utilizar MySQL en lugar de Postgres, puedes hacerlo. +Puedes actualizar estos archivos... o eliminar ambos + +00:02:26.876 --> 00:02:28.586 align:middle +y ejecutar php bin/console +make:docker:database para + +00:02:28.856 --> 00:02:33.076 align:middle +generar un nuevo archivo de +composición para MySQL o MariaDB. + +00:02:33.446 --> 00:02:37.666 align:middle +Yo me voy a quedar con +Postgres porque es increíble. + +00:02:37.666 --> 00:02:41.856 align:middle +Llegados a este punto, vamos a poner en marcha + +00:02:42.656 --> 00:02:47.096 align:middle +Docker y aprender un poco sobre cómo +comunicarse con la base de datos que vive dentro. + +00:02:47.096 --> 00:02:48.886 align:middle +Si te sientes bastante cómodo con + +00:02:49.616 --> 00:02:54.116 align:middle +Docker, no dudes en pasar al siguiente capítulo. +De todas formas, vamos a poner en marcha + +00:02:55.016 --> 00:02:57.016 align:middle +nuestro contenedor. Primero, asegúrate + +00:02:57.616 --> 00:03:01.946 align:middle +de que tienes Docker realmente +instalado en tu máquina: No lo + +00:03:02.676 --> 00:03:05.886 align:middle +mostraré porque varía según el +sistema operativo. Luego, busca tu + +00:03:06.516 --> 00:03:12.436 align:middle +terminal y ejecuta: docker-compose up +-d El -d significa "ejecutar en segundo + +00:03:12.546 --> 00:03:14.916 align:middle +plano como un demonio". La primera vez que + +00:03:15.746 --> 00:03:19.696 align:middle +lo ejecute, probablemente descargará un montón +de cosas. Pero al final, ¡nuestro contenedor + +00:03:20.216 --> 00:03:23.556 align:middle +debería arrancar! ¡Genial! ¿Pero ahora qué? + +00:03:23.886 --> 00:03:26.316 align:middle +¿Cómo podemos hablar con + +00:03:26.876 --> 00:03:28.756 align:middle +el contenedor? Ejecuta un +comando llamado: docker-compose + +00:03:29.576 --> 00:03:34.426 align:middle +ps Esto muestra información +sobre todos los contenedores que + +00:03:34.426 --> 00:03:36.596 align:middle +se están ejecutando actualmente... sólo uno + +00:03:36.876 --> 00:03:38.236 align:middle +para nosotros. Lo realmente + +00:03:38.846 --> 00:03:45.186 align:middle +importante es que el puerto 5432 del +contenedor está conectado al puerto 50700 + +00:03:45.186 --> 00:03:49.516 align:middle +de mi máquina anfitriona. Esto significa que + +00:03:50.176 --> 00:03:56.176 align:middle +si hablamos con este puerto, estaremos hablando realmente +con esa base de datos Postgres. Ah, y este puerto + +00:03:56.936 --> 00:04:01.326 align:middle +es aleatorio: será diferente en tu +máquina... e incluso cambiará cada vez + +00:04:01.646 --> 00:04:05.726 align:middle +que paremos y arranquemos nuestro contenedor. +Pronto hablaremos de ello. Pero ahora + +00:04:06.086 --> 00:04:07.346 align:middle +que conocemos el puerto + +00:04:08.136 --> 00:04:14.536 align:middle +50700, podemos utilizarlo para conectarnos a la base de +datos. Por ejemplo, como estoy utilizando Postgres, podría + +00:04:15.076 --> 00:04:22.826 align:middle +ejecutar: Esto significa: conectar con Postgres +en el puerto 127.0.0.1 50700 utilizando el usuario + +00:04:22.826 --> 00:04:35.986 align:middle +symfony y hablando con la base de +datos app. Todo esto está configurado + +00:04:35.986 --> 00:04:41.066 align:middle +en el archivo docker-compose.yml. +Copia la contraseña de ChangeMe + +00:04:41.066 --> 00:04:56.226 align:middle +porque esa última bandera le dice a Postgres +que te pida esa contraseña. Pégala y... + +00:04:56.846 --> 00:04:57.926 align:middle +¡estamos dentro! + +00:04:58.276 --> 00:05:04.796 align:middle +Si utilizas MySQL, podemos hacer esto mismo con +un comando mysql. Pero esto sólo funciona si + +00:05:05.446 --> 00:05:12.156 align:middle +tenemos ese comando psql instalado en nuestra +máquina local. Así que vamos a probar con + +00:05:12.676 --> 00:05:14.256 align:middle +otro comando. Ejecuta docker-compose ps de + +00:05:14.846 --> 00:05:16.826 align:middle +nuevo. El contenedor se + +00:05:17.636 --> 00:05:23.216 align:middle +llama database, que proviene de nuestro +archivo docker-compose.yml . Así podremos + +00:05:23.796 --> 00:05:30.586 align:middle +cambiar el comando anterior por: docker-compose +exec database psql --username symfony --password + +00:05:31.396 --> 00:05:35.796 align:middle +app Esta vez, estamos ejecutando el comando + +00:05:35.916 --> 00:05:39.916 align:middle +psql dentro del contenedor, por lo que + +00:05:40.376 --> 00:05:42.456 align:middle +no necesitamos instalarlo localmente. +Escribe ChangeMe como contraseña + +00:05:43.246 --> 00:05:45.826 align:middle +y... ¡vamos a estar dentro! La + +00:05:46.046 --> 00:05:47.586 align:middle +cuestión es: ¡sólo + +00:05:48.316 --> 00:05:51.866 align:middle +con ejecutar docker-compose +up, tenemos un contenedor de + +00:05:52.246 --> 00:05:56.456 align:middle +base de datos Postgres con el +que podemos hablar! Por cierto, + +00:05:57.216 --> 00:06:02.266 align:middle +cuando estés preparado para detener el contenedor más +adelante, podrás ejecut arlo: docker-compose stop Que + +00:06:02.816 --> 00:06:05.216 align:middle +básicamente apaga el +contenedor. O puedes ejecutar + +00:06:05.716 --> 00:06:08.436 align:middle +el más común docker-compose down que apaga + +00:06:08.436 --> 00:06:12.326 align:middle +los contenedores y los elimina. +Para volver a arrancar, + +00:06:13.076 --> 00:06:18.266 align:middle +es lo mismo: docker-compose up -d +Pero fíjate en que cuando volvemos + +00:06:18.266 --> 00:06:21.016 align:middle +a ejecutar docker-compose ps , ¡el puerto de + +00:06:21.606 --> 00:06:25.766 align:middle +mi máquina anfitriona es un puerto +aleatorio diferente! Así que, en + +00:06:26.446 --> 00:06:32.176 align:middle +teoría, podríamos configurar la variable +DATABASE_URLpara que apunte a nuestra base + +00:06:32.176 --> 00:06:36.366 align:middle +de datos Postgres, incluyendo +el uso del puerto correcto. + +00:06:37.106 --> 00:06:41.556 align:middle +¡Pero ese puerto aleatorio que sigue +cambiando va a ser molesto! Afortunadamente, + +00:06:42.236 --> 00:06:44.656 align:middle +¡hay un truco para esto! Resulta + +00:06:45.346 --> 00:06:50.716 align:middle +que nuestra aplicación ya está configurada, +¡sin que nosotros hagamos nada! Eso + +00:06:51.346 --> 00:06:52.316 align:middle +a continuación diff --git a/sfcasts/ep3-doctrine/es/docker-env-vars.md b/sfcasts/ep3-doctrine/es/docker-env-vars.md new file mode 100644 index 0000000..d37ff9a --- /dev/null +++ b/sfcasts/ep3-doctrine/es/docker-env-vars.md @@ -0,0 +1,55 @@ +# Docker y variables de entorno + +Ahora tenemos una base de datos Postgres ejecutándose dentro de un contenedor Docker. Podemos verlo ejecutando: + +```terminal +docker-compose ps +``` + +Esto también nos dice que si queremos hablar con esta base de datos, podemos conectarnos al puerto`50739` en nuestra máquina local. Ese será un puerto diferente para ti, porque se elige al azar cuando iniciamos Docker. + +También hemos aprendido que podemos hablar con la base de datos directamente a través de: + +```terminal +docker-compose exec database psql --user symfony --password app +``` + +Para conseguir que nuestra aplicación real apunte a la base de datos que se ejecuta en este puerto, podríamos entrar en `.env` o `.env.local` y personalizar `DATABASE_URL`en consecuencia: con el usuario `symfony` la contraseña `ChangeMe`... y con el puerto que tengas actualmente. Aunque... tendríamos que actualizar ese puerto cada vez que iniciemos y detengamos Docker. + +## El Binario de Symfony y variables de entorno de Docker + +Afortunadamente, no tenemos que hacer nada de eso porque, sorpresa, ¡la variable de entorno `DATABASE_URL`ya está correctamente configurada! Cuando configuramos nuestro proyecto, iniciamos un servidor local de desarrollo utilizando el binario de Symfony. + +Como recordatorio, voy a ejecutar + +```terminal +symfony server:stop +``` + +para detener ese servidor. Y luego reiniciarlo con: + +```terminal +symfony serve -d +``` + +Menciono esto porque el binario `symfony` tiene un superpoder de Docker bastante impresionante. + +Observa: cuando actualices ahora... y pases el ratón por la esquina inferior derecha de la barra de herramientas de depuración de la web, dirá "Env Vars: De Docker". + +En resumen, ¡el binario de Symfony se dio cuenta de que Docker se estaba ejecutando y expuso algunas nuevas variables de entorno que apuntaban a la base de datos! Te lo mostraré. Abre`public/index.php`. Normalmente no nos preocupamos por este archivo... pero es un buen lugar para volcar algo de información justo cuando nuestra aplicación empieza a arrancar. Dentro de la llamada de retorno,`dd()` la superglobal `$_SERVER`. Esa variable contiene mucha información, incluyendo cualquier variable de entorno. + +Bien, gira y actualiza. ¡Una gran lista! Busca `DATABASE_URL` y... ¡ahí está! Pero ese no es el valor que tenemos en nuestro archivo `.env`: el puerto no es el que tenemos ahí. No, ¡es el puerto correcto necesario para hablar con el contenedor Docker! + +Sí, el binario de Symfony detecta que Docker se está ejecutando y establece una variable de entorno real`DATABASE_URL` que apunta a ese contenedor. Y recuerda que, al tratarse de una variable de entorno real, ganará a cualquier valor colocado en los archivos `.env` o `.env.local`. + +La cuestión es que, con sólo iniciar Docker, ya está todo configurado: no hemos tenido que tocar ningún archivo de configuración. Eso está muy bien. + +Por cierto, si quieres ver todas las variables de entorno que configura el binario de Symfony, puedes ejecutarlo: + +```terminal +symfony var:export --multiline +``` + +Pero por mucho la más importante es `DATABASE_URL`. + +Bien: ¡Doctrine está configurado! A continuación, vamos a crear la base de datos propiamente dicha mediante un comando `bin/console`. Cuando lo hagamos, aprenderemos un truco para hacerlo con las variables de entorno del binario de Symfony. diff --git a/sfcasts/ep3-doctrine/es/docker-env-vars.vtt b/sfcasts/ep3-doctrine/es/docker-env-vars.vtt new file mode 100644 index 0000000..3de8afd --- /dev/null +++ b/sfcasts/ep3-doctrine/es/docker-env-vars.vtt @@ -0,0 +1,178 @@ +WEBVTT + +00:00:01.006 --> 00:00:05.066 align:middle +Ahora tenemos una base de datos Postgres +ejecutándose dentro de un contenedor Docker. + +00:00:05.636 --> 00:00:12.386 align:middle +Podemos verlo ejecutando: docker-compose ps +Esto también nos dice que si queremos hablar + +00:00:12.386 --> 00:00:18.516 align:middle +con esta base de datos, podemos conectarnos +al puerto 50739 en nuestra máquina local. + +00:00:19.136 --> 00:00:24.546 align:middle +Ese será un puerto diferente para ti, porque +se elige al azar cuando iniciamos Docker. + +00:00:25.356 --> 00:00:28.616 align:middle +También hemos aprendido que podemos hablar +con la base de datos directamente a través de + +00:00:28.676 --> 00:00:33.386 align:middle +docker-compose exec +database psql --user symfony + +00:00:33.536 --> 00:00:40.006 align:middle +--password app Para conseguir que nuestra aplicación +real apunte a la base de datos que se ejecuta + +00:00:40.006 --> 00:00:44.756 align:middle +en este puerto, podríamos +entrar en .env o .env.local + +00:00:44.916 --> 00:00:50.936 align:middle +y personalizar DATABASE_URL en consecuencia: con +el usuario symfony la contraseña ChangeMe... + +00:00:51.476 --> 00:00:54.246 align:middle +y con el puerto que tengas actualmente. + +00:00:54.806 --> 00:00:59.896 align:middle +Aunque... tendríamos que actualizar ese puerto +cada vez que iniciemos y detengamos Docker. + +00:01:00.736 --> 00:01:05.796 align:middle +Por suerte, no necesitamos hacer +nada de eso porque, sorpresa, + +00:01:06.116 --> 00:01:11.696 align:middle +¡la variable de entorno DATABASE_URL +ya está correctamente configurada! + +00:01:12.546 --> 00:01:17.556 align:middle +Cuando configuramos nuestro proyecto, iniciamos un servidor +local de desarrollo utilizando el binario de Symfony. + +00:01:18.316 --> 00:01:22.976 align:middle +Como recordatorio, voy a ejecutar: symfony +server:stop para detener ese servidor. + +00:01:23.476 --> 00:01:29.016 align:middle +Y luego reiniciarlo con: +symfony serve -d Menciono esto + +00:01:29.316 --> 00:01:34.616 align:middle +porque el binario symfony tiene un +superpoder de Docker bastante impresionante. + +00:01:35.416 --> 00:01:37.116 align:middle +Observa: cuando actualices ahora... + +00:01:37.546 --> 00:01:39.936 align:middle +y pasas el ratón por la +esquina inferior derecha de + +00:01:39.936 --> 00:01:45.056 align:middle +la barra de herramientas de depuración +de la web, dice "Env Vars: De Docker". En + +00:01:45.776 --> 00:01:50.236 align:middle +resumen, ¡el binario de Symfony se dio +cuenta de que Docker se estaba ejecutando + +00:01:50.606 --> 00:01:56.116 align:middle +y expuso algunas nuevas variables de +entorno que apuntaban a la base de datos! + +00:01:56.786 --> 00:01:57.316 align:middle +Te lo mostraré. + +00:01:57.846 --> 00:01:59.726 align:middle +Abre public/index.php. + +00:01:59.726 --> 00:02:02.986 align:middle +Normalmente no nos interesa este archivo... + +00:02:03.366 --> 00:02:08.076 align:middle +pero es un buen lugar para volcar algo de información +justo cuando nuestra aplicación empieza a arrancar. + +00:02:08.946 --> 00:02:13.956 align:middle +Dentro de la llamada de retorno, +dd() la superglobal $_SERVER. + +00:02:14.686 --> 00:02:20.396 align:middle +Esa variable contiene mucha información, +incluyendo cualquier variable de entorno. + +00:02:21.146 --> 00:02:22.876 align:middle +Bien, gira y actualiza. + +00:02:23.566 --> 00:02:27.446 align:middle +¡Una gran lista! Busca DATABASE_URL y... + +00:02:27.976 --> 00:02:28.936 align:middle +¡ahí está! + +00:02:29.656 --> 00:02:37.216 align:middle +Pero ese no es el valor que tenemos en nuestro +archivo .env: el puerto no es el que tenemos ahí. + +00:02:37.816 --> 00:02:42.276 align:middle +No, ¡es el puerto correcto necesario +para hablar con el contenedor Docker! + +00:02:42.796 --> 00:02:45.916 align:middle +Sí, el binario de Symfony detecta +que Docker se está ejecutando + +00:02:46.266 --> 00:02:52.486 align:middle +y establece una variable de entorno real +DATABASE_URL que apunta a ese contenedor. + +00:02:53.316 --> 00:02:56.926 align:middle +Y recuerda que, al tratarse de +una variable de entorno real, + +00:02:57.246 --> 00:03:03.166 align:middle +ganará a cualquier valor colocado +en los archivos .env o .env.local. + +00:03:03.926 --> 00:03:09.176 align:middle +La cuestión es: con sólo iniciar +Docker, ya está todo configurado: + +00:03:09.876 --> 00:03:12.436 align:middle +no necesitamos tocar ningún +archivo de configuración. + +00:03:12.836 --> 00:03:13.996 align:middle +Eso está muy bien. Por + +00:03:14.926 --> 00:03:19.066 align:middle +cierto, si quieres ver todas las variables de +entorno que configura el binario de Symfony + +00:03:19.066 --> 00:03:22.426 align:middle +, puedes ejecutar: symfony var:export + +00:03:22.626 --> 00:03:27.316 align:middle +--multiline Pero por mucho la más +importante es DATABASE_URL. + +00:03:28.216 --> 00:03:30.526 align:middle +Bien: ¡Doctrine está configurado! + +00:03:31.206 --> 00:03:35.926 align:middle +A continuación, vamos a crear la base de datos +propiamente dicha mediante un comando bin/console. + +00:03:36.636 --> 00:03:39.056 align:middle +Cuando lo hagamos, aprenderemos +un truco para hacerlo + +00:03:39.056 --> 00:03:42.646 align:middle +con las variables de entorno +del binario de Symfony diff --git a/sfcasts/ep3-doctrine/es/entity.md b/sfcasts/ep3-doctrine/es/entity.md new file mode 100644 index 0000000..8d24256 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/entity.md @@ -0,0 +1,66 @@ +# Clase de entidad + +Una de las cosas más chulas, y quizá más sorprendentes, de Doctrine es que quiere que finjas que la base de datos no existe Sí, en lugar de pensar en tablas y columnas, Doctrine quiere que pensemos en objetos y propiedades. + +Por ejemplo, supongamos que queremos guardar los datos de un producto. La forma de hacerlo con Doctrine es crear una clase `Product` con propiedades que contengan los datos. Entonces instancias un objeto `Product`, pones los datos en él y pides amablemente a Doctrine que los guarde por ti. No tenemos que preocuparnos de cómo lo hace Doctrine. + +Pero, por supuesto, entre bastidores Doctrine está hablando con una base de datos. Insertará los datos del objeto `Product` en una tabla `product` en la que cada propiedad está asignada a una columna. Esto se llama mapeador relacional de objetos, o ORM. + +Más tarde, cuando queramos recuperar esos datos, no pensaremos en "consultar" esa tabla y sus columnas. No, simplemente le pedimos a Doctrine que encuentre el objeto que teníamos antes. Por supuesto, consultará la tabla... y luego volverá a crear el objeto con los datos. Pero no es un detalle en el que pensemos: pedimos el objeto `Product`, y nos lo da. Doctrine se encarga de guardar y consultar todo automáticamente. + +## Generación de la entidad con make:entity + +De todos modos, cuando utilizamos un ORM como Doctrine, si queremos guardar algo en la base de datos, tenemos que crear una clase que modele lo que queremos guardar, como una clase `Product`. En Doctrine, estas clases reciben un nombre especial: entidades, aunque en realidad son clases normales de PHP. Y aunque puedes crear estas clases de entidad a mano, hay un comando MakerBundle que te hace la vida mucho más agradable. + +Ve a tu terminal y ejecuta: + +```terminal +php bin/console make:entity +``` + +En este caso, no tenemos que ejecutar `symfony console make:entity` porque este comando no hablará con la base de datos: sólo genera código. Pero, si alguna vez no estás seguro, usar `symfony console` siempre es seguro. + +Bien, queremos crear una clase para almacenar todas las mezclas de vinilos de nuestro sistema. Así que vamos a crear una nueva clase llamada `VinylMix`. A continuación, responde a `no` para transmitir las actualizaciones de las entidades: es una característica extra relacionada con Symfony Turbo. + +Bien, aquí está la parte importante: nos pregunta qué propiedades queremos. Vamos a añadir varias. Empezamos con una llamada `title`. A continuación nos pregunta de qué tipo es este campo. Pulsa `?` para ver la lista completa. + +Estos son tipos de Doctrine... y cada uno de ellos se asignará a un tipo de columna diferente en tu base de datos, dependiendo de la base de datos que estés utilizando, como MySQL o Postgres. Los tipos básicos están en la parte superior como `string`, `text` - que puede contener más que una cadena) - `boolean`, `integer` y `float`. Luego los campos de relación -de los que hablaremos en el próximo tutorial-, algunos campos especiales, como el almacenamiento de JSON y los campos de fecha. + +Para `title`, utiliza `string`, que puede contener hasta 255 caracteres. Mantendré la longitud por defecto... luego nos pregunta si el campo puede ser nulo en la base de datos. Responderé `no`. Esto significa que la columna no puede ser nula. En otras palabras, la columna será obligatoria en la base de datos. + +Y... ¡un campo hecho! Vamos a añadir algunos más. Necesitamos un `description`, y que sea del tipo `text`. `string` tiene un máximo de 255 caracteres, `text` puede contener un montón más. Esta vez, diré `yes` para que sea anulable. Así será una columna opcional en la base de datos. ¡Otra más para abajo! + +Para la siguiente propiedad, llámala `trackCount`. Será un `integer` y será no nulo. Luego añade `genre`, como un `string`, de longitud 255... y también no nulo para que sea obligatorio en la base de datos. + +Por último, añade un campo `createdAt` para que podamos saber cuándo se creó originalmente cada mezcla de vinilo. Esta vez, como el nombre del campo termina en "At", el comando sugiere un tipo `datetime_immutable`. Pulsa "intro" para utilizarlo, y también para que no sea nulo en la base de datos. + +Ahora no necesitamos añadir más propiedades, así que pulsa "intro" una vez más para salir del comando. + +Ya está ¿Qué ha hecho esto? Bueno, en primer lugar, puedo decirte que esto no ha hablado con nuestra base de datos ni la ha modificado en absoluto. No, simplemente ha generado dos clases. La primera es `src/Entity/VinylMix.php`. La segunda es `src/Repository/VinylMixRepository.php`. Ignora la `Repository` por ahora... hablaremos de su propósito en unos minutos. + +[[[ code('33b9a4adde') ]]] + +## Comprobación de la clase y los atributos de la entidad + +Ve a abrir la entidad `VinylMix.php`. Saluda a... una... ¡vaya, una clase PHP bastante normal y aburrida! Generó una propiedad `private` para cada campo que añadimos, además de una propiedad extra `id`. El comando también añadió un método getter y setter para cada una de ellas. Así que... esto es básicamente una clase que contiene datos... y podemos acceder y establecer esos datos a través de los métodos getter y setter + +Lo único que hace especial a esta clase son los atributos. El `ORM\Entity`sobre la clase le dice a Doctrine: + +> Quiero poder guardar objetos de esta clase en la base de datos. Este +> es una entidad. + +Luego, encima de cada propiedad, utilizamos `ORM\Column` para decirle a Doctrine que queremos guardar esta propiedad como una columna en la tabla. Esto también comunica otras opciones como la longitud de la columna y si debe ser anulable o no.`nullable: false` es el valor por defecto... así que el comando sólo generó `nullable: true`en la única propiedad que lo necesita. + +La otra cosa que controla `ORM\Column` es el tipo de campo. Eso se establece mediante esta opción `type`. Como ya he dicho, esto no se refiere directamente a un tipo de MySQL o Postgres... es un tipo de Doctrine que luego se asignará a algo específico en función de nuestra base de datos. + +## Adivinar el tipo de campo + +Pero, interesante: la opción `type` sólo aparece en el campo `$description`. La razón de esto es realmente genial... ¡y nueva! Doctrine es inteligente. Mira el tipo de tu propiedad y adivina el tipo de campo a partir de él. Así que cuando tienes un tipo de propiedad `string`, Doctrine asume que quieres que ese sea su tipo `string`. Podrías escribir `Types::STRING` dentro de `ORM\Column`... pero eso sería totalmente redundante. + +Sin embargo, lo necesitamos para el campo `description`... porque queremos utilizar el tipo`TEXT`, no el tipo `STRING`. Pero en cualquier otra situación, funciona. Doctrine adivina el tipo correcto a partir del tipo de propiedad `?int`... y lo mismo ocurre aquí abajo para el tipo `?\DateTimeImmutable`. + +## Nombres de tablas y columnas + +Además de controlar las cosas de cada columna, también podemos controlar el nombre de la tabla añadiendo un `ORM\Table` encima de la clase con el nombre establecido, por ejemplo,`vinyl_mix`. Pero, ¡sorpresa! ¡No necesitamos hacer eso! ¿Por qué? Porque Doctrine es muy bueno generando grandes nombres. Genera el nombre de la tabla transformando la clase en caso de serpiente. Así que incluso sin `ORM\Table`, éste será el nombre de la tabla. Lo mismo ocurre con las propiedades. `$trackCount` se asignará a una columna`track_count`. Doctrine se encarga de todo esto por nosotros: no tenemos que pensar en absoluto en los nombres de nuestras tablas o columnas. + +Llegados a este punto, hemos ejecutado `make:entity` y nos ha generado una clase de entidad. Pero... todavía no tenemos una tabla `vinyl_mix` en nuestra base de datos. ¿Cómo creamos una? Con la magia de las migraciones de bases de datos. Eso a continuación. diff --git a/sfcasts/ep3-doctrine/es/entity.vtt b/sfcasts/ep3-doctrine/es/entity.vtt new file mode 100644 index 0000000..dba9aa5 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/entity.vtt @@ -0,0 +1,470 @@ +WEBVTT + +00:00:01.036 --> 00:00:06.656 align:middle +Una de las cosas más chulas, y quizá más +sorprendentes, de Doctrine es que quiere que + +00:00:06.656 --> 00:00:09.896 align:middle +finjas que la base de datos no existe + +00:00:10.606 --> 00:00:13.786 align:middle +Sí, en lugar de pensar en tablas y columnas, + +00:00:14.046 --> 00:00:17.376 align:middle +Doctrine quiere que pensemos +en objetos y propiedades. + +00:00:18.136 --> 00:00:22.286 align:middle +Por ejemplo, supongamos que queremos guardar +los datos de un producto. La forma de + +00:00:22.946 --> 00:00:26.666 align:middle +hacerlo con Doc trine es +creando una clase Product + +00:00:26.886 --> 00:00:29.056 align:middle +con propiedades que contengan los datos. + +00:00:29.676 --> 00:00:33.396 align:middle +Luego instanciamos un objeto +Product, le ponemos los datos + +00:00:33.776 --> 00:00:37.256 align:middle +y pedimos amablemente a Doctrine +que los guarde por nosotros. + +00:00:37.776 --> 00:00:40.966 align:middle +No tenemos que preocuparnos +de cómo lo hace Doctrine. + +00:00:41.646 --> 00:00:46.586 align:middle +Pero, por supuesto, entre bastidores Doctrine +está hablando con una base de datos. + +00:00:46.986 --> 00:00:51.516 align:middle +Insertará los datos del objeto +Product en una tabla product en + +00:00:52.086 --> 00:00:54.696 align:middle +la que cada propiedad está +asignada a una columna. + +00:00:55.246 --> 00:00:58.976 align:middle +Esto se llama mapeador +relacional de objetos, o ORM. + +00:00:59.986 --> 00:01:02.616 align:middle +Más tarde, cuando queramos +recuperar esos datos, no pensaremos + +00:01:02.946 --> 00:01:06.146 align:middle +en "consultar" esa tabla y sus columnas. + +00:01:06.646 --> 00:01:11.876 align:middle +No, simplemente pedimos a Doctrine que +encuentre el objeto que teníamos antes. + +00:01:12.716 --> 00:01:15.056 align:middle +Por supuesto, consultará la tabla... + +00:01:15.416 --> 00:01:18.056 align:middle +y luego volverá a crear +el objeto con los datos. + +00:01:18.746 --> 00:01:25.876 align:middle +Pero no es un detalle en el que pensemos: +le pedimos el objeto Product, y nos lo da. + +00:01:26.446 --> 00:01:29.596 align:middle +Doctrine se encarga de guardar y +consultar todo automáticamente. De + +00:01:30.406 --> 00:01:35.876 align:middle +todos modos, cuando utilizamos un ORM como Doc trine +, si queremos guardar algo en la base de datos, + +00:01:36.276 --> 00:01:42.036 align:middle +tenemos que crear una clase que modele lo +que queremos guardar, como una clase Product. + +00:01:42.836 --> 00:01:46.876 align:middle +En Doctrine, estas clases reciben +un nombre especial: entidades. + +00:01:47.546 --> 00:01:50.416 align:middle +Aunque, en realidad, son +clases normales de PHP. + +00:01:50.966 --> 00:01:54.426 align:middle +Y aunque puedes crear estas +clases de entidad a mano, hay + +00:01:54.746 --> 00:01:58.456 align:middle +un comando MakerBundle que te +hace la vida mucho más agradable. + +00:01:59.246 --> 00:02:04.196 align:middle +Ve a tu terminal y ejecuta: php +bin/console make:entity En este caso, + +00:02:04.196 --> 00:02:09.546 align:middle +no tenemos que ejecutar symfony console +make:entity porque este comando no hablará + +00:02:09.546 --> 00:02:12.096 align:middle +con la base de datos: sólo genera código. + +00:02:12.516 --> 00:02:16.686 align:middle +Pero, si alguna vez no estás seguro, +usar symfony console siempre es seguro. + +00:02:17.536 --> 00:02:22.456 align:middle +Bien, queremos crear una clase para almacenar +todas las mezclas de vinilos de nuestro sistema. + +00:02:22.806 --> 00:02:25.736 align:middle +Así que vamos a crear una +nueva clase llamada VinylMix. + +00:02:26.406 --> 00:02:29.506 align:middle +A continuación, responde a no para transmitir +las actualizaciones de las entidades: + +00:02:29.806 --> 00:02:32.586 align:middle +que es una característica extra +relacionada con Symfony Turbo. + +00:02:33.476 --> 00:02:37.966 align:middle +Bien, aquí está la parte importante: +nos pregunta qué propiedades queremos. + +00:02:38.536 --> 00:02:40.126 align:middle +Vamos a añadir varias. + +00:02:40.606 --> 00:02:42.016 align:middle +Empezamos con una llamada title. + +00:02:42.716 --> 00:02:45.576 align:middle +A continuación nos pregunta +de qué tipo es este campo. + +00:02:45.976 --> 00:02:47.966 align:middle +Pulsa ? para ver la lista completa. + +00:02:48.646 --> 00:02:50.816 align:middle +Son tipos de Doctrine... + +00:02:51.176 --> 00:02:55.136 align:middle +y cada uno se asignará a un tipo de +columna diferente en tu base de datos, + +00:02:55.346 --> 00:02:59.876 align:middle +dependiendo de la base de datos que +estés utilizando, como MySQL o Postgres. + +00:03:00.646 --> 00:03:05.446 align:middle +Los tipos básicos están en la parte superior +como string, text - que puede contener más + +00:03:05.446 --> 00:03:07.936 align:middle +que una cadena) - boolean, integer y float. + +00:03:08.776 --> 00:03:12.686 align:middle +Luego los campos de relación -de los +que hablaremos en el próximo tutorial-, + +00:03:13.166 --> 00:03:16.906 align:middle +algunos campos especiales, como el +almacenamiento de JSON y los campos de fecha. + +00:03:17.716 --> 00:03:22.496 align:middle +Para title, utiliza string, que +puede contener hasta 255 caracteres. + +00:03:23.046 --> 00:03:24.696 align:middle +Mantendré la longitud por defecto... + +00:03:25.246 --> 00:03:29.246 align:middle +entonces nos pregunta si el campo +puede ser nulo en la base de datos. + +00:03:29.686 --> 00:03:31.186 align:middle +Responderé no. + +00:03:31.886 --> 00:03:34.756 align:middle +Esto significa que la +columna no puede ser nula. + +00:03:35.336 --> 00:03:39.316 align:middle +En otras palabras, la columna será +obligatoria en la base de datos. + +00:03:39.946 --> 00:03:42.386 align:middle +Y... ¡un campo hecho! + +00:03:43.256 --> 00:03:44.126 align:middle +Vamos a añadir algunos más. + +00:03:44.576 --> 00:03:48.356 align:middle +Necesitamos un description, +y que sea del tipo text. + +00:03:48.946 --> 00:03:54.026 align:middle +string tiene un máximo de 255 caracteres, +text puede contener un montón más. + +00:03:54.946 --> 00:03:57.746 align:middle +Esta vez, diré yes para que sea anulable. + +00:03:58.346 --> 00:04:00.986 align:middle +Así será una columna +opcional en la base de datos. + +00:04:01.716 --> 00:04:02.926 align:middle +¡Otra más para abajo! + +00:04:03.576 --> 00:04:06.526 align:middle +Para la siguiente propiedad, +llámala trackCount. + +00:04:07.126 --> 00:04:10.526 align:middle +Será un integer y será no nulo. + +00:04:11.176 --> 00:04:14.676 align:middle +Luego añade genre, como un +string, de longitud 255... + +00:04:15.316 --> 00:04:18.616 align:middle +y también not null para que sea +obligatorio en la base de datos. + +00:04:19.496 --> 00:04:26.216 align:middle +Por último, añade un campo createdAt para que podamos +saber cuándo se creó originalmente cada mezcla de vinilo. + +00:04:27.076 --> 00:04:33.836 align:middle +Esta vez, como el nombre del campo termina en "At", +el comando sugiere un tipo datetime_immutable. + +00:04:34.516 --> 00:04:38.586 align:middle +Pulsa "intro" para utilizarlo, y también para +que no sea nulo en la base de datos. Ahora no + +00:04:39.466 --> 00:04:42.296 align:middle +necesitamos añadir más +propiedades, así que pulsa "intro" + +00:04:42.386 --> 00:04:44.726 align:middle +una vez más para salir del comando. + +00:04:45.576 --> 00:04:48.306 align:middle +Ya está ¿Qué ha hecho esto? + +00:04:48.806 --> 00:04:55.656 align:middle +Bueno, en primer lugar, puedo decirte que esto no habló +con nuestra base de datos ni la modificó en absoluto. + +00:04:56.376 --> 00:04:59.786 align:middle +No, simplemente ha generado dos clases. + +00:05:00.446 --> 00:05:04.246 align:middle +La primera es src/Entity/VinylMix.php. + +00:05:04.246 --> 00:05:10.126 align:middle +La segunda es +src/Repository/VinylMixRepository.php. + +00:05:10.126 --> 00:05:12.586 align:middle +Ignora el Repository por ahora... + +00:05:12.946 --> 00:05:15.116 align:middle +hablaremos de su propósito en unos minutos. + +00:05:15.806 --> 00:05:18.156 align:middle +Ve a abrir la entidad VinylMix.php. + +00:05:18.776 --> 00:05:19.856 align:middle +Saluda a... + +00:05:20.166 --> 00:05:24.726 align:middle +una... ¡vaya, una clase PHP +bastante normal y aburrida! + +00:05:25.316 --> 00:05:30.756 align:middle +Generó una propiedad private para cada campo +que añadimos, además de una propiedad extra id. + +00:05:31.316 --> 00:05:35.306 align:middle +El comando también añadió un método +getter y setter para cada una de ellas. + +00:05:35.946 --> 00:05:40.106 align:middle +Así que... esto es básicamente +una clase que contiene datos... + +00:05:40.546 --> 00:05:47.566 align:middle +y podemos acceder y establecer esos datos +mediante los métodos getter y setter Lo único + +00:05:47.566 --> 00:05:50.836 align:middle +que hace especial a esta +clase son los atributos. + +00:05:51.446 --> 00:05:55.406 align:middle +El ORM\Entity sobre la clase +le dice a Doctrine: ¡Oye! + +00:05:55.766 --> 00:06:00.176 align:middle +Quiero poder guardar objetos de +esta clase en la base de datos. + +00:06:00.636 --> 00:06:02.256 align:middle +Esto es una entidad. + +00:06:02.946 --> 00:06:09.696 align:middle +Luego, encima de cada propiedad, utilizamos +ORM\Column para decirle a Doctrine que queremos + +00:06:09.696 --> 00:06:13.026 align:middle +guardar esta propiedad como +una columna en la tabla. + +00:06:13.836 --> 00:06:18.276 align:middle +Esto también comunica otras opciones +como la longitud de la columna + +00:06:18.706 --> 00:06:21.186 align:middle +y si debe ser anulable o no. + +00:06:21.806 --> 00:06:23.936 align:middle +nullable: false es el valor por defecto... + +00:06:24.396 --> 00:06:30.036 align:middle +para que el comando sólo genere nullable: +true en la única propiedad que lo necesita. + +00:06:30.916 --> 00:06:34.536 align:middle +La otra cosa que controla +ORM\Column es el tipo de campo. + +00:06:35.106 --> 00:06:37.346 align:middle +Eso se establece a través +de esta opción type. + +00:06:38.146 --> 00:06:44.036 align:middle +Como he mencionado, esto no se refiere +directamente a un tipo MySQL o Postgres... + +00:06:44.626 --> 00:06:50.786 align:middle +es un tipo Doctrine que luego se asignará a algo +específico en función de nuestra base de datos. + +00:06:51.516 --> 00:06:56.866 align:middle +Pero, interesante: la opción type +sólo aparece en el campo $description. + +00:06:57.746 --> 00:06:59.856 align:middle +La razón de esto es realmente genial... + +00:07:00.176 --> 00:07:03.336 align:middle +¡y nueva! Doctrine es inteligente. + +00:07:03.806 --> 00:07:10.516 align:middle +Mira el tipo de tu propiedad y adivina +el tipo de campo a partir de él. + +00:07:11.246 --> 00:07:13.276 align:middle +Así que cuando tienes un +tipo de propiedad string, + +00:07:13.746 --> 00:07:17.266 align:middle +Doctrine asume que quieres +que ese sea su tipo string. + +00:07:18.046 --> 00:07:22.776 align:middle +Podrías escribir Types::STRING +dentro de ORM\Column... + +00:07:23.306 --> 00:07:26.096 align:middle +pero eso sería totalmente redundante. + +00:07:26.786 --> 00:07:29.806 align:middle +Sin embargo, lo necesitamos +para el campo description... + +00:07:30.246 --> 00:07:34.366 align:middle +porque queremos utilizar el +tipo TEXT, no el tipo STRING. + +00:07:34.986 --> 00:07:37.946 align:middle +Pero en cualquier otra situación, funciona. + +00:07:38.506 --> 00:07:41.506 align:middle +Doctrine adivina el tipo correcto a +partir del tipo de propiedad ?int... + +00:07:42.016 --> 00:07:45.936 align:middle +y lo mismo ocurre aquí abajo +para el tipo ?\DateTimeImmutable. + +00:07:46.786 --> 00:07:52.246 align:middle +Además de controlar las cosas de cada +columna, también podemos controlar el nombre + +00:07:52.246 --> 00:08:00.926 align:middle +de la tabla añadiendo un ORM\Table encima de la clase +con el nombre establecido, por ejemplo, vinyl_mix. + +00:08:01.546 --> 00:08:03.216 align:middle +Pero, ¡sorpresa! + +00:08:03.676 --> 00:08:04.906 align:middle +¡No necesitamos hacer eso! + +00:08:05.576 --> 00:08:10.146 align:middle +¿Por qué? Porque Doctrine es muy +bueno generando grandes nombres. + +00:08:10.816 --> 00:08:15.906 align:middle +Genera el nombre de la tabla transformando +la clase en caso de serpiente. + +00:08:16.416 --> 00:08:21.916 align:middle +Así que incluso sin ORM\Table, +éste será el nombre de la tabla. + +00:08:22.746 --> 00:08:24.426 align:middle +Lo mismo ocurre con las propiedades. + +00:08:24.826 --> 00:08:29.026 align:middle +$trackCount se asignará a +una columna de track_count. + +00:08:29.726 --> 00:08:33.596 align:middle +Doctrine se encarga de todo esto +por nosotros: no tenemos que + +00:08:33.596 --> 00:08:36.386 align:middle +pensar en absoluto en los nombres +de nuestras tablas o columnas. + +00:08:37.316 --> 00:08:42.816 align:middle +Llegados a este punto, hemos ejecutado make:entity +y nos ha generado una clase de entidad. + +00:08:43.286 --> 00:08:45.276 align:middle +¡Bien! Pero... + +00:08:45.276 --> 00:08:51.046 align:middle +todavía no tenemos una tabla +vinyl_mix en nuestra base de datos. + +00:08:51.716 --> 00:08:53.246 align:middle +¿Cómo creamos una? + +00:08:53.776 --> 00:08:57.116 align:middle +Con la magia de las +migraciones de bases de datos. + +00:08:57.376 --> 00:08:58.406 align:middle +Eso a continuación diff --git a/sfcasts/ep3-doctrine/es/fixtures.md b/sfcasts/ep3-doctrine/es/fixtures.md new file mode 100644 index 0000000..393becc --- /dev/null +++ b/sfcasts/ep3-doctrine/es/fixtures.md @@ -0,0 +1,50 @@ +# Accesorios de datos sencillos de Doctrine + +se denomina "Data Fixtures" a los datos ficticios que añades a tu aplicación mientras desarrollas o ejecutas pruebas para facilitarte la vida. Es mucho más agradable trabajar en una nueva función cuando realmente tienes datos decentes en tu base de datos. Hemos creado algunos datos fijos, en cierto sentido, mediante esta acción de `new`. Pero Doctrine tiene un sistema específicamente diseñado para esto. +# Instalar DoctrineFixturesBundle + +Busca "doctrinefixturesbundle" para encontrar su repositorio en GitHub. Y puedes leer su documentación en Symfony.com. Copia la línea de instalación y, en tu terminal, ejecútala: + +```terminal +composer require --dev orm-fixtures +``` + +`orm-fixtures` es, por supuesto, un alias de Flex, en este caso, a`doctrine/doctrine-fixtures-bundle`. Y... ¡listo! Ejecuta + +```terminal +git status +``` + +para ver que esto ha añadido un bundle, así como un nuevo directorio `src/DataFixtures/`. Ve a abrirlo. Dentro, tenemos un único archivo nuevo llamado `AppFixtures.php`. + +[[[ code('766a29102b') ]]] + +DoctrineFixturesBundle es un bundle deliciosamente sencillo. Nos proporciona un nuevo comando de consola llamado `doctrine:fixtures:load`. Cuando lo ejecutemos, vaciará nuestra base de datos y luego ejecutará el método `load()` dentro de `AppFixtures`. Bueno, en realidad ejecutará el método `load()` en cualquier servicio que tengamos que extienda esta clase `Fixture`. Así que podemos tener varias clases en este directorio si queremos. + +Si lo ejecutamos ahora mismo... con un método `load()` vacío, limpia nuestra base de datos, llama a ese método vacío y... ¡el resultado en la página "Examinar" es que no tenemos nada! + +```terminal-silent +php bin/console doctrine:fixtures:load +``` + +## Rellenando el método load() + +¡Eso no es muy interesante, así que vamos a rellenar ese método `load()`! Empieza en`MixController`: roba todo el código de `VinylMix`... y pégalo aquí. Pulsa "Ok" para añadir la declaración `use`. + +[[[ code('76e06fff28') ]]] + +Fíjate en que el método `load()` acepta algún argumento de `ObjectManager`. En realidad es el `EntityManager`, ya que estamos utilizando el ORM. Si miras aquí abajo, ya tiene la llamada `flush()`. Lo único que nos falta es la llamada `persist()`:`$manager->persist($mix)`. + +[[[ code('fb84406a7e') ]]] + +Así que la variable se llama aquí `$manager`... pero estas dos líneas son exactamente las que tiene nuestro controlador: `persist()` y `flush()`. + +Prueba de nuevo el comando: + +```terminal +php bin/console doctrine:fixtures:load +``` + +Vacía la base de datos, ejecuta nuestros accesorios y tenemos... ¡una nueva mezcla! + +Vale, esto es genial. Tenemos un nuevo comando `bin/console` para cargar cosas. Pero para el desarrollo, quiero un conjunto realmente rico de datos de fijos, como... tal vez 25 mezclas. Podríamos añadirlas a mano aquí... o incluso crear un bucle. Pero hay una forma mejor, a través de una biblioteca llamada "Foundry". ¡Vamos a explorarla a continuación! diff --git a/sfcasts/ep3-doctrine/es/fixtures.vtt b/sfcasts/ep3-doctrine/es/fixtures.vtt new file mode 100644 index 0000000..8c130fd --- /dev/null +++ b/sfcasts/ep3-doctrine/es/fixtures.vtt @@ -0,0 +1,164 @@ +WEBVTT + +00:00:01.046 --> 00:00:06.586 align:middle +se denomina " Data fixtures" a los datos ficticios +que añades a tu aplicación mientras desarrollas + +00:00:06.586 --> 00:00:09.056 align:middle +o ejecutas pruebas para facilitarte la vida. + +00:00:09.826 --> 00:00:15.356 align:middle +Es mucho más agradable trabajar en una nueva función +cuando realmente tienes datos decentes en tu base de datos. + +00:00:16.496 --> 00:00:20.856 align:middle +Hemos creado algunos datos fijos, en cierto +sentido, mediante esta acción de new. + +00:00:21.196 --> 00:00:25.166 align:middle +Pero Doctrine tiene un sistema +específicamente diseñado para esto. + +00:00:26.316 --> 00:00:30.206 align:middle +Busca "doctrinefixturesbundle" para +encontrar su repositorio en GitHub. + +00:00:31.636 --> 00:00:36.136 align:middle +Y puedes leer su documentación en Symfony.com. + +00:00:38.736 --> 00:00:44.136 align:middle +Copia la línea de instalación y, en tu +terminal, ejecútala: composer require + +00:00:44.136 --> 00:00:52.826 align:middle +--dev orm-fixtures orm-fixtures es, por +supuesto, un alias de Flex, en este caso, + +00:00:52.826 --> 00:00:55.366 align:middle +a doctrine/doctrine-fixtures-bundle. + +00:00:56.206 --> 00:00:57.616 align:middle +Y... ¡listo! + +00:00:58.586 --> 00:01:06.586 align:middle +Ejecuta git status para ver que esto ha añadido un +bundle, así como un nuevo directorio src/DataFixtures/. + +00:01:07.306 --> 00:01:08.206 align:middle +Ve a abrirlo. + +00:01:09.416 --> 00:01:13.096 align:middle +Dentro, tenemos un único archivo +nuevo llamado AppFixtures.php. + +00:01:13.096 --> 00:01:18.286 align:middle +DoctrineFixturesBundle es un +bundle deliciosamente sencillo. + +00:01:18.896 --> 00:01:22.776 align:middle +Nos proporciona un nuevo comando de +consola llamado doctrine:fixtures:load. + +00:01:23.796 --> 00:01:27.156 align:middle +Cuando lo ejecutemos, +vaciará nuestra base de datos + +00:01:27.376 --> 00:01:31.816 align:middle +y luego ejecutará el método +load() dentro de AppFixtures. + +00:01:32.766 --> 00:01:35.896 align:middle +Bueno, en realidad ejecutará el método load() + +00:01:35.896 --> 00:01:40.056 align:middle +en cualquier servicio que tengamos +que extienda esta clase Fixture. + +00:01:40.756 --> 00:01:44.786 align:middle +Así que podemos tener varias clases +en este directorio si queremos. + +00:01:46.196 --> 00:01:47.786 align:middle +Si lo ejecutamos ahora mismo... + +00:01:48.216 --> 00:01:55.226 align:middle +con un método load() vacío, limpia nuestra +base de datos, llama a ese método vacío y... + +00:01:55.226 --> 00:01:59.526 align:middle +el resultado en la página +"Examinar" es que no tenemos nada + +00:02:00.426 --> 00:02:04.436 align:middle +Eso no es muy interesante, así que +¡vamos a rellenar ese método load()! + +00:02:04.606 --> 00:02:09.746 align:middle +Empieza en MixController: roba +todo el código de VinylMix... + +00:02:10.086 --> 00:02:11.536 align:middle +y pégalo aquí. + +00:02:13.876 --> 00:02:15.626 align:middle +Pulsa "Ok" para añadir la declaración use. + +00:02:16.926 --> 00:02:20.746 align:middle +Fíjate en que el método load() acepta +algún argumento de ObjectManager. + +00:02:21.636 --> 00:02:25.766 align:middle +En realidad es el EntityManager, +ya que estamos utilizando el ORM. + +00:02:26.806 --> 00:02:30.086 align:middle +Si miras aquí abajo, ya +tiene la llamada flush(). + +00:02:30.526 --> 00:02:36.426 align:middle +Lo único que nos falta es la llamada +persist(): $manager->persist($mix). + +00:02:37.396 --> 00:02:40.136 align:middle +Así que la variable se llama aquí $manager... + +00:02:40.576 --> 00:02:46.026 align:middle +pero estas dos líneas son exactamente las que +tiene nuestro controlador: persist() y flush(). + +00:02:47.316 --> 00:02:53.476 align:middle +Prueba de nuevo el comando: php bin/console +doctrine:fixtures:load Vacía la base de datos, + +00:02:53.706 --> 00:02:56.746 align:middle +ejecuta nuestros accesorios y tenemos... + +00:02:56.746 --> 00:02:58.676 align:middle +¡una nueva mezcla! + +00:02:59.476 --> 00:03:01.226 align:middle +Vale, esto es genial. + +00:03:01.766 --> 00:03:04.336 align:middle +Tenemos un nuevo comando +bin/console para cargar cosas. + +00:03:04.646 --> 00:03:10.166 align:middle +Pero para el desarrollo, quiero un conjunto +realmente rico de datos fijos, como... + +00:03:10.276 --> 00:03:12.326 align:middle +tal vez 25 mezclas. + +00:03:13.266 --> 00:03:15.276 align:middle +Podríamos añadirlas a mano aquí... + +00:03:15.746 --> 00:03:17.276 align:middle +o incluso crear un bucle. + +00:03:17.986 --> 00:03:22.566 align:middle +Pero hay una forma mejor, a través +de una biblioteca llamada "Foundry". + +00:03:23.176 --> 00:03:24.556 align:middle +¡Vamos a explorarla a continuación! diff --git a/sfcasts/ep3-doctrine/es/forever-scroll.md b/sfcasts/ep3-doctrine/es/forever-scroll.md new file mode 100644 index 0000000..52d16ac --- /dev/null +++ b/sfcasts/ep3-doctrine/es/forever-scroll.md @@ -0,0 +1,47 @@ +# Desplazamiento por siempre con Turbo Frames + +¡Has llegado al último capítulo del tutorial de Doctrine! Este capítulo es... un bonus total. En lugar de hablar de Doctrine, vamos a aprovechar algo de JavaScript para convertir esta página en un "scroll eterno". Pero no te preocupes Hablaremos más de Doctrine en el próximo tutorial, cuando tratemos las Relaciones de Doctrine. + +Éste es el objetivo: en lugar de enlaces de paginación, quiero que esta página cargue nueve resultados como los que vemos en la página 1. Luego, cuando nos desplacemos hasta el final, quiero hacer una petición AJAX para mostrar los nueve resultados siguientes, y así sucesivamente. El resultado es un "scroll eterno". + +En el primer tutorial de esta serie, instalamos una biblioteca llamada Symfony UX Turbo, que habilitó un paquete de JavaScript llamado Turbo. Turbo convierte todos los clics de los enlaces y los envíos de formularios en llamadas AJAX, lo que nos proporciona una experiencia realmente agradable, similar a la de una aplicación de página única, sin hacer nada especial. + +Aunque esto es genial, Turbo tiene otros dos superpoderes opcionales: Turbo Frames y Turbo Streams. Puedes aprender todo sobre ellos en nuestro tutorial de Turbo. Pero vamos a dar una muestra rápida de cómo podríamos aprovechar los Turbo Frames para añadir un desplazamiento eterno sin escribir una sola línea de JavaScript. + +## conceptos básicos de los turbo-marcos + +Los marcos funcionan dividiendo partes de tu página en elementos separados de `turbo-frame`, que actúa de forma muy parecida a un `iframe`... si eres lo suficientemente viejo como para recordar aquellos. Cuando rodeas algo en un ``, cualquier clic dentro de ese marco sólo navegará por ese marco. + +Por ejemplo, abre la plantilla de esta página - `templates/vinyl/browse.html.twig` - y desplázate hasta donde tenemos nuestro bucle `for`. Añade un nuevo elemento `turbo-frame` justo aquí. La única regla de un turbo marco es que debe tener un ID único. Así que di`id="mix-browse-list"`, y luego ve hasta el final de esa fila y pega la etiqueta de cierre. Y, sólo por mi propia cordura, voy a aplicar una sangría a esa fila. + +[[[ code('cc5da7cb54') ]]] + +Bien, entonces... ¿qué hace eso? Si actualizas la página ahora, cualquier navegación dentro de este marco se queda dentro del marco. ¡Fíjate! Si hago clic en "2"... eso ha funcionado. Hizo una petición AJAX para la página 2, nuestra aplicación devolvió esa página HTML completa -incluyendo la cabecera, el pie de página y todo-, pero luego Turbo Frame encontró el `mix-browse-list``` correspondiente dentro de eso, cogió su contenido y lo puso aquí. + +Y aunque no es fácil de ver en este ejemplo, la única parte de la página que está cambiando es este elemento ``. Si yo... digamos... me meto con el título aquí arriba en mi página, y luego hago clic aquí abajo y vuelvo a la página 2... eso no actualiza esa parte de la página. Una vez más, funciona de forma muy parecida a los iframes, pero sin las rarezas. Podrías imaginar que esto se utiliza, por ejemplo, para alimentar un botón "Editar" que añada edición en línea. + +Pero en nuestra situación, esto no es muy útil todavía... porque funciona más o menos igual que antes: hacemos clic en el enlace, vemos nuevos resultados. La única diferencia es que al hacer clic dentro de un `` no se cambia la URL. Así que, independientemente de la página en la que me encuentre, si actualizo, soy transportado de nuevo a la página 1. Así que esto fue una especie de paso atrás + +Pero sigue conmigo. Tengo una solución, pero implica unas cuantas piezas. Para empezar, voy a hacer que el ID sea único para la página actual. Añade un `-`, y entonces podremos decir `pager.currentPage`. + +[[[ code('310c7b233c') ]]] + +A continuación, en la parte inferior, elimina los enlaces de Pagerfanta y sustitúyelos por otro Marco Turbo. Di `{% if pager.hasNextPage %}`, y dentro de él, añade un`turbo-frame`, igual que arriba, con ese mismo `id="mix-browse-list-{{ }}"`. Pero esta vez, di `pager.nextPage`. Permíteme dividir esto en varias líneas aquí... y también vamos a decirle qué `src` debe utilizar para ello. Oh, déjame arreglar mi error tipográfico... y luego utiliza otro ayudante de Pagerfanta llamado `pagerfanta_page_url` y pásale ese `pager` y luego `pager.nextPage`. Por último, añade `loading="lazy"`. + +[[[ code('9af460b3a9') ]]] + +¡Woh! Deja que me explique, porque esto es un poco salvaje. En primer lugar, uno de los superpoderes de un `` es que puedes darle un atributo `src` y dejarlo vacío. Esto le dice a Turbo: + +> ¡Oye! Voy a ser perezoso y empezar este elemento vacío... quizás porque es +> un poco pesado de cargar. Pero en cuanto este elemento sea visible para +> el usuario, haz una petición Ajax a esta URL para obtener su contenido. + +Así, este `` comenzará vacío... pero en cuanto nos desplacemos hasta él, Turbo hará una petición AJAX para la siguiente página de resultados. + +Por ejemplo, si este marco se está cargando para la página 2, la respuesta Ajax contendrá un `` con `id="mix-browse-list-2"`. El sistema Turbo Frame lo tomará de la respuesta Ajax y lo pondrá aquí, al final de nuestra lista. Y si hay una página 3, incluirá otro Turbo Frame aquí abajo que apuntará a la página 3. + +Todo esto puede parecer un poco loco, así que vamos a probarlo. Voy a desplazarme hasta la parte superior de la página, refresco y... ¡perfecto! Ahora desplázate hacia abajo y observa. Deberías ver que aparece una petición AJAX en la barra de herramientas de depuración de la web. Mientras nos desplazamos... aquí abajo... ¡ah! ¡Ahí está la petición AJAX! Vuelve a desplazarte hacia abajo y... hay una segunda petición AJAX: una para la página 2 y otra para la página 3. Si seguimos desplazándonos, nos quedamos sin resultados y llegamos al final de la página. + +Si eres nuevo en Turbo Frames, este concepto puede haber sido un poco confuso, pero puedes aprender más en nuestro tutorial de Turbo. Y un saludo a [una entrada del blog de AppSignal](https://blog.appsignal.com/2022/07/06/get-started-with-hotwire-in-your-ruby-on-rails-app) que introdujo esta genial idea. + +¡Muy bien, equipo! ¡Enhorabuena por haber terminado el curso de Doctrine! Espero que te sientas poderoso. ¡Deberías estarlo! La única parte importante que le falta a Doctrine ahora es la de Relaciones de Doctrine: poder asociar una entidad a otra mediante relaciones, como las de muchos a uno y muchos a muchos. Cubriremos todo eso en el próximo tutorial. Hasta entonces, si tienes alguna duda o tienes una gran adivinanza que quieras plantearnos, estamos a tu disposición en la sección de comentarios. ¡Muchas gracias, amigos! ¡Y hasta la próxima vez! diff --git a/sfcasts/ep3-doctrine/es/forever-scroll.vtt b/sfcasts/ep3-doctrine/es/forever-scroll.vtt new file mode 100644 index 0000000..f5242df --- /dev/null +++ b/sfcasts/ep3-doctrine/es/forever-scroll.vtt @@ -0,0 +1,399 @@ +WEBVTT + +00:00:01.036 --> 00:00:04.746 align:middle +¡Has llegado al último capítulo +del tutorial de Doctrine! + +00:00:05.276 --> 00:00:06.376 align:middle +Este capítulo es... + +00:00:06.666 --> 00:00:08.376 align:middle +un bonus total. + +00:00:08.846 --> 00:00:13.186 align:middle +En lugar de hablar de Doctrine , +vamos a aprovechar algo de JavaScript + +00:00:13.256 --> 00:00:16.216 align:middle +para convertir esta página +en un "scroll eterno". + +00:00:16.946 --> 00:00:17.546 align:middle +Pero no te preocupes + +00:00:17.916 --> 00:00:22.026 align:middle +Hablaremos más de Doctrine en el próximo tutorial, +cuando tratemos las Relaciones de Doctrine. Éste es el + +00:00:22.886 --> 00:00:26.186 align:middle +objetivo: en lugar de enlaces de paginación, + +00:00:26.456 --> 00:00:30.426 align:middle +quiero que esta página cargue nueve +resultados como los que vemos en la página 1. + +00:00:31.276 --> 00:00:35.266 align:middle +Luego, cuando nos desplacemos hasta el +final, quiero hacer una petición AJAX + +00:00:35.266 --> 00:00:38.276 align:middle +para mostrar los nueve resultados +siguientes, y así sucesivamente. + +00:00:38.856 --> 00:00:41.356 align:middle +El resultado es un "scroll eterno". + +00:00:42.226 --> 00:00:47.646 align:middle +En el primer tutorial de esta serie, instalamos +una biblioteca llamada Symfony UX Turbo, + +00:00:48.106 --> 00:00:51.286 align:middle +que habilitó un paquete de +JavaScript llamado Turbo. + +00:00:51.936 --> 00:00:56.656 align:middle +Turbo convierte todos los clics de los enlaces y +los envíos de formularios en llamadas AJAX, lo que + +00:00:57.126 --> 00:01:03.186 align:middle +nos proporciona una experiencia realmente agradable, similar a +la de una aplicación de página única, sin hacer nada especial. + +00:01:03.946 --> 00:01:08.166 align:middle +Aunque esto es genial, Turbo tiene otros dos + +00:01:08.386 --> 00:01:12.506 align:middle +superpoderes opcionales: +Turbo Frames y Turbo Streams. + +00:01:13.116 --> 00:01:16.116 align:middle +Puedes aprender todo sobre ellos +en nuestro tutorial de Turbo. + +00:01:16.816 --> 00:01:21.746 align:middle +Pero vamos a dar una muestra rápida de cómo podríamos +aprovechar los Turbo Frames para añadir un desplazamiento eterno + +00:01:21.906 --> 00:01:25.316 align:middle +sin escribir una sola línea de JavaScript. + +00:01:26.316 --> 00:01:31.356 align:middle +Los marcos funcionan dividiendo partes de tu +página en elementos separados de turbo-frame, + +00:01:31.776 --> 00:01:34.426 align:middle +que actúa de forma muy parecida a un iframe... + +00:01:34.746 --> 00:01:36.876 align:middle +si eres lo suficientemente +mayor como para recordarlos. + +00:01:37.676 --> 00:01:40.116 align:middle +Cuando rodeas algo en un , + +00:01:40.516 --> 00:01:45.586 align:middle +cualquier clic dentro de ese marco +sólo navegará por ese marco. + +00:01:46.316 --> 00:01:51.226 align:middle +Por ejemplo, abre la plantilla de esta +página - templates/vinyl/browse.html.twig - + +00:01:51.616 --> 00:01:54.156 align:middle +y desplázate hasta donde +tenemos nuestro bucle for. + +00:01:54.916 --> 00:01:57.166 align:middle +Añade un nuevo elemento +turbo-frame justo aquí. + +00:01:57.906 --> 00:02:02.486 align:middle +La única regla de un turbo marco +es que debe tener un ID único. + +00:02:03.056 --> 00:02:08.796 align:middle +Así que di id="mix-browse-list", +y luego ve hasta el final + +00:02:08.796 --> 00:02:11.116 align:middle +de esa fila y pega la etiqueta de cierre. + +00:02:11.876 --> 00:02:16.756 align:middle +Y, sólo por mi propia cordura, voy +a aplicar una sangría a esa fila. + +00:02:17.476 --> 00:02:18.666 align:middle +Bien, entonces... + +00:02:18.666 --> 00:02:20.366 align:middle +¿qué hace eso? + +00:02:21.196 --> 00:02:28.016 align:middle +Si ahora actualizas la página, cualquier navegación +dentro de este marco se queda dentro del marco. + +00:02:28.576 --> 00:02:30.566 align:middle +¡Fíjate! Si hago clic en "2"... + +00:02:31.216 --> 00:02:32.306 align:middle +ha funcionado. + +00:02:32.716 --> 00:02:39.266 align:middle +Hizo una petición AJAX para la página 2, nuestra +aplicación devolvió esa página HTML completa + +00:02:39.686 --> 00:02:41.936 align:middle +-incluyendo la cabecera, +el pie de página y todo-, + +00:02:42.446 --> 00:02:49.316 align:middle +pero luego Turbo Frame encontró el mix-browse-list + correspondiente dentro de eso, + +00:02:49.696 --> 00:02:52.396 align:middle +cogió su contenido y lo puso aquí. + +00:02:53.186 --> 00:02:56.416 align:middle +Y aunque no es fácil de ver en este ejemplo, + +00:02:56.806 --> 00:03:01.906 align:middle +la única parte de la página que está +cambiando es este elemento . + +00:03:02.646 --> 00:03:04.056 align:middle +Si yo... digamos... + +00:03:04.176 --> 00:03:13.676 align:middle +cambiara el título aquí arriba en mi página, y luego +hiciera clic aquí abajo y volviera a la página 2... + +00:03:14.346 --> 00:03:17.176 align:middle +no actualizaría esa parte de la página. + +00:03:17.736 --> 00:03:22.266 align:middle +De nuevo, funciona muy parecido a +los iframes, pero sin las rarezas. + +00:03:23.036 --> 00:03:28.856 align:middle +Podrías imaginar usar esto, por ejemplo, para alimentar +un botón "Editar" que añada edición en línea. + +00:03:29.676 --> 00:03:33.406 align:middle +Pero en nuestra situación, +esto no es muy útil todavía... + +00:03:33.896 --> 00:03:40.726 align:middle +porque funciona más o menos igual que antes: +hacemos clic en el enlace, vemos nuevos resultados. + +00:03:41.316 --> 00:03:47.056 align:middle +La única diferencia es que al hacer clic dentro +de un no se cambia la URL. + +00:03:47.746 --> 00:03:54.276 align:middle +Así que, independientemente de la página en la que me +encuentre, si actualizo, soy transportado de nuevo a la página 1. + +00:03:55.086 --> 00:03:58.676 align:middle +Así que esto fue una especie de paso atrás + +00:03:59.016 --> 00:04:00.486 align:middle +Pero sigue conmigo. + +00:04:01.106 --> 00:04:04.876 align:middle +Tengo una solución, pero +implica unas cuantas piezas. + +00:04:05.726 --> 00:04:10.536 align:middle +Para empezar, voy a hacer que el ID +sea único para la página actual. + +00:04:11.146 --> 00:04:15.046 align:middle +Añade un -, y entonces podremos +decir pager.currentPage. + +00:04:15.916 --> 00:04:22.546 align:middle +A continuación, en la parte inferior, elimina los enlaces +de Pagerfanta y sustitúyelos por otro Marco Turbo. Di + +00:04:22.546 --> 00:04:30.376 align:middle +{% if pager.hasNextPage %}, y dentro de él, +añade un turbo-frame, igual que arriba, + +00:04:30.816 --> 00:04:34.566 align:middle +con ese mismo id="mix-browse-list-{{ }}". + +00:04:34.776 --> 00:04:39.376 align:middle +Pero esta vez, di pager.nextPage. + +00:04:40.106 --> 00:04:42.206 align:middle +Permíteme dividir esto +en varias líneas aquí... + +00:04:42.676 --> 00:04:46.536 align:middle +y también vamos a decirle qué +src debe utilizar para ello. + +00:04:47.256 --> 00:04:48.706 align:middle +Oh, déjame arreglar mi error tipográfico... + +00:04:49.226 --> 00:04:55.736 align:middle +y luego usaremos otro ayudante de Pagerfanta +llamado pagerfanta_page_url y le pasaremos + +00:04:55.736 --> 00:04:59.246 align:middle +ese pager y luego pager.nextPage. + +00:05:00.066 --> 00:05:03.286 align:middle +Por último, añade loading="lazy". + +00:05:04.046 --> 00:05:08.956 align:middle +¡Woh! Deja que me explique, +porque esto es un poco salvaje. + +00:05:09.616 --> 00:05:13.616 align:middle +En primer lugar, uno de los superpoderes +de un es + +00:05:13.616 --> 00:05:17.556 align:middle +que puedes darle un atributo +src y dejarlo vacío. + +00:05:18.286 --> 00:05:20.276 align:middle +Esto le dice a Turbo: ¡Oye! + +00:05:20.656 --> 00:05:23.926 align:middle +Voy a ser perezoso y empezar +este elemento vacío... + +00:05:24.406 --> 00:05:26.816 align:middle +quizás porque es un poco pesado de cargar. + +00:05:27.506 --> 00:05:31.566 align:middle +Pero en cuanto este elemento +sea visible para el usuario, + +00:05:32.006 --> 00:05:36.126 align:middle +haz una petición Ajax a esta +URL para obtener su contenido. + +00:05:36.886 --> 00:05:39.646 align:middle +Así, este +comenzará vacío... + +00:05:40.016 --> 00:05:42.446 align:middle +pero en cuanto nos desplacemos hasta él, + +00:05:42.946 --> 00:05:47.616 align:middle +Turbo hará una petición AJAX para la +siguiente página de resultados. Por + +00:05:48.336 --> 00:05:52.146 align:middle +ejemplo, si este marco se está +cargando para la página 2, + +00:05:52.616 --> 00:06:01.006 align:middle +la respuesta Ajax contendrá un + con id="mix-browse-list-2". + +00:06:01.676 --> 00:06:05.706 align:middle +El sistema Turbo Frame lo +tomará de la respuesta Ajax + +00:06:06.076 --> 00:06:08.786 align:middle +y lo pondrá aquí, al final de nuestra lista. + +00:06:09.446 --> 00:06:14.486 align:middle +Y si hay una página 3, +incluirá otro Turbo Frame + +00:06:14.486 --> 00:06:17.486 align:middle +aquí abajo que apuntará a la página 3. + +00:06:18.176 --> 00:06:21.916 align:middle +Todo esto puede parecer un poco +loco, así que vamos a probarlo. + +00:06:21.976 --> 00:06:25.916 align:middle +Voy a desplazarme hasta la parte +superior de la página, refresco y... + +00:06:26.376 --> 00:06:30.676 align:middle +¡perfecto! Ahora desplázate +hacia abajo y observa. + +00:06:31.186 --> 00:06:35.836 align:middle +Deberías ver que aparece una petición AJAX +en la barra de herramientas de depuración web. + +00:06:36.446 --> 00:06:37.786 align:middle +Mientras nos desplazamos... + +00:06:38.076 --> 00:06:38.906 align:middle +aquí abajo... + +00:06:39.256 --> 00:06:41.766 align:middle +¡ah! ¡Ahí está la petición AJAX! + +00:06:42.416 --> 00:06:44.886 align:middle +Vuelve a desplazarte hacia abajo y... + +00:06:45.746 --> 00:06:51.626 align:middle +hay una segunda petición AJAX: una para +la página 2 y otra para la página 3. + +00:06:52.466 --> 00:06:58.186 align:middle +Si seguimos desplazándonos, nos quedamos sin +resultados y llegamos al final de la página. + +00:06:58.886 --> 00:07:03.016 align:middle +Si eres nuevo en Turbo Frames, este +concepto puede haber sido un poco confuso, + +00:07:03.406 --> 00:07:05.716 align:middle +pero puedes aprender más en +nuestro tutorial de Turbo. + +00:07:06.426 --> 00:07:11.456 align:middle +Y un saludo a un post del blog de +AppSignal que introdujo esta genial idea. + +00:07:12.476 --> 00:07:13.046 align:middle +¡Muy bien, equipo! + +00:07:13.306 --> 00:07:15.846 align:middle +¡Enhorabuena por haber +terminado el curso de Doctrine! + +00:07:16.376 --> 00:07:18.316 align:middle +Espero que te sientas poderoso. + +00:07:18.646 --> 00:07:19.286 align:middle +¡Deberías estarlo! + +00:07:20.076 --> 00:07:24.226 align:middle +La única parte importante que le falta a +Doctrine ahora es la de Relaciones de Doctrine: + +00:07:24.746 --> 00:07:29.136 align:middle +poder asociar una entidad +a otra mediante relaciones, + +00:07:29.486 --> 00:07:31.616 align:middle +como las de muchos a uno y muchos a muchos. + +00:07:32.336 --> 00:07:34.976 align:middle +Cubriremos todo eso en el próximo tutorial. + +00:07:35.706 --> 00:07:40.746 align:middle +Hasta entonces, si tienes alguna duda o tienes una +gran adivinanza que quieras plantearnos, estamos a + +00:07:41.176 --> 00:07:43.216 align:middle +tu disposición en la sección de comentarios. + +00:07:43.976 --> 00:07:44.786 align:middle +¡Muchas gracias, amigos! + +00:07:45.006 --> 00:07:45.976 align:middle +¡Y hasta la próxima vez! diff --git a/sfcasts/ep3-doctrine/es/foundry.md b/sfcasts/ep3-doctrine/es/foundry.md new file mode 100644 index 0000000..44aa8fe --- /dev/null +++ b/sfcasts/ep3-doctrine/es/foundry.md @@ -0,0 +1,101 @@ +# Fundición: Accesorios que te encantarán + +Construir accesorios es bastante sencillo, pero algo aburrido. Y sería súper aburrido crear manualmente 25 mezclas dentro del método `load()`. Por eso vamos a instalar una impresionante biblioteca llamada "Foundry". Para ello, ejecuta: + +```terminal +composer require zenstruck/foundry --dev +``` + +Ejecuta: `--dev` porque sólo necesitamos esta herramienta cuando estamos desarrollando o ejecutando pruebas. Cuando termine, ejecuta + +```terminal +git status +``` + +para ver que la receta ha habilitado un bundle y también ha creado un archivo de configuración... que no necesitaremos mirar. + +## Fábricas: make:factory + +En resumen, Foundry nos ayuda a crear objetos de entidad. Es... casi más fácil verlo en acción. En primer lugar, para cada entidad de tu proyecto (ahora mismo, sólo tenemos una), necesitarás una clase fábrica correspondiente. Créala ejecutando + +```terminal +php bin/console make:factory +``` + +que es un comando Maker que viene de Foundry. Luego, puedes seleccionar para qué entidad quieres crear una fábrica... o generar una fábrica para todas tus entidades. Nosotros generaremos una para `VinylMix`. Y... eso creó un único archivo: `VinylMixFactory.php`. Vamos a comprobarlo:`src/Factory/VinylMixFactory.php`. + +[[[ code('f4cf88fd24') ]]] + +¡Qué bien! Encima de la clase, puedes ver que se describen un montón de métodos... que ayudarán a nuestro editor a saber qué superpoderes tiene esto. Esta fábrica es realmente buena para crear y guardar objetos `VinylMix`... o para crear muchos de ellos, o para encontrar uno al azar, o un conjunto al azar, o un rango al azar. ¡Uf! + +## getDefaults() + +El único código importante que vemos dentro de esta clase es `getDefaults()`, que devuelve los datos por defecto que deben utilizarse para cada propiedad cuando se crea un `VinylMix`. Hablaremos más de eso en un minuto. + +Pero antes... ¡vamos a avanzar a ciegas y a utilizar esta clase! En `AppFixtures`, borra todo y sustitúyelo por `VinylMixFactory::createOne()`. + +[[[ code('b572b3c087') ]]] + +¡Ya está! Gira y vuelve a cargar los accesorios con: + +```terminal +symfony console doctrine:fixtures:load +``` + +Y... ¡falla! Boo + +> Tipo de argumento esperado "DateTime", "null" dado en la ruta de la propiedad "createdAt" + +Nos está diciendo que algo intentó llamar a `setCreatedAt()` en `VinylMix`... pero en lugar de pasar un objeto `DateTime`, pasó `null`. Hmm. Dentro de`VinylMix`, si te desplazas hacia arriba y abres `TimestampableEntity`, ¡sí! Tenemos un método`setCreatedAt()` que espera un objeto `DateTime`. Algo llamado así... pero que pasa `null`. + +Esto ayuda a mostrar cómo funciona Foundry. Cuando llamamos a`VinylMixFactory::createOne()`, crea un nuevo `VinylMix` y luego le pone todos estos datos. Pero recuerda que todas estas propiedades son privadas. Así que no establece la propiedad del título directamente. En su lugar, llama a `setTitle()` y `setTrackCount()`Aquí abajo, para `createdAt` y `updatedAt`, llamó a `setCreatedAt()`y le pasó a `null`. + +En realidad, no necesitamos establecer estas dos propiedades porque las establecerá automáticamente el comportamiento timestampable. + +Si probamos esto ahora... + +```terminal-silent +symfony console doctrine:fixtures:load +``` + +¡Funciona! Y si vamos a ver nuestro sitio... impresionante. Esta mezcla tiene 928.000 pistas, un título aleatorio y 301 votos. Todo esto proviene del método `getDefaults()`. + +## Datos falsos con Faker + +Para generar datos interesantes, Foundry aprovecha otra biblioteca llamada "Faker", cuyo único trabajo es... crear datos falsos. Así que si quieres un texto falso, puedes decir `self::faker()->`, seguido de lo que quieras generar. Hay muchos métodos diferentes que puedes invocar en `faker()` para obtener todo tipo de datos falsos divertidos. ¡Es muy útil! + +## Creando muchos objetos + +Nuestra fábrica ha hecho un trabajo bastante bueno... pero vamos a personalizar las cosas para hacerlas un poco más realistas. En realidad, en primer lugar, tener un `VinylMix` sigue sin ser muy útil. Así que, dentro de `AppFixtures`, cambia esto por `createMany(25)`. + +[[[ code('68aa0810e8') ]]] + +Aquí es donde Foundry brilla. Si ahora recargamos nuestras instalaciones: + +```terminal-silent +symfony console doctrine:fixtures:load +``` + +Con una sola línea de código, ¡tenemos 25 accesorios aleatorios con los que trabajar! Sin embargo, los datos aleatorios podrían ser un poco mejores... así que vamos a mejorar eso. + +## Personalizar getDefaults() + +Dentro de `VinylMixFactory`, cambia el título. En lugar de `text()` -que a veces puede ser un muro de texto-, cambia a `words()`... y utiliza 5 palabras, y pasa true para que lo devuelva como una cadena. De lo contrario, el método `words()` devuelve una matriz. Para `trackCount`, sí queremos un número aleatorio, pero... probablemente un número entre 5 y 20. Para `genre`, vamos a buscar un `randomElement()` para elegir aleatoriamente `pop` o `rock`. Esos son los dos géneros con los que hemos trabajado hasta ahora. Y, vaya... asegúrate de llamar a esto como una función, ya está. Por último, para `votes`, elige un número aleatorio entre -50 y 50. + +[[[ code('8aa7ee7c72') ]]] + +¡Mucho mejor! Ah, y puedes ver que `make:factory` ha añadido aquí un montón de nuestras propiedades por defecto, pero no las ha añadido todas. Una de las que falta es`description`. Añádela: `'description' => self::faker()->` y luego usa `paragraph()`. Por último, para `slug`, no la necesitamos en absoluto porque se establecerá automáticamente. + +[[[ code('5a3fff72f5') ]]] + +¡Ufff! ¡Vamos a probarlo! Recarga los accesorios: + +```terminal-silent +symfony console doctrine:fixtures:load +``` + +Luego dirígete y actualiza. Esto se ve mucho mejor. Tenemos una imagen rota... pero eso es sólo porque la API que estoy utilizando tiene algunas "lagunas"... nada de lo que preocuparse. + +Foundry puede hacer un montón de cosas interesantes, así que no dudes en consultar su documentación. Es especialmente útil cuando se escriben pruebas, y funciona muy bien con las relaciones de la base de datos. Así que lo volveremos a ver de forma más compleja en el próximo tutorial. + +A continuación, ¡vamos a añadir la paginación! Porque, al final, no podremos listar todas las mezclas de nuestra base de datos de una sola vez. diff --git a/sfcasts/ep3-doctrine/es/foundry.vtt b/sfcasts/ep3-doctrine/es/foundry.vtt new file mode 100644 index 0000000..29c5102 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/foundry.vtt @@ -0,0 +1,351 @@ +WEBVTT + +00:00:01.016 --> 00:00:04.656 align:middle +Construir accesorios es bastante +sencillo, pero algo aburrido. + +00:00:05.126 --> 00:00:11.886 align:middle +Y sería súper aburrido crear manualmente +25 mezclas dentro del método load(). + +00:00:12.506 --> 00:00:16.826 align:middle +Por eso vamos a instalar una impresionante +biblioteca llamada "Foundry". + +00:00:17.476 --> 00:00:23.566 align:middle +Para ello, ejecuta: composer require +zenstruck/foundry --dev Ejecuta: + +00:00:23.656 --> 00:00:28.796 align:middle +--dev porque sólo necesitamos esta herramienta +cuando estamos desarrollando o ejecutando pruebas. + +00:00:29.376 --> 00:00:34.546 align:middle +Cuando termine, ejecuta git status para +ver que la receta ha habilitado un bundle + +00:00:35.066 --> 00:00:37.626 align:middle +y también ha creado un +archivo de configuración... + +00:00:37.976 --> 00:00:39.886 align:middle +que no necesitaremos mirar. + +00:00:40.686 --> 00:00:45.306 align:middle +En resumen, Foundry nos ayuda +a crear objetos de entidad. + +00:00:45.896 --> 00:00:49.006 align:middle +Es... casi más fácil verlo en acción. + +00:00:49.686 --> 00:00:54.786 align:middle +En primer lugar, para cada entidad de tu +proyecto (ahora mismo, sólo tenemos una), + +00:00:55.286 --> 00:00:58.526 align:middle +necesitarás una clase +fábrica correspondiente. + +00:00:59.446 --> 00:01:02.156 align:middle +Créala ejecutando php bin/console make:factory + +00:01:02.516 --> 00:01:05.656 align:middle +que es un comando Maker que viene de Foundry. + +00:01:06.436 --> 00:01:10.516 align:middle +Luego, puedes seleccionar para qué +entidad quieres crear una fábrica... + +00:01:10.976 --> 00:01:14.276 align:middle +o generar una fábrica +para todas tus entidades. + +00:01:14.816 --> 00:01:16.586 align:middle +Nosotros generaremos una para VinylMix. + +00:01:17.246 --> 00:01:22.686 align:middle +Y... eso creó un único +archivo: VinylMixFactory.php. + +00:01:22.686 --> 00:01:27.556 align:middle +Vamos a comprobarlo: +src/Factory/VinylMixFactory.php. + +00:01:27.556 --> 00:01:32.786 align:middle +¡Qué bien! Encima de la clase, puedes ver +que se describen un montón de métodos... + +00:01:33.276 --> 00:01:37.826 align:middle +que ayudarán a nuestro editor a +saber qué superpoderes tiene esto. + +00:01:38.656 --> 00:01:44.146 align:middle +Esta fábrica es realmente buena para +crear y guardar objetos de VinylMix... + +00:01:44.656 --> 00:01:53.426 align:middle +o crear muchos de ellos, o encontrar uno al +azar, o un conjunto al azar, o un rango al azar. + +00:01:53.686 --> 00:01:59.486 align:middle +¡Uf! El único código importante que +vemos dentro de esta clase es getDefaults(), + +00:01:59.916 --> 00:02:05.026 align:middle +que devuelve los datos por defecto que deben utilizarse +para cada propiedad cuando se crea un VinylMix. + +00:02:05.646 --> 00:02:07.306 align:middle +Hablaremos más de eso dentro de un minuto. + +00:02:07.946 --> 00:02:09.186 align:middle +Pero antes... + +00:02:09.426 --> 00:02:13.446 align:middle +¡vamos a avanzar a ciegas +y a utilizar esta clase! + +00:02:14.126 --> 00:02:21.356 align:middle +En AppFixtures, borra todo y sustitúyelo +por VinylMixFactory::createOne(). + +00:02:22.156 --> 00:02:23.296 align:middle +¡Ya está! + +00:02:23.906 --> 00:02:30.766 align:middle +Gira y vuelve a cargar los accesorios con: +symfony console doctrine:fixtures:load Y... + +00:02:31.196 --> 00:02:38.766 align:middle +¡falla! Boo Tipo de argumento esperado +"DateTime", "null" dado en la ruta de la propiedad + +00:02:38.866 --> 00:02:45.666 align:middle +"createdAt" Nos está diciendo que algo +intentó llamar a setCreatedAt() en VinylMix... + +00:02:46.086 --> 00:02:50.866 align:middle +pero en lugar de pasar un +objeto DateTime, pasó null. + +00:02:51.446 --> 00:02:59.526 align:middle +Hmm. Dentro de VinylMix, si te desplazas hacia +arriba y abres TimestampableEntity, ¡sí! + +00:03:00.076 --> 00:03:05.056 align:middle +Tenemos un método setCreatedAt() +que espera un objeto DateTime. + +00:03:05.646 --> 00:03:07.026 align:middle +Algo llamado así... + +00:03:07.246 --> 00:03:08.656 align:middle +pero que pasa a null. + +00:03:09.516 --> 00:03:12.626 align:middle +Esto ayuda a mostrar cómo funciona Foundry. + +00:03:13.216 --> 00:03:18.366 align:middle +Cuando llamamos a VinylMixFactory::createOne(), +crea un nuevo VinylMix + +00:03:18.646 --> 00:03:21.736 align:middle +y luego le pone todos estos datos. + +00:03:22.356 --> 00:03:26.476 align:middle +Pero recuerda que todas estas +propiedades son privadas. + +00:03:26.996 --> 00:03:29.716 align:middle +Así que no establece la propiedad +del título directamente. + +00:03:30.186 --> 00:03:37.756 align:middle +En su lugar, llama a setTitle() y setTrackCount() +Aquí abajo, para createdAt y updatedAt, + +00:03:38.126 --> 00:03:41.526 align:middle +llamó a setCreatedAt() y le pasó a null. + +00:03:42.346 --> 00:03:46.586 align:middle +En realidad, no necesitamos +establecer estas dos propiedades + +00:03:46.856 --> 00:03:50.826 align:middle +porque las establecerá automáticamente +el comportamiento timestampable. + +00:03:51.646 --> 00:03:53.026 align:middle +Si probamos esto ahora... + +00:03:54.346 --> 00:03:58.216 align:middle +¡Funciona! Y si vamos a +comprobar nuestro sitio... + +00:03:59.256 --> 00:04:07.446 align:middle +es impresionante. Esta mezcla tiene 928.000 +pistas, un título aleatorio y 301 votos. + +00:04:08.116 --> 00:04:10.796 align:middle +Todo esto procede del método getDefaults(). + +00:04:11.686 --> 00:04:16.096 align:middle +Para generar datos interesantes, Foundry +aprovecha otra biblioteca llamada + +00:04:16.096 --> 00:04:18.476 align:middle +"Faker", cuyo único trabajo es... + +00:04:18.706 --> 00:04:20.376 align:middle +crear datos falsos. + +00:04:21.116 --> 00:04:26.026 align:middle +Así que si quieres un texto falso, +puedes decir self::faker()->, + +00:04:26.476 --> 00:04:28.956 align:middle +seguido de lo que quieras generar. + +00:04:29.606 --> 00:04:36.026 align:middle +Hay muchos métodos diferentes que puedes invocar en +faker() para obtener todo tipo de datos falsos divertidos. + +00:04:36.506 --> 00:04:37.596 align:middle +¡Muy útil! + +00:04:38.686 --> 00:04:40.926 align:middle +Nuestra fábrica ha hecho un buen trabajo... + +00:04:41.426 --> 00:04:44.826 align:middle +pero vamos a personalizar las cosas +para que sea un poco más realista. + +00:04:45.476 --> 00:04:50.226 align:middle +En realidad, primero, tener un +VinylMix sigue sin ser muy útil. + +00:04:50.886 --> 00:04:56.416 align:middle +Así que en su lugar, dentro de +AppFixtures, cambia esto por createMany(25). + +00:04:57.376 --> 00:04:59.906 align:middle +Aquí es donde Foundry brilla. + +00:05:00.586 --> 00:05:04.756 align:middle +Si ahora recargamos nuestras instalaciones: +Con una sola línea de código, + +00:05:04.886 --> 00:05:08.426 align:middle +¡tenemos 25 accesorios +aleatorios con los que trabajar! + +00:05:09.206 --> 00:05:12.456 align:middle +Aunque, los datos aleatorios +podrían ser un poco mejores... + +00:05:12.786 --> 00:05:14.366 align:middle +así que vamos a mejorarlo. + +00:05:15.046 --> 00:05:17.926 align:middle +Dentro de VinylMixFactory, cambia el título. + +00:05:18.496 --> 00:05:24.796 align:middle +En lugar de text() -que a veces puede +ser un muro de texto- cambia a words()... + +00:05:25.216 --> 00:05:31.056 align:middle +y usemos 5 palabras, y pasemos true +para que devuelva esto como una cadena. + +00:05:31.546 --> 00:05:34.056 align:middle +De lo contrario, el método +words() devuelve una matriz. + +00:05:34.996 --> 00:05:38.826 align:middle +Para trackCount, sí queremos +un número aleatorio, pero... + +00:05:38.976 --> 00:05:41.966 align:middle +probablemente un número entre 5 y 20. + +00:05:42.906 --> 00:05:50.056 align:middle +Para genre, vamos a buscar un randomElement() +para elegir aleatoriamente pop o rock. + +00:05:50.606 --> 00:05:53.956 align:middle +Esos son los dos géneros con los +que hemos trabajado hasta ahora. + +00:05:54.616 --> 00:05:55.696 align:middle +Y, vaya... + +00:05:56.046 --> 00:05:58.586 align:middle +asegúrate de llamar a esto como una función. + +00:05:59.176 --> 00:05:59.626 align:middle +Ya está. + +00:06:00.446 --> 00:06:05.346 align:middle +Por último, para votes, elige un +número aleatorio entre -50 y 50. + +00:06:06.146 --> 00:06:07.326 align:middle +¡Mucho mejor! + +00:06:08.076 --> 00:06:12.556 align:middle +Ah, y puedes ver que make:factory ha añadido +aquí un montón de nuestras propiedades + +00:06:12.556 --> 00:06:15.756 align:middle +por defecto, pero no las ha añadido todas. + +00:06:16.316 --> 00:06:18.526 align:middle +Una de las que falta es description. + +00:06:19.086 --> 00:06:25.226 align:middle +Añádela: 'description' => +self::faker()-> y luego utiliza paragraph(). + +00:06:26.236 --> 00:06:31.926 align:middle +Por último, para slug, no lo necesitamos en +absoluto porque se establecerá automáticamente. + +00:06:32.746 --> 00:06:35.156 align:middle +¡Ufff! ¡Vamos a probarlo! + +00:06:35.486 --> 00:06:39.286 align:middle +Recarga los accesorios: Luego +dirígete a él y actualízalo. + +00:06:40.836 --> 00:06:43.086 align:middle +Esto se ve mucho mejor. + +00:06:43.686 --> 00:06:46.096 align:middle +Tenemos una imagen rota... + +00:06:46.386 --> 00:06:50.966 align:middle +pero eso es sólo porque la API que estoy +utilizando tiene algunos "huecos"... + +00:06:51.276 --> 00:06:52.526 align:middle +nada de lo que preocuparse. + +00:06:53.316 --> 00:06:58.686 align:middle +Foundry puede hacer un montón de cosas interesantes, +así que no dudes en consultar su documentación. + +00:06:59.336 --> 00:07:05.326 align:middle +Es especialmente útil para escribir pruebas, y funciona +muy bien con las relaciones de la base de datos. + +00:07:05.766 --> 00:07:09.586 align:middle +Así que lo volveremos a ver de forma +más compleja en el próximo tutorial. + +00:07:10.446 --> 00:07:13.086 align:middle +A continuación, ¡vamos +a añadir la paginación! + +00:07:13.506 --> 00:07:19.176 align:middle +Porque, al final, no podremos listar todas las +mezclas de nuestra base de datos de una sola vez diff --git a/sfcasts/ep3-doctrine/es/install.md b/sfcasts/ep3-doctrine/es/install.md new file mode 100644 index 0000000..553119f --- /dev/null +++ b/sfcasts/ep3-doctrine/es/install.md @@ -0,0 +1,63 @@ +# Instalación de Doctrine + +¡Bienvenido de nuevo, equipo, al tercer episodio de nuestra serie Symfony 6! Los dos primeros cursos han sido súper importantes: nos han llevado desde los fundamentos hasta el núcleo de cómo funciona todo en Symfony: todo lo bueno de los "servicios" y la configuración. Ahora estás preparado para utilizar cualquier otra parte de Symfony y empezar a construir realmente un sitio. + +Y... ¿qué mejor manera de hacerlo que añadir una base de datos? Porque... hasta ahora, a pesar de todas las cosas geniales que hemos hecho, el sitio que hemos estado construyendo es 100% estático ¡Aburrido! Es hora de cambiar eso. + +## Hola Doctrine + +Sabemos que Symfony es una colección de un montón de bibliotecas para resolver un montón de problemas diferentes. Entonces... ¿tiene Symfony algunas herramientas para ayudarnos a hablar con la base de datos? La respuesta es... ¡no! Porque... ¡no tiene que hacerlo! + +¿Por qué? Entra Doctrine: la biblioteca más potente del mundo PHP para trabajar con bases de datos. Y Symfony y Doctrine funcionan muy bien juntos: son los Frodo y Sam Gamgee de la Tierra Media de PHP: los Han Solo y Chewbacca de la Alianza Rebelde de PHP. Symfony y Doctrine son como dos personajes de Disney que se acaban los bocadillos el uno al otro + +## Configuración del proyecto + +Para ver este dúo dinámico en acción, vamos a configurar nuestro proyecto. Jugar con las bases de datos es divertido, ¡así que codifica conmigo! Hazlo descargando el código del curso desde esta página. Tras descomprimirlo, encontrarás un directorio `start/` con el mismo código que ves aquí. Abre este archivo `README.MD` para obtener todas las instrucciones de configuración. + +El último paso será abrir un terminal, entrar en tu proyecto y ejecutar: + +```terminal +symfony serve -d +``` + +Esto utiliza el binario de Symfony para iniciar un servidor web local que vive en https://127.0.0.1:8000. Tomaré el camino más perezoso y haré clic en eso para ver... ¡Vinilo mezclado! Nuestra última idea de startup -y te juro que va a ser enorme- combina la nostalgia de las "cintas de mezcla" de los años 80 y 90 con la experiencia de audio de los discos de vinilo. Tú creas tus dulces cintas de mezcla, y nosotros las prensamos en un disco de vinilo para obtener una experiencia de audio totalmente hipster. + +Hasta ahora, nuestro sitio tiene una página de inicio y una página para navegar por las mezclas que han creado otras personas. Sin embargo, esa página no es realmente dinámica: se extrae de un repositorio de GitHub... y a menos que hayas configurado una clave API como hicimos en el último episodio, ¡esta página está rota! Eso es lo primero que vamos a arreglar: consultar una base de datos para las mezclas. + +## Instalar Doctrine + +¡Así que vamos a instalar Doctrine! Busca tu terminal y ejecútalo: + +```terminal +composer require "doctrine:^2.2" +``` + +Esto es, por supuesto, un alias de Flex para una biblioteca llamada `symfony/orm-pack`. Y recuerda: un "paquete" es una especie de "biblioteca falsa" que sirve como atajo para instalar varios paquetes a la vez. En este caso, estamos instalando el propio Doctrine, pero también algunas otras bibliotecas relacionadas, como el excelente sistema Doctrine Migrations. + +## Configuración de Docker + +Ah, y ¡mira esto! El comando pregunta: + +> ¿Quieres incluir la configuración Docker de las recetas? + +Así, ocasionalmente, cuando instales un paquete, la receta de ese paquete contendrá la configuración de Docker que puede, por ejemplo, iniciar un contenedor de base de datos. Esto es totalmente opcional, pero voy a decir `p` por sí mismo permanentemente. Hablaremos más sobre la configuración Docker en unos minutos. + +## Las recetas de Doctrine + +Pero ahora mismo, vamos a comprobar lo que hizo la receta. Ejecuta: + +```terminal +git status +``` + +Muy bien: esto modificó los archivos normales como `composer.json`, `composer.lock` y`symfony.lock`... y también modificó `config/bundles.php`. Si lo compruebas... no es ninguna sorpresa: nuestra aplicación tiene ahora dos nuevos bundles: DoctrineBundle y DoctrineMigrationsBundle. + +Pero probablemente la parte más importante de la receta es el cambio que se ha realizado en nuestro archivo`.env`. Recuerda: aquí es donde podemos configurar las variables de entorno... y la receta nos dio una nueva llamada `DATABASE_URL`. Ésta, como puedes ver, contiene todos los detalles de la conexión, como el nombre de usuario y la contraseña. + +¿Qué utiliza esta variable de entorno? ¡Excelente pregunta! Mira el nuevo archivo que nos dio la receta: `config/packages/doctrine.yaml`. La mayor parte de esta configuración no tendrás que pensarla ni cambiarla. Pero fíjate en esta clave `url`: ¡lee la variable de entorno `DATABASE_URL`! + +La cuestión es: la variable de entorno `DATABASE_URL` es la clave para configurar tu aplicación para que hable con una base de datos... y jugaremos con ella en unos minutos. + +La receta también ha añadido unos cuantos directorios nuevos: `migrations/` `src/Entity/` y`src/Repository/`. Ahora mismo, aparte de un archivo `.gitignore` sin sentido, están todos vacíos. Pronto empezaremos a llenarlos. + +Bien: Doctrine ya está instalado. Pero para hablar de una base de datos... tenemos que asegurarnos de que tenemos una base de datos en funcionamiento y que la variable de entorno `DATABASE_URL` apunta a ella. Hagamos eso a continuación, pero con un giro opcional y delicioso: vamos a utilizar Docker para iniciar la base de datos. diff --git a/sfcasts/ep3-doctrine/es/install.vtt b/sfcasts/ep3-doctrine/es/install.vtt new file mode 100644 index 0000000..37fccd5 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/install.vtt @@ -0,0 +1,289 @@ +WEBVTT + +00:00:02.626 --> 00:00:07.736 align:middle +¡Bienvenido de nuevo, equipo, al tercer +episodio de nuestra serie Symfony 6! + +00:00:08.376 --> 00:00:14.106 align:middle +Los dos primeros cursos fueron súper importantes: +nos llevaron desde los fundamentos hasta el núcleo + +00:00:14.106 --> 00:00:19.976 align:middle +de cómo funciona todo en Symfony: todo lo +bueno de los "servicios" y la configuración. + +00:00:20.776 --> 00:00:26.286 align:middle +Ahora estás preparado para utilizar cualquier otra parte +de Symfony y empezar realmente a construir un sitio. + +00:00:26.926 --> 00:00:31.016 align:middle +Y... ¿qué mejor manera de hacerlo +que añadir una base de datos? + +00:00:31.426 --> 00:00:32.246 align:middle +Porque... + +00:00:32.416 --> 00:00:39.966 align:middle +hasta ahora, a pesar de todas las cosas geniales que hemos +hecho, el sitio que hemos estado construyendo es 100% estático. + +00:00:40.306 --> 00:00:43.046 align:middle +¡Aburrido! Es hora de cambiar eso. + +00:00:43.806 --> 00:00:48.206 align:middle +Así que sabemos que Symfony es una +colección de un montón de bibliotecas + +00:00:48.276 --> 00:00:50.946 align:middle +para resolver un montón +de problemas diferentes. + +00:00:51.436 --> 00:00:55.546 align:middle +Entonces... ¿Symfony tiene algunas herramientas +para ayudarnos a hablar con la base de datos? + +00:00:56.016 --> 00:00:56.886 align:middle +La respuesta es... + +00:00:57.166 --> 00:00:59.156 align:middle +¡no! Porque... + +00:00:59.306 --> 00:01:00.956 align:middle +¡no tiene que hacerlo! + +00:01:01.546 --> 00:01:09.226 align:middle +¿Por qué? Entra Doctrine: la biblioteca más potente +del mundo PHP para trabajar con bases de datos. + +00:01:09.796 --> 00:01:15.316 align:middle +Y Symfony y Doctrine funcionan muy +bien juntos: son los Frodo y Sam Gamgee + +00:01:15.316 --> 00:01:21.596 align:middle +de la Tierra Media de PHP: los Han Solo +y Chewbacca de la Alianza Rebelde de PHP. + +00:01:22.136 --> 00:01:26.916 align:middle +Symfony y Doctrine son como dos personajes de +Disney que se acaban los bocadillos el uno al otro + +00:01:27.646 --> 00:01:32.506 align:middle +Para ver a este dúo dinámico en acción, +vamos a preparar nuestro proyecto. + +00:01:33.186 --> 00:01:37.246 align:middle +Jugar con las bases de datos es +divertido, ¡así que codifica conmigo! + +00:01:37.946 --> 00:01:40.426 align:middle +Hazlo descargando el código +del curso desde esta página. + +00:01:41.216 --> 00:01:46.516 align:middle +Tras descomprimirlo, encontrarás un directorio +start/ con el mismo código que ves aquí. + +00:01:47.136 --> 00:01:51.936 align:middle +Abre este archivo README.MD para obtener +todas las instrucciones de configuración. + +00:01:52.816 --> 00:01:58.186 align:middle +El último paso será abrir un terminal, +entrar en tu proyecto y ejecutar: + +00:01:58.276 --> 00:02:05.026 align:middle +symfony serve -d Esto utiliza el binario de +Symfony para iniciar un servidor web local + +00:02:05.476 --> 00:02:10.086 align:middle +que vive en https://127.0.0.1:8000. + +00:02:10.846 --> 00:02:14.826 align:middle +Tomaré el camino más perezoso +y haré clic en eso para ver... + +00:02:15.446 --> 00:02:16.846 align:middle +¡Vinilo mezclado! + +00:02:17.246 --> 00:02:24.586 align:middle +Nuestra última idea de startup -y te juro +que va a ser enorme- combina la nostalgia + +00:02:24.586 --> 00:02:30.986 align:middle +de las "cintas de mezcla" de los años 80 y 90 con +la experiencia de audio de los discos de vinilo. + +00:02:31.586 --> 00:02:36.706 align:middle +Tú creas tus dulces cintas de mezclas, y +nosotros las prensamos en un disco de vinilo + +00:02:36.876 --> 00:02:40.186 align:middle +para obtener una experiencia +de audio totalmente hipster. + +00:02:40.876 --> 00:02:47.366 align:middle +Hasta ahora, nuestro sitio tiene una página de inicio y una +página para navegar por las mezclas que han creado otras personas. + +00:02:47.946 --> 00:02:53.286 align:middle +Aunque, esa página no es realmente +dinámica: tira de un repositorio de GitHub... + +00:02:53.876 --> 00:03:00.716 align:middle +y a menos que hayas configurado una clave de API como +hicimos en el último episodio, ¡esta página está rota! + +00:03:01.316 --> 00:03:06.846 align:middle +Eso es lo primero que vamos a arreglar: +consultando una base de datos para las mezclas. + +00:03:07.676 --> 00:03:09.316 align:middle +¡Así que vamos a instalar Doctrine! + +00:03:09.766 --> 00:03:15.056 align:middle +Busca tu terminal y ejecuta: composer +require doctrine Esto es, por supuesto, + +00:03:15.246 --> 00:03:20.516 align:middle +un alias de Flex para una +biblioteca llamada symfony/orm-pack. + +00:03:21.046 --> 00:03:27.246 align:middle +Y recuerda: un "paquete" es una especie +de "biblioteca falsa" que sirve como atajo + +00:03:27.296 --> 00:03:30.056 align:middle +para instalar varios paquetes a la vez. + +00:03:30.746 --> 00:03:36.236 align:middle +En este caso, estamos instalando la propia Doctrine, +pero también algunas otras bibliotecas relacionadas, + +00:03:36.646 --> 00:03:39.616 align:middle +como el excelente sistema Doctrine Migrations. + +00:03:40.416 --> 00:03:41.466 align:middle +Ah, y ¡mira esto! + +00:03:41.776 --> 00:03:47.696 align:middle +El comando pregunta: ¿Quieres incluir +la configuración Docker de las recetas? + +00:03:48.516 --> 00:03:51.586 align:middle +Así, ocasionalmente, +cuando instales un paquete, + +00:03:51.886 --> 00:03:56.716 align:middle +la receta de ese paquete contendrá +la configuración de Docker que puede, + +00:03:56.946 --> 00:03:59.756 align:middle +por ejemplo, iniciar un +contenedor de base de datos. + +00:04:00.506 --> 00:04:05.156 align:middle +Esto es totalmente opcional, pero voy +a decir p por sí permanentemente. + +00:04:06.046 --> 00:04:09.716 align:middle +Hablaremos más sobre la +configuración Docker en unos minutos. + +00:04:10.176 --> 00:04:13.306 align:middle +Pero ahora mismo, vamos a +comprobar lo que hizo la receta. + +00:04:13.876 --> 00:04:20.786 align:middle +Ejecuta: git status Bien, genial: esto modificó +los archivos normales como composer.json, + +00:04:20.976 --> 00:04:23.116 align:middle +composer.lock y symfony.lock... + +00:04:23.626 --> 00:04:27.036 align:middle +y también modificó config/bundles.php. + +00:04:27.846 --> 00:04:29.206 align:middle +Si lo compruebas... + +00:04:30.046 --> 00:04:37.096 align:middle +no hay sorpresa: nuestra aplicación tiene ahora dos +nuevos bundles: DoctrineBundle y DoctrineMigrationsBundle. + +00:04:37.846 --> 00:04:44.376 align:middle +Pero probablemente la parte más importante de la receta +es el cambio que ha realizado en nuestro archivo .env. + +00:04:45.316 --> 00:04:49.796 align:middle +Recuerda: aquí es donde podemos +configurar las variables de entorno... + +00:04:50.276 --> 00:04:54.856 align:middle +y la receta nos dio una +nueva llamada DATABASE_URL. + +00:04:55.646 --> 00:05:01.426 align:middle +Ésta, como puedes ver, contiene todos los detalles de +la conexión, como el nombre de usuario y la contraseña. + +00:05:02.176 --> 00:05:05.046 align:middle +¿Qué utiliza esta variable de entorno? + +00:05:05.576 --> 00:05:06.616 align:middle +¡Excelente pregunta! + +00:05:07.236 --> 00:05:12.236 align:middle +Echa un vistazo a un nuevo archivo que nos +dio la receta: config/packages/doctrine.yaml. + +00:05:13.036 --> 00:05:17.026 align:middle +La mayor parte de esta configuración +no tendrás que pensarla ni cambiarla. + +00:05:17.476 --> 00:05:23.756 align:middle +Pero fíjate en esta clave url: ¡lee la +variable de entorno DATABASE_URL! La cuestión + +00:05:24.446 --> 00:05:31.226 align:middle +es: la variable de entorno +DATABASE_URL es la clave para + +00:05:31.226 --> 00:05:33.236 align:middle +configurar tu aplicación para +que hable con una base de datos... + +00:05:33.586 --> 00:05:35.696 align:middle +y jugaremos con ella en unos minutos. + +00:05:36.576 --> 00:05:43.296 align:middle +La receta también ha añadido unos cuantos directorios +nuevos: migrations/ src/Entity/ y src/Repository/. + +00:05:44.046 --> 00:05:49.456 align:middle +Ahora mismo, aparte de un archivo .gitignore +sin sentido, están todos vacíos. + +00:05:50.106 --> 00:05:52.546 align:middle +Pronto empezaremos a llenarlos. + +00:05:53.376 --> 00:05:55.686 align:middle +Bien: Doctrine ya está instalado. + +00:05:56.286 --> 00:05:57.826 align:middle +Pero para hablar con una base de datos... + +00:05:58.116 --> 00:06:01.086 align:middle +tenemos que asegurarnos de que tenemos +una base de datos en funcionamiento + +00:06:01.446 --> 00:06:05.826 align:middle +y que la variable de entorno +DATABASE_URL apunta a ella. + +00:06:06.546 --> 00:06:10.706 align:middle +Hagamos eso a continuación, pero +con un giro opcional y delicioso: + +00:06:11.106 --> 00:06:14.016 align:middle +vamos a utilizar Docker para +iniciar la base de datos diff --git a/sfcasts/ep3-doctrine/es/migrations.md b/sfcasts/ep3-doctrine/es/migrations.md new file mode 100644 index 0000000..57a87b2 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/migrations.md @@ -0,0 +1,66 @@ +# Migraciones + +¡Hemos creado una clase de entidad! Pero... eso es todo. La tabla correspondiente aún no existe en nuestra base de datos. + +Pensemos. En teoría, Doctrine conoce nuestra entidad, todas sus propiedades y sus atributos `ORM\Column`. Entonces... ¿no debería Doctrine ser capaz de crear esa tabla por nosotros automáticamente? Sí Puede hacerlo. + +## El comando make:migration + +Cuando instalamos Doctrine anteriormente, venía con una biblioteca de migraciones que es increíble. ¡Échale un vistazo! Cada vez que hagas un cambio en la estructura de tu base de datos, como añadir una nueva clase de entidad, o incluso añadir una nueva propiedad a una entidad existente, debes ir a tu terminal y ejecutar + +```terminal +symfony console make:migration +``` + +En este caso, estoy ejecutando `symfony console` porque esto va a hablar con nuestra base de datos. Ejecuta eso y... ¡perfecto! Se ha creado un nuevo archivo en el directorio `migrations/`con una marca de tiempo para la fecha de hoy. Vamos a comprobarlo! Busca `migrations/` y abre el nuevo archivo. + +[[[ code('2a34551803') ]]] + +Esto contiene una clase con los métodos `up()` y `down()`... aunque nunca ejecuto las migraciones en sentido "descendente", así que nos centraremos sólo en `up()`. Y... ¡esto es genial! El comando de migraciones vio nuestra entidad `VinylMix`, se dio cuenta de que faltaba su tabla en la base de datos y generó el SQL necesario en Postgres para crearla, incluyendo todas las columnas. Ha sido muy fácil. + +## Ejecutar la migración + +Bien... ¿cómo ejecutamos esta migración? De vuelta a tu terminal, ejecuta: + +```terminal +symfony console doctrine:migrations:migrate +``` + +Di `y` para confirmar y... ¡precioso! Nos dice que es `Migrating up to`esa versión específica. Parece... ¡que ha funcionado! Para asegurarte, puedes probar con otro comando `bin/console`: `symfony console doctrine:query:sql` +con `SELECT * FROM vinyl_mix`. + +```terminal-silent +symfony console doctrine:query:sql 'SELECT * FROM vinyl_mix' +``` + +Cuando probamos eso... ¡Ups! Perdona mi error tipográfico... aquí no hay nada que ver. Inténtalo de nuevo y... ¡perfecto! ¡No nos da ningún error! Sólo dice que`The query yielded an empty result set`. Si esa tabla no existiera, como`vinyl_foo`, Doctrine nos habría gritado. + +Así pues, ¡la migración se ha ejecutado! + +## Cómo funcionan las migraciones + +Este hermoso sistema merece algunas explicaciones. Ejecuta + +```terminal +symfony console doctrine:migrations:migrate +``` + +de nuevo. ¡Compruébalo! ¡Es lo suficientemente inteligente como para evitar ejecutar esa migración por segunda vez! Sabe que ya lo hizo. Pero... ¿cómo? Prueba a ejecutar otro comando: + +```terminal +symfony console doctrine:migrations:status +``` + +Esto da alguna información general sobre el sistema de migración. La parte más importante está en `Storage` donde dice `Table Name` y `doctrine_migration_versions`. + +El asunto es el siguiente: la primera vez que ejecutamos la migración, Doctrine creó esta tabla especial, que almacena literalmente una lista de todas las clases de migración que se han ejecutado. Entonces, cada vez que ejecutamos `doctrine:migrations:migrate`, busca en nuestro directorio `migrations/`, encuentra todas las clases, comprueba en la base de datos cuáles no se han ejecutado ya, y sólo llama a esas. Una vez que las nuevas migraciones terminan, las añade como filas a la tabla `doctrine_migration_versions`. + +Puedes visualizar esta tabla ejecutando: + +```terminal +symfony console doctrine:migrations:list +``` + +Ve nuestra única migración y sabe que ya la ha ejecutado. ¡Incluso tiene la fecha! + +Esto es genial... pero vamos a ir más allá. A continuación, añadamos una nueva propiedad a nuestra entidad y generemos una segunda migración para añadir la columna. diff --git a/sfcasts/ep3-doctrine/es/migrations.vtt b/sfcasts/ep3-doctrine/es/migrations.vtt new file mode 100644 index 0000000..7ef933b --- /dev/null +++ b/sfcasts/ep3-doctrine/es/migrations.vtt @@ -0,0 +1,227 @@ +WEBVTT + +00:00:01.016 --> 00:00:03.196 align:middle +¡Hemos creado una clase de entidad! + +00:00:03.486 --> 00:00:05.296 align:middle +Pero... eso es todo. + +00:00:05.676 --> 00:00:09.456 align:middle +La tabla correspondiente aún no +existe en nuestra base de datos. + +00:00:10.016 --> 00:00:10.536 align:middle +Pensemos. + +00:00:11.156 --> 00:00:14.176 align:middle +En teoría, Doctrine conoce nuestra entidad, + +00:00:14.466 --> 00:00:18.796 align:middle +todas sus propiedades y +sus atributos ORM\Column. + +00:00:18.796 --> 00:00:24.396 align:middle +Entonces... ¿no debería Doctrine ser capaz de +crear esa tabla por nosotros automáticamente? + +00:00:24.976 --> 00:00:26.856 align:middle +Sí, puede hacerlo. + +00:00:27.656 --> 00:00:33.186 align:middle +Cuando instalamos Doctrine antes, venía con +una biblioteca de migraciones que es increíble. + +00:00:33.786 --> 00:00:34.266 align:middle +¡Compruébalo! + +00:00:34.876 --> 00:00:39.706 align:middle +Cada vez que hagas un cambio en la estructura de tu +base de datos, como añadir una nueva clase de entidad, + +00:00:40.086 --> 00:00:46.156 align:middle +o incluso añadir una nueva propiedad a una +entidad existente, debes ir a tu terminal + +00:00:46.316 --> 00:00:51.726 align:middle +y ejecutar symfony console make:migration En +este caso, estoy ejecutando symfony console + +00:00:51.846 --> 00:00:55.566 align:middle +porque esto va a hablar +con nuestra base de datos. + +00:00:55.566 --> 00:00:56.666 align:middle +Ejecuta eso y... + +00:00:57.116 --> 00:01:01.676 align:middle +¡perfecto! Se ha creado un nuevo +archivo en el directorio migrations/ + +00:01:02.106 --> 00:01:04.236 align:middle +con una marca de tiempo para la fecha de hoy. + +00:01:04.966 --> 00:01:05.706 align:middle +¡Vamos a comprobarlo! + +00:01:05.706 --> 00:01:08.786 align:middle +Busca migrations/ y abre el nuevo archivo. + +00:01:09.546 --> 00:01:12.396 align:middle +Éste contiene una clase con +los métodos up() y down()... + +00:01:12.676 --> 00:01:18.376 align:middle +aunque nunca ejecuto las migraciones en sentido +"descendente", así que nos centraremos sólo en up(). + +00:01:19.186 --> 00:01:21.316 align:middle +Y... ¡esto es genial! + +00:01:21.676 --> 00:01:27.756 align:middle +El comando de migraciones vio nuestra entidad +VinylMix, se dio cuenta de que faltaba su tabla + +00:01:27.756 --> 00:01:31.936 align:middle +en la base de datos y generó +el SQL necesario en Postgres + +00:01:32.186 --> 00:01:35.396 align:middle +para crearla, incluyendo todas las columnas. + +00:01:35.806 --> 00:01:37.436 align:middle +Ha sido muy fácil. + +00:01:38.306 --> 00:01:41.196 align:middle +Vale... entonces, ¿cómo +ejecutamos esta migración? + +00:01:41.976 --> 00:01:48.606 align:middle +De vuelta a tu terminal, ejecuta: symfony +console doctrine:migrations:migrate Di y + +00:01:48.606 --> 00:01:50.146 align:middle +para confirmar y... + +00:01:50.476 --> 00:01:51.256 align:middle +¡guapísimo! + +00:01:51.656 --> 00:01:55.556 align:middle +Nos dice que es Migrating +up to esa versión concreta. + +00:01:56.106 --> 00:01:57.076 align:middle +Parece... + +00:01:57.076 --> 00:01:58.056 align:middle +¡que ha funcionado! + +00:01:58.786 --> 00:02:01.776 align:middle +Para asegurarte, puedes probar +con otro comando bin/console: + +00:02:02.016 --> 00:02:07.976 align:middle +symfony console doctrine:query:sql +con SELECT * FROM vinyl_mix. + +00:02:08.776 --> 00:02:09.876 align:middle +Cuando probamos eso... + +00:02:10.546 --> 00:02:12.236 align:middle +¡Ups! Perdona mi error tipográfico... + +00:02:12.516 --> 00:02:13.706 align:middle +no hay nada que ver aquí. + +00:02:14.446 --> 00:02:15.736 align:middle +Inténtalo de nuevo y... + +00:02:16.246 --> 00:02:18.586 align:middle +¡perfecto! ¡No hay ningún error! + +00:02:19.006 --> 00:02:23.006 align:middle +Sólo dice que The query +yielded an empty result set. + +00:02:23.776 --> 00:02:30.196 align:middle +Si esa tabla no existiera, como +vinyl_foo, Doctrine nos habría gritado. + +00:02:30.196 --> 00:02:32.926 align:middle +Así pues, ¡la migración sí se ejecutó! + +00:02:33.806 --> 00:02:37.186 align:middle +Este hermoso sistema +merece alguna explicación. + +00:02:37.846 --> 00:02:41.256 align:middle +Ejecuta de nuevo symfony console +doctrine:migrations:migrate. + +00:02:42.556 --> 00:02:43.176 align:middle +¡Compruébalo! + +00:02:43.516 --> 00:02:48.426 align:middle +¡Es lo suficientemente inteligente como para +evitar ejecutar esa migración por segunda vez! + +00:02:49.046 --> 00:02:51.506 align:middle +Sabe que ya lo hizo. + +00:02:52.146 --> 00:02:53.306 align:middle +Pero... ¿cómo? + +00:02:54.046 --> 00:03:00.816 align:middle +Prueba a ejecutar otro comando: symfony console +doctrine:migrations:status Esto da alguna + +00:03:00.816 --> 00:03:02.896 align:middle +información general sobre +el sistema de migración. + +00:03:03.546 --> 00:03:12.066 align:middle +La parte más importante está en Storage donde +dice Table Name y doctrine_migration_versions. El + +00:03:12.876 --> 00:03:19.696 align:middle +asunto es el siguiente: la primera vez que ejecutamos +la migración, Doctrine creó esta tabla especial, + +00:03:20.106 --> 00:03:26.766 align:middle +que almacena literalmente una lista de todas +las clases de migración que se han ejecutado. + +00:03:27.516 --> 00:03:30.376 align:middle +Entonces, cada vez que ejecutamos +doctrine:migrations:migrate, + +00:03:30.806 --> 00:03:37.316 align:middle +busca en nuestro directorio migrations/, encuentra +todas las clases, comprueba en la base de datos + +00:03:37.316 --> 00:03:42.786 align:middle +cuáles no se han ejecutado +ya, y sólo llama a esas. + +00:03:43.506 --> 00:03:49.556 align:middle +Una vez terminadas las nuevas migraciones, las añade +como filas a la tabla doctrine_migration_versions. + +00:03:50.286 --> 00:03:52.326 align:middle +Puedes visualizar esta tabla ejecutando: + +00:03:52.536 --> 00:03:59.836 align:middle +symfony console doctrine:migrations:list Ve nuestra +única migración y sabe que ya la ha ejecutado. + +00:04:00.386 --> 00:04:01.816 align:middle +¡Incluso tiene la fecha! + +00:04:02.606 --> 00:04:03.326 align:middle +Esto es genial... + +00:04:03.716 --> 00:04:05.496 align:middle +pero vamos a ir más allá. + +00:04:06.376 --> 00:04:12.976 align:middle +A continuación, añadamos una nueva propiedad a nuestra entidad +y generemos una segunda migración para añadir la columna diff --git a/sfcasts/ep3-doctrine/es/pagination.md b/sfcasts/ep3-doctrine/es/pagination.md new file mode 100644 index 0000000..2bb9f12 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/pagination.md @@ -0,0 +1,101 @@ +# Paginación + +Al final, esta página se va a hacer superlarga. Cuando tengamos mil mezclas, ¡probablemente ni siquiera se cargue! Podemos solucionarlo añadiendo la paginación. ¿Doctrine tiene la capacidad de paginar los resultados? Sí, la tiene Aunque yo suelo instalar otra biblioteca que añade más funciones además de las de Doctrine. + +Busca tu terminal y ejecuta: + +```termninal +composer require babdev/pagerfanta-bundle pagerfanta/doctrine-orm-adapter +``` + +Esto instala un bundle de Pagerfanta, que es una envoltura de una biblioteca realmente buena llamada Pagerfanta. Pagerfanta puede paginar muchas cosas, como los resultados de Doctrine, los resultados de Elasticsearch y mucho más. También instalamos su adaptador ORM de Doctrine, que nos dará todo lo que necesitamos para paginar nuestros resultados de Doctrine. En este caso, cuando ejecutamos + +```terminal +git status +``` + +añadió un bundle, pero la receta no necesitó hacer nada más. ¡Genial! Entonces, ¿cómo funciona esta biblioteca? + +Abre `src/Controller/VinylController` y busca la acción `browse()`. En lugar de consultar todas las mezclas, como estamos haciendo ahora, vamos a decirle a la biblioteca Pagerfanta en qué página se encuentra el usuario, cuántos resultados debe mostrar por página, y entonces nos consultará los resultados correctos. + +## Devolver un QueryBuilder + +Para que esto funcione, en lugar de llamar a `findAllOrderedByVotes()` y obtener todos los resultados, tenemos que llamar a un método de nuestro repositorio que devuelva un QueryBuilder. Abre `src/Repository/VinylMixRepository` y desplázate hasta`findAllOrderedByVotes()`. Por el momento sólo estamos utilizando este método, así que cámbiale el nombre a `createOrderedByVotesQueryBuilder()`... y esto devolverá ahora un `QueryBuilder` - el de Doctrine ORM. Eliminaré la documentación de PHP en la parte superior... y lo único que tenemos que hacer aquí abajo es eliminar`getQuery()` y `getResult()` para que sólo devolvamos `$queryBuilder`. + +[[[ code('6c1a403e0d') ]]] + +En `VinylController`, cambia esto por`$queryBuilder = $mixRepository->createOrderedByVotesQueryBuilder($slug)` + +[[[ code('46e927c5d9') ]]] + +Inicializar Pagerfanta son dos líneas. Primero, crea el adaptador -`$adapter = new QueryAdapter()` y pásale `$queryBuilder`. Luego crea el objeto `Pagerfanta` con`$pagerfanta = Pagerfanta::createForCurrentPageWithMaxPerPage()` + +Esto es un bocado. Pásale el `$adapter`, la página actual - en este momento, voy a codificar en duro `1` - y finalmente el máximo de resultados por página que queremos. Vamos a utilizar `9` ya que nuestras mezclas aparecen en tres columnas. + +[[[ code('269af6c02f') ]]] + +Ahora que tenemos este objeto Pagerfanta, vamos a pasarlo a la plantilla en lugar de `mixes`. Sustitúyelo por una nueva variable llamada `pager` ajustada a `$pagerfanta`. + +[[[ code('e6c7cdde64') ]]] + +Lo bueno de este objeto `$pagerfanta` es que puedes hacer un bucle sobre él. Y en cuanto lo hagas, ejecutará la consulta correcta para obtener sólo los resultados de esta página. En `templates/vinyl/browse.html.twig`, en lugar de `{% for mix in mixes %}`, di`{% for mix in pager %}`. + +[[[ code('27de4f1c48') ]]] + +Eso es todo. Cada resultado del bucle seguirá siendo un objeto `VinylMix`. + +Si vamos y recargamos... ¡lo tenemos! Muestra nueve resultados: ¡los resultados de la página 1! + +## Enlace a la página siguiente + +Lo que necesitamos ahora son enlaces a las páginas siguientes y anteriores... y esta biblioteca también puede ayudarnos con eso. De vuelta a tu terminal, ejecuta: + +```terminal +composer require pagerfanta/twig +``` + +Una de las cosas más complicadas de la biblioteca Pagerfanta es que, en lugar de ser una biblioteca gigante que tiene todo lo que necesitas, está dividida en un montón de bibliotecas más pequeñas. Así que si quieres el soporte del adaptador ORM, tienes que instalarlo como hemos hecho antes. Si quieres el soporte de Twig para añadir enlaces, también tienes que instalarlo. Sin embargo, una vez que lo hagas, es bastante sencillo. + +De vuelta a nuestra plantilla, busca el objeto `{% endfor %}`, y justo después, di`{{ pagerfanta() }}`, pasándole el objeto `pager`. + +[[[ code('436293d547') ]]] + +¡Compruébalo! Cuando actualizamos... ¡tenemos enlaces en la parte inferior! Son... feos, pero lo arreglaremos en un momento. + +## Leer la página actual + +Si haces clic en el enlace "Siguiente", arriba en nuestra URL, vemos `?page=2`. Aunque... los resultados no cambian realmente. Seguimos viendo los mismos resultados de la página 1. Y... eso tiene sentido. Recuerda que en `VinylController`, codifiqué la página actual en `1`. Así que, aunque tengamos `?page=2` aquí arriba, Pagerfanta sigue pensando que estamos en la Página 1. + +Lo que tenemos que hacer es leer este parámetro de consulta y pasarlo como este segundo argumento ¡No hay problema! ¿Cómo leemos los parámetros de consulta? Bueno, es información de la petición, así que necesitamos el objeto `Request`. + +Justo antes de nuestro argumento opcional, añade un nuevo argumento `$request` de tipo`Request`: el de HttpFoundation. Ahora, aquí abajo, en lugar de `1`, di `$request->query` (así es como se obtienen los parámetros de consulta), con`->get('page')`... y por defecto esto es `1` si no hay `?page=` en la URL. + +[[[ code('e22d104f10') ]]] + +Por cierto, si quieres, también puedes añadir `{page}` aquí arriba. De este modo, Pagerfanta pondrá automáticamente el número de página dentro de la URL en lugar de establecerlo como parámetro de consulta. + +Si nos dirigimos y refrescamos... ahora mismo, tenemos `?page=2`. Aquí abajo... ¡sabe que estamos en la página 2! Si vamos a la siguiente página... ¡sí! ¡Vemos un conjunto diferente de resultados! + +## Estilizando los enlaces de paginación + +Aunque, esto sigue siendo súper feo. Afortunadamente, el bundle nos da una forma de controlar el marcado que se utiliza para los enlaces de paginación. E incluso viene con soporte automático para el marcado compatible con CSS de Bootstrap. Sólo tenemos que decirle al bundle que lo utilice. + +Así que... tenemos que configurar el bundle. Pero... el bundle no nos ha dado ningún archivo de configuración nuevo cuando se ha instalado. No pasa nada No todos los bundles nuevos nos dan archivos de configuración. Pero en cuanto necesites uno, ¡crea uno! Como este bundle se llama`BabdevPagerfantaBundle`, voy a crear un nuevo archivo llamado`babdev_pagerfanta.yaml`. Como aprendimos en el último tutorial, el nombre de estos archivos no es importante. Lo importante es la clave raíz, que debe ser`babdev_pagerfanta`. Para cambiar la forma en que se muestra la paginación, añade `default_view: twig`y, a continuación, `default_twig_template`, que debe ser`@BabDevPagerfanta/twitter_bootstrap5.html.twig`. + +[[[ code('80afb85b20') ]]] + +Como cualquier otra configuración, no hay forma de que sepas que ésta es la configuración correcta simplemente adivinando. Tienes que consultar la documentación. + +Si volvemos y refrescamos... eh, no ha cambiado nada. Este es un pequeño error que a veces se encuentra en Symfony cuando se crea un nuevo archivo de configuración. Symfony no se dio cuenta... y por eso no sabía que tenía que reconstruir su caché. Esta es una situación súper rara, pero si alguna vez crees que puede estar ocurriendo, es bastante fácil borrar manualmente la caché ejecutando: + +```terminal +php bin/console cache:clear +``` + +Y... oh... explota. Seguramente te habrás dado cuenta de por qué. ¡Me encanta este error! + +> No hay ninguna extensión capaz de cargar la configuración para "baberdev_pagerfanta" + +Se supone que es `babdev_pagerfanta`. ¡Ups! Y ahora... ¡perfecto! Está contento. Y cuando refrescamos... ¡lo ve! En un proyecto real, probablemente querremos añadir algo de CSS adicional para hacer este "modo oscuro"... pero ya lo tenemos. + +Bien, equipo, ¡ya hemos terminado! Como extra, vamos a refactorizar esta paginación para convertirla en un scroll eterno impulsado por JavaScript... ¡excepto el giro argumental! Vamos a hacerlo sin escribir una sola línea de JavaScript. Eso a continuación. diff --git a/sfcasts/ep3-doctrine/es/pagination.vtt b/sfcasts/ep3-doctrine/es/pagination.vtt new file mode 100644 index 0000000..c69d65c --- /dev/null +++ b/sfcasts/ep3-doctrine/es/pagination.vtt @@ -0,0 +1,473 @@ +WEBVTT + +00:00:01.046 --> 00:00:04.016 align:middle +Al final, esta página +se va a hacer superlarga. + +00:00:04.536 --> 00:00:09.286 align:middle +Cuando tengamos mil mezclas, +¡probablemente ni se cargue! + +00:00:09.886 --> 00:00:12.016 align:middle +Podemos solucionarlo añadiendo paginación. + +00:00:12.816 --> 00:00:16.466 align:middle +¿Doctrine permite paginar los resultados? + +00:00:17.026 --> 00:00:20.856 align:middle +Sí, la tiene Aunque yo suelo +instalar otra biblioteca + +00:00:20.946 --> 00:00:23.846 align:middle +que añade más funciones +además de las de Doctrine. + +00:00:24.646 --> 00:00:34.456 align:middle +Busca tu terminal y ejecuta: Esto instala un +bundle de Pagerfanta, que es una envoltura + +00:00:34.536 --> 00:00:39.716 align:middle +de una biblioteca realmente buena +llamada Pagerfanta. Pagerfanta + +00:00:40.416 --> 00:00:44.906 align:middle +puede paginar muchas cosas, +como resultados de Doctrine, + +00:00:45.116 --> 00:00:47.546 align:middle +resultados de Elasticsearch y mucho más. + +00:00:48.376 --> 00:00:54.216 align:middle +También hemos instalado su adaptador ORM Doctrine, +que nos proporcionará todo lo que necesitamos + +00:00:54.306 --> 00:00:56.486 align:middle +para paginar nuestros resultados Doctrine. + +00:00:57.276 --> 00:01:01.726 align:middle +En este caso, cuando ejecutamos +git status añadió un bundle, + +00:01:02.086 --> 00:01:05.016 align:middle +pero la receta no necesitó hacer nada más. + +00:01:05.876 --> 00:01:09.156 align:middle +¡Genial! Entonces, ¿cómo +funciona esta biblioteca? + +00:01:09.746 --> 00:01:13.586 align:middle +Abre src/Controller/VinylController +y busca la acción browse(). + +00:01:14.286 --> 00:01:18.226 align:middle +En lugar de consultar todas las mezclas, +como estamos haciendo ahora, vamos a + +00:01:18.686 --> 00:01:23.726 align:middle +decirle a la biblioteca Pagerfanta en qué +página se encuentra actualmente el usuario, + +00:01:24.106 --> 00:01:31.286 align:middle +cuántos resultados mostrar por página, y +entonces nos consultará los resultados correctos. + +00:01:32.076 --> 00:01:37.746 align:middle +Para que esto funcione, en lugar de llamar a +findAllOrderedByVotes() y obtener de vuelta todos los resultados + +00:01:37.746 --> 00:01:43.766 align:middle +, tenemos que llamar a un método de nuestro +repositorio que devuelva un QueryBuilder. + +00:01:44.686 --> 00:01:50.346 align:middle +Abre src/Repository/VinylMixRepository y desplázate +hasta findAllOrderedByVotes(). De momento + +00:01:51.006 --> 00:01:54.036 align:middle +sólo vamos a utilizar este método aquí, + +00:01:54.406 --> 00:01:58.056 align:middle +así que cámbiale el nombre a +createOrderedByVotesQueryBuilder()... + +00:01:58.756 --> 00:02:03.536 align:middle +y esto devolverá ahora un +QueryBuilder - el de Doctrine ORM. + +00:02:04.466 --> 00:02:07.236 align:middle +Eliminaré la documentación +PHP de la parte superior... + +00:02:07.776 --> 00:02:12.916 align:middle +y lo único que tenemos que hacer aquí +abajo es eliminar getQuery() y getResult() + +00:02:13.536 --> 00:02:16.296 align:middle +para que sólo devolvamos $queryBuilder. En + +00:02:17.146 --> 00:02:19.376 align:middle +VinylController, cambia esto + +00:02:19.376 --> 00:02:25.156 align:middle +por $queryBuilder = +$mixRepository->createOrderedByVotesQueryBuilder($slug) Inicializar + +00:02:26.186 --> 00:02:29.116 align:middle +Pagerfanta son dos líneas. Primero, + +00:02:29.576 --> 00:02:35.786 align:middle +crea el adaptador - $adapter = new QueryAdapter() +y pásale $queryBuilder. Luego crea + +00:02:36.606 --> 00:02:38.506 align:middle +el objeto Pagerfanta con + +00:02:38.506 --> 00:02:44.956 align:middle +$pagerfanta = +Pagerfanta::createForCurrentPageWithMaxPerPage() Eso es + +00:02:44.956 --> 00:02:47.486 align:middle +un trabalenguas. Pásale + +00:02:48.286 --> 00:02:53.776 align:middle +el $adapter, la página actual -ahora +mismo, voy a hardcodear 1 - y, por último, + +00:02:54.196 --> 00:02:58.056 align:middle +el máximo de resultados por página +que queremos. Vamos a utilizar + +00:02:58.646 --> 00:03:03.166 align:middle +9 ya que nuestras mezclas aparecen +en tres columnas. Ahora que + +00:03:04.046 --> 00:03:07.716 align:middle +tenemos este objeto +Pagerfanta, vamos a pasarlo a + +00:03:07.716 --> 00:03:10.886 align:middle +la plantilla en lugar de mixes. Sustitúyelo + +00:03:11.716 --> 00:03:15.406 align:middle +por una nueva variable llamada pager +ajustada a $pagerfanta. Lo bueno + +00:03:16.436 --> 00:03:21.906 align:middle +de este objeto $pagerfanta es que puedes +hacer un bucle sobre él. Y en cuanto lo + +00:03:22.486 --> 00:03:29.166 align:middle +hagas, ejecutará la consulta correcta para obtener sólo los +resultados de esta página. En templates/vinyl/browse.html.twig , en + +00:03:29.956 --> 00:03:34.316 align:middle +lugar de {% for mix in mixes + +00:03:34.316 --> 00:03:38.866 align:middle +%}, di {% for mix in pager %}. Y ya está. + +00:03:38.866 --> 00:03:39.846 align:middle +Cada resultado + +00:03:40.436 --> 00:03:44.476 align:middle +del bucle seguirá siendo un +objeto VinylMix. Si volvemos + +00:03:45.286 --> 00:03:47.316 align:middle +y recargamos... ¡ya está! + +00:03:48.776 --> 00:03:53.626 align:middle +Muestra nueve resultados: ¡los +resultados de la Página 1! Lo que + +00:03:54.436 --> 00:03:58.386 align:middle +necesitamos ahora son enlaces a las páginas +siguiente y anterior... y esta biblioteca + +00:03:58.816 --> 00:04:00.946 align:middle +también puede ayudarnos con eso. De vuelta a + +00:04:01.686 --> 00:04:07.846 align:middle +tu terminal, ejecuta: composer require pagerfanta/twig +Una de las cosas más complicadas de la biblioteca + +00:04:07.846 --> 00:04:13.676 align:middle +Pagerfanta es que, en lugar de ser +una biblioteca gigante que tiene + +00:04:13.676 --> 00:04:18.346 align:middle +todo lo que necesitas, está dividida en un +montón de bibliotecas más pequeñas. Así que si + +00:04:19.116 --> 00:04:24.496 align:middle +quieres el soporte del adaptador ORM, tienes que +instalarlo como hicimos antes. Si quieres la compatibilidad + +00:04:25.246 --> 00:04:29.646 align:middle +con Twig para añadir enlaces, también +tienes que instalarla. Una vez que lo hayas + +00:04:30.476 --> 00:04:32.986 align:middle +hecho, es bastante sencillo. De vuelta a + +00:04:33.776 --> 00:04:37.696 align:middle +nuestra plantilla, busca el objeto +{% endfor %}, y justo después, di {{ + +00:04:38.046 --> 00:04:42.716 align:middle +pagerfanta() }}, pasándole el +objeto pager. ¡ Compruébalo + +00:04:43.616 --> 00:04:44.016 align:middle +! Cuando + +00:04:44.316 --> 00:04:45.726 align:middle +actualizamos... ¡tenemos + +00:04:46.546 --> 00:04:48.616 align:middle +enlaces en la parte inferior! Son... feos, + +00:04:49.046 --> 00:04:49.646 align:middle +pero + +00:04:49.876 --> 00:04:52.146 align:middle +lo arreglaremos en un minuto. Si + +00:04:52.966 --> 00:04:58.776 align:middle +haces clic en el enlace "Siguiente", arriba en +nuestra URL, vemos ?page=2. Aunque... en realidad los + +00:04:59.476 --> 00:05:03.246 align:middle +resultados no cambian. Seguimos + +00:05:03.806 --> 00:05:06.486 align:middle +viendo los mismos resultados +de la Página 1. Y... eso + +00:05:06.966 --> 00:05:08.786 align:middle +tiene sentido. Recuerda que + +00:05:09.346 --> 00:05:14.546 align:middle +en VinylController, codifiqué +la página actual en 1. Así que + +00:05:15.106 --> 00:05:22.156 align:middle +aunque tengamos ?page=2 aquí arriba, Pagerfanta sigue +pensando que estamos en la Página 1. Lo que tenemos que + +00:05:23.016 --> 00:05:28.456 align:middle +hacer es leer este parámetro de consulta y +pasarlo como segundo argumento. ¡No hay problema! + +00:05:29.076 --> 00:05:29.786 align:middle +¿Cómo leemos los parámetros de consulta + +00:05:30.516 --> 00:05:32.366 align:middle +? Bueno, es + +00:05:33.046 --> 00:05:39.166 align:middle +información de la petición, así que +necesitamos el objeto Request. Justo antes de + +00:05:39.916 --> 00:05:44.826 align:middle +nuestro argumento opcional, añade un +nuevo argumento $request de tipo Request: + +00:05:44.826 --> 00:05:48.256 align:middle +el de HttpFoundation. Ahora, + +00:05:49.116 --> 00:05:51.456 align:middle +aquí abajo, en lugar de +1, pon $request->query + +00:05:51.576 --> 00:05:58.286 align:middle +(así es como se obtienen los parámetros de +consulta), con ->get('page')... y por defecto + +00:05:58.846 --> 00:06:04.456 align:middle +esto a 1 si no hay ?page= en la URL. Por + +00:06:05.316 --> 00:06:10.086 align:middle +cierto, si quieres, también puedes +añadir {page} aquí arriba. De esta forma, + +00:06:10.646 --> 00:06:16.586 align:middle +Pagerfanta pondrá automáticamente el número de página +dentro de la URL en lugar de establecerlo como parámetro de + +00:06:16.586 --> 00:06:18.386 align:middle +consulta. Si nos dirigimos + +00:06:19.336 --> 00:06:20.696 align:middle +y actualizamos... ahora mismo, + +00:06:21.746 --> 00:06:24.486 align:middle +tenemos ?page=2. Aquí abajo... + +00:06:25.246 --> 00:06:26.016 align:middle +¡sabe que estamos + +00:06:26.516 --> 00:06:28.786 align:middle +en la Página 2! Si vamos +a la página siguiente... + +00:06:29.486 --> 00:06:30.916 align:middle +¡sí! ¡ + +00:06:31.416 --> 00:06:34.156 align:middle +Vemos un conjunto diferente +de resultados! Aunque, esto + +00:06:35.016 --> 00:06:37.926 align:middle +sigue siendo super feo. +Afortunadamente, el bundle + +00:06:38.446 --> 00:06:41.236 align:middle +nos ofrece una forma de controlar + +00:06:41.236 --> 00:06:44.916 align:middle +el marcado que se utiliza para +los enlaces de paginación. E + +00:06:45.616 --> 00:06:50.476 align:middle +incluso viene con soporte automático para el +marcado compatible con CSS de Bootstrap. Sólo + +00:06:51.036 --> 00:06:53.506 align:middle +tenemos que decirle al bundle que +lo utilice. Así que ... tenemos + +00:06:54.326 --> 00:06:56.716 align:middle +que configurar el bundle. Pero ... el bundle + +00:06:57.416 --> 00:07:03.216 align:middle +no nos ha proporcionado ningún archivo de +configuración nuevo al instalarlo. ¡No pasa nada! + +00:07:03.876 --> 00:07:04.506 align:middle +No todos los + +00:07:05.076 --> 00:07:07.936 align:middle +bundles nuevos nos dan archivos +de configuración. Pero en cuanto + +00:07:08.346 --> 00:07:10.726 align:middle +necesites uno, ¡crea uno! Como este bundle se + +00:07:11.416 --> 00:07:14.696 align:middle +llama BabdevPagerfantaBundle , voy a + +00:07:15.206 --> 00:07:20.226 align:middle +crear un nuevo archivo llamado +babdev_pagerfanta.yaml. Como + +00:07:21.146 --> 00:07:26.816 align:middle +aprendimos en el último tutorial, el nombre de +estos archivos no es importante. Lo importante + +00:07:27.346 --> 00:07:32.766 align:middle +es la clave raíz, que debe ser +babdev_pagerfanta. Para cambiar + +00:07:33.586 --> 00:07:37.226 align:middle +la forma en que se muestra la +paginación, añade default_view: twig y + +00:07:37.346 --> 00:07:46.746 align:middle +luego default_twig_template a +@BabDevPagerfanta/twitter_bootstrap5.html.twig. Como con cualquier + +00:07:47.676 --> 00:07:51.186 align:middle +otra configuración, no hay +forma de que sepas que ésta es + +00:07:51.186 --> 00:07:54.426 align:middle +la configuración correcta +simplemente adivinando. Tienes + +00:07:54.946 --> 00:07:56.426 align:middle +que consultar la documentación. Si + +00:07:57.276 --> 00:07:58.616 align:middle +volvemos atrás y actualizamos... +eh, no ha cambiado nada. + +00:07:59.376 --> 00:08:00.936 align:middle +Se trata de + +00:08:01.576 --> 00:08:05.346 align:middle +un pequeño error que a veces +te encuentras en Symfony cuando + +00:08:05.486 --> 00:08:07.916 align:middle +creas un nuevo archivo de +configuración. Symfony no se + +00:08:08.776 --> 00:08:10.816 align:middle +dio cuenta... y por eso + +00:08:11.106 --> 00:08:15.116 align:middle +no sabía que necesitaba +reconstruir su caché. Es + +00:08:15.816 --> 00:08:20.106 align:middle +una situación super rara, pero si alguna +vez crees que puede estar ocurriendo, es + +00:08:20.416 --> 00:08:27.186 align:middle +bastante fácil borrar manualmente la caché +ejecutando: php bin/console cache:clear Y... oh... + +00:08:27.566 --> 00:08:29.886 align:middle +explota. Probablemente + +00:08:30.256 --> 00:08:31.686 align:middle +te habrás dado cuenta de +por qué. ¡ Me encanta + +00:08:32.056 --> 00:08:33.066 align:middle +este error! + +00:08:33.606 --> 00:08:35.976 align:middle +No hay ninguna extensión capaz de cargar +la configuración para "baberdev_pagerfanta" + +00:08:35.976 --> 00:08:42.416 align:middle +Se supone que es babdev_pagerfanta. ¡Ups! Y + +00:08:42.716 --> 00:08:44.776 align:middle +ahora... ¡perfecto! Está + +00:08:46.006 --> 00:08:47.826 align:middle +contento. Y cuando + +00:08:48.436 --> 00:08:50.076 align:middle +actualizamos... ¡lo ve + +00:08:51.006 --> 00:08:52.096 align:middle +! En un + +00:08:52.706 --> 00:08:58.216 align:middle +proyecto real, probablemente querremos añadir algo +más de CSS para hacer este "modo oscuro"... pero ya + +00:08:58.686 --> 00:09:00.396 align:middle +lo tenemos. Bien equipo, + +00:09:01.346 --> 00:09:03.446 align:middle +¡básicamente hemos terminado! Como + +00:09:03.866 --> 00:09:07.186 align:middle +extra, vamos a refactorizar esta +paginación para convertirla en un + +00:09:07.416 --> 00:09:10.456 align:middle +desplazamiento eterno con JavaScript... +¡excepto el giro argumental + +00:09:11.046 --> 00:09:12.546 align:middle +! Vamos a + +00:09:12.886 --> 00:09:16.856 align:middle +hacerlo sin escribir una sola línea +de JavaScript. Eso a continuación. diff --git a/sfcasts/ep3-doctrine/es/param-converter.md b/sfcasts/ep3-doctrine/es/param-converter.md new file mode 100644 index 0000000..4df5bd1 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/param-converter.md @@ -0,0 +1,48 @@ +# Convertidor de parámetros y 404 + +Hemos programado el camino feliz. Cuando voy a `/mix/13`, mi base de datos sí encuentra una mezcla con ese id y... la vida es buena. Pero, ¿y si lo cambio a `/99`? Vaya, eso es un error 500: no es algo que queramos que nuestro sitio haga nunca. En realidad debería ser un error 404. Entonces, ¿cómo activamos un 404? + +## Activar una página 404 + +En el método, esta variable `$mix` será un objeto `VinylMix` o null si no se encuentra ninguno. Así que podemos decir `if (!$mix)`, y luego, para desencadenar un 404,`throw $this->createNotFoundException()`. Puedes darle un mensaje si quieres, pero sólo lo verán los desarrolladores. + +[[[ code('743daf7827') ]]] + +Este `createNotFoundException()`, como su nombre indica, crea un objeto de excepción. Así que en realidad estamos lanzando una excepción aquí... lo que está bien, porque significa que el código que sigue a esto no se ejecutará. + +Ahora bien, normalmente si tú o algo de tu código lanza una excepción, provocará un error 500. Pero este método crea un tipo especial de excepción que se corresponde con un 404. ¡Observa! Aquí, en la parte superior derecha, cuando refresco... ¡404! + +Por cierto, este no es el aspecto que tendrían las páginas 404 o 500 en producción. Si pasáramos al entorno `prod`, veríamos una página de error bastante genérica y sin detalles. Luego puedes personalizar su aspecto, incluso haciendo estilos separados para los errores 404, 403 Acceso Denegado, o incluso... gasp... 500 errores si algo va realmente mal. Consulta la documentación de Symfony para saber cómo personalizar las páginas de error. + +## Convertidor de parámetros: Consulta automática + +¡Muy bien! Hemos consultado un único objeto `VinylMix` e incluso hemos gestionado la ruta 404. Pero podemos hacerlo con mucho menos trabajo. ¡Compruébalo! Sustituye el argumento `$id` por un nuevo argumento, de tipo con nuestra clase de entidad `VinylMix`. Llámalo, qué tal, `$mix` para que coincida con la variable de abajo. Luego... elimina la consulta... y también el 404. Y ahora, ni siquiera necesitamos el argumento `$mixRepository`. + +[[[ code('11e978077a') ]]] + +Esto... merece alguna explicación. Hasta ahora, las "cosas" que se nos "permiten" como argumentos de nuestros controladores son (1) comodines de ruta como `$id` o (2) servicios. Ahora tenemos una tercera cosa. Cuando escribes una clase de entidad, Symfony consultará el objeto automáticamente. Como tenemos un comodín llamado `{id}`, tomará este valor (por ejemplo "99" o "16") y buscará un `VinylMix` cuyo `id`sea igual a ese. El nombre del comodín - `id` en este caso - debe coincidir con el nombre de la propiedad que debe utilizar para la consulta. + +Pero si vuelvo y actualizo... ¡no funciona! + +> No se puede autoconducir el argumento `$mix` de `MixController::show()`: hace referencia a +> `VinylMix` pero no existe tal servicio. + +Sabemos que no es un servicio... así que tiene sentido. Pero... ¿por qué no consulta el objeto como acabo de decir? + +Porque... para que esta función funcione, ¡tenemos que instalar otro bundle! Bueno, si estás usando Symfony 6.2 y un DoctrineBundle suficientemente nuevo - probablemente la versión 2.8 - entonces esto debería funcionar sin necesidad de nada más. Pero como estamos usando Symfony 6.1, necesitamos una librería extra. + +Busca tu terminal y di: + +```terminal +composer require sensio/framework-extra-bundle +``` + +Este es un bundle lleno de pequeños y bonitos atajos que, para Symfony 6.2, se habrán trasladado al propio Symfony. Así que, con el tiempo, no necesitarás esto. + +Y ahora... sin hacer nada más... ¡funciona! ¡Se ha consultado automáticamente el objeto `VinylMix` y la página se renderiza! Y si vas a un ID malo, como`/99`... ¡sí! ¡Compruébalo! ¡Obtenemos un 404! Esta función se llama "ParamConverter"... que se menciona en el error: + +> Objeto `VinylMix` no encontrado por la anotación `@ParamConverter`. + +En cualquier caso, me encanta esta función. Si necesito consultar varios objetos, como en la acción `browse()`, utilizaré el servicio de repositorio correcto. Pero si necesito consultar un solo objeto en un controlador, utilizo este truco. + +A continuación, vamos a hacer posible que nuestras mezclas sean votadas por arriba y por abajo aprovechando un simple formulario. Para ello, por primera vez, actualizaremos una entidad en la base de datos. diff --git a/sfcasts/ep3-doctrine/es/param-converter.vtt b/sfcasts/ep3-doctrine/es/param-converter.vtt new file mode 100644 index 0000000..bc90f4e --- /dev/null +++ b/sfcasts/ep3-doctrine/es/param-converter.vtt @@ -0,0 +1,254 @@ +WEBVTT + +00:00:01.006 --> 00:00:03.876 align:middle +Hemos programado el camino feliz. + +00:00:03.876 --> 00:00:10.576 align:middle +Cuando voy a /mix/13, mi base de datos sí +encuentra una mezcla con esa identificación y... + +00:00:10.676 --> 00:00:12.256 align:middle +la vida es buena. + +00:00:12.936 --> 00:00:16.326 align:middle +¿Pero qué pasa si lo cambio a /99? + +00:00:17.146 --> 00:00:24.056 align:middle +Vaya. Eso es un error 500: no es algo que +queramos que nuestro sitio haga nunca. + +00:00:24.736 --> 00:00:27.756 align:middle +En realidad debería ser un error 404. + +00:00:27.756 --> 00:00:30.996 align:middle +Entonces, ¿cómo provocamos un 404? + +00:00:31.786 --> 00:00:37.166 align:middle +En el método, esta variable +$mix será un objeto VinylMix + +00:00:37.416 --> 00:00:40.906 align:middle +o será nula si no se encuentra ninguno. + +00:00:40.906 --> 00:00:49.826 align:middle +Así que podemos decir if (!$mix), y luego, para activar +un 404, throw $this->createNotFoundException(). + +00:00:50.676 --> 00:00:55.046 align:middle +Puedes darle un mensaje si quieres, pero +sólo lo verán los desarrolladores. + +00:00:55.806 --> 00:01:02.606 align:middle +Este createNotFoundException(), como su +nombre indica, crea un objeto de excepción. + +00:01:02.606 --> 00:01:06.246 align:middle +Así que en realidad estamos +lanzando una excepción aquí... + +00:01:06.606 --> 00:01:12.176 align:middle +lo que está bien, porque significa que el +código que sigue a esto no se ejecutará. + +00:01:12.936 --> 00:01:17.596 align:middle +Ahora bien, normalmente si tú o algo +de tu código lanza una excepción, + +00:01:17.836 --> 00:01:20.626 align:middle +provocará un error 500. + +00:01:21.076 --> 00:01:26.196 align:middle +Pero este método crea un tipo especial de +excepción que se corresponde con un 404. + +00:01:26.946 --> 00:01:31.206 align:middle +¡Fíjate! Aquí, en la parte +superior derecha, cuando refresco... + +00:01:31.206 --> 00:01:40.356 align:middle +¡404! Por cierto, este no es el aspecto que +tendrían las páginas 404 o 500 en producción. + +00:01:41.016 --> 00:01:48.626 align:middle +Si pasáramos al entorno prod, veríamos una +página de error bastante genérica y sin detalles. + +00:01:49.276 --> 00:01:55.936 align:middle +Luego puedes personalizar su aspecto, incluso +haciendo estilos separados para los errores 404, + +00:01:55.936 --> 00:01:59.526 align:middle +403 Acceso denegado, o incluso... + +00:01:59.796 --> 00:02:04.456 align:middle +jadeo... 500 errores si algo va realmente mal. + +00:02:05.306 --> 00:02:08.836 align:middle +Consulta la documentación de Symfony para +saber cómo personalizar las páginas de error. + +00:02:09.696 --> 00:02:16.876 align:middle +¡Muy bien! Hemos consultado un único objeto +VinylMix e incluso hemos gestionado la ruta 404. + +00:02:16.876 --> 00:02:20.106 align:middle +Pero podemos hacer esto +con mucho menos trabajo. + +00:02:20.706 --> 00:02:21.206 align:middle +¡Compruébalo! + +00:02:21.576 --> 00:02:29.246 align:middle +Sustituye el argumento $id por un nuevo argumento, de +tipo "type-hinted" con nuestra clase de entidad VinylMix. + +00:02:30.046 --> 00:02:33.926 align:middle +Llámalo, qué tal, $mix para que +coincida con la variable de abajo. + +00:02:34.746 --> 00:02:36.276 align:middle +Luego... borra la consulta... + +00:02:36.906 --> 00:02:38.696 align:middle +y también el 404. + +00:02:39.426 --> 00:02:43.986 align:middle +Y ahora, ni siquiera necesitamos +el argumento $mixRepository. + +00:02:44.676 --> 00:02:47.426 align:middle +Esto... merece alguna explicación. Hasta + +00:02:48.046 --> 00:02:52.746 align:middle +ahora, las "cosas" que se nos +"permiten" como argumentos + +00:02:52.746 --> 00:02:59.746 align:middle +de nuestros controladores son (1) +comodines de ruta como $id o (2) servicios. + +00:03:00.476 --> 00:03:02.796 align:middle +Ahora tenemos una tercera cosa. + +00:03:03.346 --> 00:03:09.426 align:middle +Cuando escribes una clase de entidad, Symfony +consultará el objeto automáticamente. + +00:03:10.106 --> 00:03:19.926 align:middle +Como tenemos un comodín llamado {id}, tomará +este valor (por ejemplo "99" o "16") y buscará + +00:03:19.926 --> 00:03:24.266 align:middle +un VinylMix cuyo id sea igual a ese. + +00:03:25.106 --> 00:03:28.086 align:middle +El nombre del comodín - id en este caso - + +00:03:28.446 --> 00:03:32.296 align:middle +debe coincidir con el nombre de la propiedad +que debe utilizar para la consulta. + +00:03:33.236 --> 00:03:35.276 align:middle +Pero si vuelvo y actualizo... + +00:03:35.916 --> 00:03:37.536 align:middle +¿¡no funciona!? + +00:03:38.176 --> 00:03:41.546 align:middle +No se puede autoconducir el argumento +$mix de MixController::show(): + +00:03:41.946 --> 00:03:47.616 align:middle +hace referencia a VinylMix +pero no existe tal servicio. + +00:03:47.616 --> 00:03:50.036 align:middle +Sabemos que no es un servicio... + +00:03:50.036 --> 00:03:51.656 align:middle +así que tiene sentido. + +00:03:52.146 --> 00:03:56.506 align:middle +Pero... ¿por qué no consulta +el objeto como acabo de decir? + +00:03:57.246 --> 00:03:58.196 align:middle +Porque... + +00:03:58.546 --> 00:04:02.956 align:middle +para que esta función funcione, +¡tenemos que instalar otro bundle! + +00:04:03.686 --> 00:04:11.306 align:middle +Bueno, si estás usando Symfony 6.2 y un DoctrineBundle +suficientemente nuevo - probablemente la versión 2.8 - + +00:04:11.806 --> 00:04:15.796 align:middle +entonces esto debería funcionar +sin necesidad de nada más. + +00:04:16.446 --> 00:04:21.706 align:middle +Pero como estamos usando Symfony +6.1, necesitamos una librería extra. + +00:04:21.706 --> 00:04:30.186 align:middle +Busca tu terminal y di: composer require +sensio/framework-extra-bundle Este es un bundle + +00:04:30.186 --> 00:04:34.956 align:middle +lleno de pequeños y bonitos atajos +que, para Symfony 6.2, se habrán + +00:04:35.306 --> 00:04:39.416 align:middle +trasladado al propio Symfony. + +00:04:39.416 --> 00:04:42.896 align:middle +Así que, con el tiempo, no necesitarás esto. + +00:04:42.896 --> 00:04:43.596 align:middle +Y ahora... + +00:04:43.906 --> 00:04:45.776 align:middle +sin hacer nada más... + +00:04:46.006 --> 00:04:53.726 align:middle +¡funciona! ¡Se ha consultado automáticamente +el objeto VinylMix y la página se renderiza! + +00:04:54.376 --> 00:04:58.086 align:middle +Y si vas a un ID malo, como /99... + +00:04:58.546 --> 00:04:59.966 align:middle +¡sí! ¡Compruébalo! + +00:05:00.146 --> 00:05:02.086 align:middle +¡Obtenemos un 404! + +00:05:02.946 --> 00:05:05.606 align:middle +Esta función se llama "ParamConverter"... + +00:05:05.846 --> 00:05:11.826 align:middle +que se menciona en el error: VinylMix objeto no +encontrado por la anotación @ParamConverter. + +00:05:12.696 --> 00:05:15.226 align:middle +En cualquier caso, me encanta esta +función. Si necesito consultar + +00:05:16.006 --> 00:05:20.956 align:middle +varios objetos, como en la acción browse(), + +00:05:21.446 --> 00:05:24.396 align:middle +utilizaré el servicio de repositorio correcto. + +00:05:24.976 --> 00:05:30.466 align:middle +Pero si necesito consultar un solo objeto +en un controlador, utilizo este truco. + +00:05:31.366 --> 00:05:37.586 align:middle +A continuación, vamos a hacer posible que nuestras mezclas sean +votadas por arriba y por abajo aprovechando un simple formulario. + +00:05:38.316 --> 00:05:43.726 align:middle +Para ello, por primera vez, actualizaremos +una entidad en la base de datos diff --git a/sfcasts/ep3-doctrine/es/persist.md b/sfcasts/ep3-doctrine/es/persist.md new file mode 100644 index 0000000..5cf39b5 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/persist.md @@ -0,0 +1,58 @@ +# Persistir en la base de datos + +Ahora que tenemos una clase de entidad y la tabla correspondiente, ¡estamos listos para guardar algunas cosas! Entonces... ¿cómo insertamos filas en la tabla? Pregunta equivocada! Sólo vamos a centrarnos en crear objetos y guardarlos. Doctrine se encargará de las consultas de inserción por nosotros. + +Para ayudar a hacer esto de la forma más sencilla posible, vamos a hacer una página falsa de "nueva mezcla de vinilo". + +En el directorio `src/Controller/`, crea una nueva clase `MixController` y haz que ésta extienda la normal `AbstractController`. Perfecto Dentro, añade un`public function` llamado `new()` que devolverá un `Response` de HttpFoundation. Para que esto sea una página, arriba, utiliza el atributo `#[Route]`, dale a "tab" para autocompletarlo y llamemos a la URL `/mix/new`. Por último, para ver si esto funciona, `dd('new mix')`. + +[[[ code('550f9491ea') ]]] + +En el mundo real, esta página podría mostrar un formulario. Entonces, al enviar ese formulario, tomaríamos sus datos, crearíamos un objeto `VinylMix()` y lo guardaríamos. Trabajaremos en cosas así en un futuro tutorial. Por ahora, vamos a ver si esta página funciona. Dirígete a `/mix/new` y... ¡ya está! + +Bien, ¡vamos a crear un objeto `VinylMix()`! Hazlo con `$mix = new VinylMix()`... ¡y entonces podremos empezar a poner datos en él! Vamos a crear una mezcla de uno de mis artistas favoritos de la infancia. Voy a establecer rápidamente algunas otras propiedades... tenemos que establecer, como mínimo, todas las propiedades que tienen columnas necesarias en la base de datos. Para `trackCount`, qué tal un poco de aleatoriedad para divertirse. Y, para `votes`, lo mismo... incluyendo votos negativos... aunque Internet nunca sería tan cruel como para votar a la baja ninguna de mis mezclas. Por último, `dd($mix)`. + +[[[ code('1fe585eee9') ]]] + +Hasta ahora, esto no tiene nada que ver con la Doctrine. Sólo estamos creando un objeto y poniendo datos en él. Estos datos están codificados, pero puedes imaginar que se sustituyen por lo que el usuario acaba de enviar a través de un formulario. Independientemente de dónde obtengamos los datos, cuando actualicemos... tendremos un objeto con datos en él. ¡Genial! + +## Servicios vs. Entidades + +Por cierto, nuestra clase de entidad, `VinylMix`, es la primera clase que hemos creado que no es un servicio. En general, hay dos tipos de clases. En primer lugar, están los objetos de servicio, como `TalkToMeCommand` o el `MixRepository` que creamos en el último tutorial. Estos objetos funcionan... pero no contienen ningún dato, aparte de quizás alguna configuración básica. Y siempre obtenemos los servicios del contenedor, normalmente mediante autoconexión. Nunca los instanciamos directamente. + +El segundo tipo de clases son las clases de datos como `VinylMix`. El trabajo principal de estas clases es mantener los datos. No suelen hacer ningún trabajo, salvo quizá alguna manipulación básica de datos. Y a diferencia de los servicios, no obtenemos estos objetos del contenedor. En su lugar, los creamos manualmente donde y cuando los necesitemos, ¡como acabamos de hacer! + +## ¡Hola Gestor de Entidades! + +De todos modos, ahora que tenemos un objeto, ¿cómo podemos guardarlo? Bueno, guardar algo en la base de datos es un trabajo. Y por eso, no es de extrañar, ¡ese trabajo lo hace un servicio! Añade un argumento al método, indicado con `EntityManagerInterface`. Llamémoslo `$entityManager`. + +`EntityManagerInterface` es, con mucho, el servicio más importante para Doctrine. Lo vamos a utilizar para guardar, e indirectamente cuando hagamos una consulta. Para guardar, llamamos a`$entityManager->persist()` y le pasamos el objeto que queremos guardar (en este caso, `$mix`). Luego también tenemos que llamar a `$entityManager->flush()` sin argumentos. + +[[[ code('7fcf0dd0be') ]]] + +Pero... espera. ¿Por qué tenemos que llamar a dos métodos? + +Esto es lo que pasa. Cuando llamamos a `persist()`, en realidad no guarda el objeto ni habla con la base de datos. Sólo le dice a Doctrine: + +> ¡Oye! Quiero que seas "consciente" de este objeto, para que luego, cuando llamemos a `flush()`, +> sabrá que debe guardarlo. + +La mayoría de las veces, verás estas dos líneas juntas: `persist()` y luego`flush()`. La razón por la que está dividido en dos métodos es para ayudar a la carga de datos por lotes... donde podrías persistir un centenar de objetos de `$mix` y luego vaciarlos en la base de datos todos a la vez, lo que es más eficiente. Pero la mayoría de las veces, llamarás a `persist()` y luego a `flush()`. + +Bien, para que sea una página válida, vamos a `return new Response()` de HttpFoundation y usaré `sprintf` para devolver un mensaje:`mix %d is %d tracks of pure 80\'s heaven`... y para esos dos comodines, pasaré `$mix->getId()` y `$mix->getTrackCount()`. + +[[[ code('120c0e0334') ]]] + +¡Vamos a probarlo! Muévete, refresca y... ¡sí! Vemos el "Mix 1". ¡Qué bien! En realidad, nunca pusimos el ID (lo que tiene sentido). Pero cuando guardamos, Doctrine cogió el nuevo ID y lo puso en la propiedad `id`. + +Si refrescamos unas cuantas veces más, obtendremos las mezclas 2, 3, 4, 5 y 6. Es súper divertido. Todo lo que hemos tenido que hacer es persistir y vaciar el objeto. Doctrine se encarga de todas las consultas por nosotros. + +Otra forma de demostrar que esto funciona es ejecutando: + +```terminal +symfony console doctrine:query:sql 'SELECT * FROM vinyl_mix' +``` + +Esta vez sí vemos los resultados. ¡Genial! + +Bien, ahora que tenemos cosas en la base de datos, ¿cómo las consultamos? Vamos a abordar eso a continuación. diff --git a/sfcasts/ep3-doctrine/es/persist.vtt b/sfcasts/ep3-doctrine/es/persist.vtt new file mode 100644 index 0000000..52deaac --- /dev/null +++ b/sfcasts/ep3-doctrine/es/persist.vtt @@ -0,0 +1,340 @@ +WEBVTT + +00:00:01.016 --> 00:00:02.596 align:middle +Ahora que tenemos una clase de entidad + +00:00:02.596 --> 00:00:06.716 align:middle +y la tabla correspondiente, ¡estamos +listos para guardar algunas cosas! + +00:00:07.416 --> 00:00:10.696 align:middle +Entonces... ¿cómo +insertamos filas en la tabla? + +00:00:11.316 --> 00:00:12.756 align:middle +¡Pregunta equivocada! + +00:00:13.306 --> 00:00:18.256 align:middle +Sólo vamos a centrarnos en +crear objetos y guardarlos. + +00:00:18.816 --> 00:00:21.836 align:middle +Doctrine se encargará de las +consultas de inserción por nosotros. + +00:00:22.646 --> 00:00:28.886 align:middle +Para ayudar a hacer esto de la forma más sencilla posible, +vamos a hacer una página falsa de "nueva mezcla de vinilo". + +00:00:29.726 --> 00:00:34.076 align:middle +En el directorio src/Controller/, +crea una nueva clase MixController + +00:00:35.576 --> 00:00:38.986 align:middle +y haz que ésta extienda la +normal AbstractController. + +00:00:39.676 --> 00:00:43.516 align:middle +Perfecto Dentro, añade un +public function llamado new() + +00:00:44.046 --> 00:00:47.486 align:middle +que devolverá un Response de HttpFoundation. + +00:00:48.336 --> 00:00:53.786 align:middle +Para que sea una página, arriba, utiliza el +atributo #[Route], dale a "tab" para autocompletarlo + +00:00:53.786 --> 00:00:57.716 align:middle +y llamemos a la URL /mix/new. + +00:00:58.476 --> 00:01:02.866 align:middle +Por último, para ver si +esto funciona, dd('new mix'). + +00:01:03.746 --> 00:01:06.986 align:middle +En el mundo real, esta página +podría mostrar un formulario. + +00:01:07.446 --> 00:01:11.386 align:middle +Luego, al enviar ese formulario, +tomaríamos sus datos, + +00:01:11.606 --> 00:01:14.566 align:middle +crearíamos un objeto +VinylMix() y lo guardaríamos. + +00:01:15.176 --> 00:01:17.986 align:middle +Trabajaremos en cosas así +en un futuro tutorial. + +00:01:18.726 --> 00:01:21.576 align:middle +Por ahora, vamos a ver +si esta página funciona. + +00:01:22.006 --> 00:01:24.866 align:middle +Dirígete a /mix/new y... + +00:01:25.286 --> 00:01:29.646 align:middle +¡lo tienes! Bien, ¡vamos a +crear un objeto VinylMix()! + +00:01:30.046 --> 00:01:32.666 align:middle +Hazlo con $mix = new VinylMix()... + +00:01:33.166 --> 00:01:35.406 align:middle +¡y luego podemos empezar a poner datos en él! + +00:01:36.246 --> 00:01:40.646 align:middle +Vamos a crear una mezcla de uno de +mis artistas favoritos de la infancia. + +00:01:41.386 --> 00:01:43.946 align:middle +Voy a establecer rápidamente +algunas otras propiedades... + +00:01:44.646 --> 00:01:48.286 align:middle +tenemos que establecer, como +mínimo, todas las propiedades + +00:01:48.286 --> 00:01:51.406 align:middle +que tienen columnas necesarias +en la base de datos. + +00:01:52.346 --> 00:01:55.596 align:middle +Para trackCount, qué tal un poco +de aleatoriedad para divertirse. + +00:01:56.776 --> 00:01:59.386 align:middle +Y, para votes, lo mismo... + +00:01:59.386 --> 00:02:01.246 align:middle +incluyendo los votos negativos... + +00:02:01.646 --> 00:02:08.116 align:middle +aunque Internet nunca sería tan cruel como +para votar negativamente alguna de mis mezclas. + +00:02:08.986 --> 00:02:10.686 align:middle +Por último, dd($mix). + +00:02:11.266 --> 00:02:14.086 align:middle +Hasta ahora, esto no tiene +nada que ver con la Doctrine. + +00:02:14.446 --> 00:02:17.766 align:middle +Sólo estamos creando un +objeto y poniendo datos en él. + +00:02:18.606 --> 00:02:22.456 align:middle +Estos datos están codificados, pero +puedes imaginar que se sustituyen + +00:02:22.456 --> 00:02:25.786 align:middle +por lo que el usuario acaba de +enviar a través de un formulario. + +00:02:26.616 --> 00:02:29.966 align:middle +Independientemente de dónde obtengamos +los datos, cuando actualicemos... + +00:02:30.376 --> 00:02:32.936 align:middle +tenemos un objeto con datos en él. + +00:02:33.546 --> 00:02:37.616 align:middle +¡Genial! Por cierto, nuestra +clase de entidad, VinylMix, + +00:02:38.026 --> 00:02:42.666 align:middle +es la primera clase que hemos +creado que no es un servicio. + +00:02:43.386 --> 00:02:46.306 align:middle +En general, hay dos tipos de clases. En + +00:02:46.616 --> 00:02:51.166 align:middle +primer lugar, están los objetos +de servicio, como TalkToMeCommand + +00:02:51.216 --> 00:02:54.956 align:middle +o el MixRepository que creamos +en el último tutorial. + +00:02:55.776 --> 00:02:58.336 align:middle +Estos objetos funcionan... + +00:02:58.716 --> 00:03:03.916 align:middle +pero no contienen ningún dato, aparte +de quizás alguna configuración básica. + +00:03:04.616 --> 00:03:09.556 align:middle +Y siempre obtenemos los servicios del +contenedor, normalmente mediante autoconexión. + +00:03:10.106 --> 00:03:12.726 align:middle +Nunca los instanciamos directamente. + +00:03:13.676 --> 00:03:18.416 align:middle +El segundo tipo de clases son las +clases de datos como VinylMix. + +00:03:19.016 --> 00:03:23.396 align:middle +El trabajo principal de estas +clases es mantener los datos. + +00:03:24.046 --> 00:03:29.686 align:middle +No suelen hacer ningún trabajo, salvo +quizá alguna manipulación básica de datos. + +00:03:30.306 --> 00:03:34.986 align:middle +Y a diferencia de los servicios, no +obtenemos estos objetos del contenedor. + +00:03:35.516 --> 00:03:41.856 align:middle +En su lugar, los creamos manualmente donde y +cuando los necesitemos, ¡como acabamos de hacer! + +00:03:42.716 --> 00:03:46.786 align:middle +De todos modos, ahora que tenemos un +objeto, ¿cómo podemos guardarlo? + +00:03:47.546 --> 00:03:50.836 align:middle +Bueno, guardar algo en la +base de datos es un trabajo. + +00:03:51.216 --> 00:03:55.836 align:middle +Y por eso, no es de extrañar, +¡ese trabajo lo hace un servicio! + +00:03:56.566 --> 00:04:01.456 align:middle +Añade un argumento al método, +indicado con EntityManagerInterface. + +00:04:01.976 --> 00:04:03.616 align:middle +Llamémoslo $entityManager. + +00:04:04.406 --> 00:04:09.926 align:middle +EntityManagerInterface es, con mucho, el +servicio más importante para Doctrine. + +00:04:10.476 --> 00:04:15.326 align:middle +Lo vamos a utilizar para guardar, e +indirectamente cuando hagamos una consulta. + +00:04:16.246 --> 00:04:21.386 align:middle +Para guardar, llamamos a +$entityManager->persist() y le pasamos el objeto + +00:04:21.386 --> 00:04:24.386 align:middle +que queremos guardar (en este caso, $mix). + +00:04:25.016 --> 00:04:31.276 align:middle +Luego también tenemos que llamar a +$entityManager->flush() sin argumentos. + +00:04:31.976 --> 00:04:32.796 align:middle +Pero... espera. + +00:04:33.106 --> 00:04:35.536 align:middle +¿Por qué tenemos que llamar a dos métodos? + +00:04:36.276 --> 00:04:36.886 align:middle +Esto es lo que pasa. + +00:04:37.486 --> 00:04:44.526 align:middle +Cuando llamamos a persist(), en realidad no +guarda el objeto ni habla con la base de datos. + +00:04:45.136 --> 00:04:47.316 align:middle +Sólo le dice a Doctrine: ¡Oye! + +00:04:47.666 --> 00:04:51.146 align:middle +Quiero que seas "consciente" de este objeto, + +00:04:51.546 --> 00:04:55.436 align:middle +para que luego, cuando llamemos a +flush(), sepas que debes guardarlo. + +00:04:56.176 --> 00:05:01.146 align:middle +La mayoría de las veces, verás estas dos +líneas juntas: persist() y luego flush(). + +00:05:01.876 --> 00:05:07.086 align:middle +La razón por la que se divide en dos métodos +es para ayudar a la carga de datos por lotes... + +00:05:07.586 --> 00:05:14.446 align:middle +donde podrías persistir un centenar de objetos de +$mix y luego vaciarlos en la base de datos todos + +00:05:14.446 --> 00:05:16.606 align:middle +a la vez, lo que es más eficiente. + +00:05:17.146 --> 00:05:20.686 align:middle +Pero la mayoría de las veces, +llamarás a persist() y luego a flush(). + +00:05:21.616 --> 00:05:28.016 align:middle +Bien, para que esto sea una página válida, +vamos a return new Response() de HttpFoundation + +00:05:28.416 --> 00:05:37.016 align:middle +y usaré sprintf para devolver un mensaje: +mix %d is %d tracks of pure 80\'s heaven... + +00:05:37.726 --> 00:05:44.466 align:middle +y para esos dos comodines, pasa +$mix->getId() y $mix->getTrackCount(). + +00:05:45.396 --> 00:05:46.066 align:middle +¡Vamos a probarlo! + +00:05:46.556 --> 00:05:48.556 align:middle +Muévete, refresca y... + +00:05:48.976 --> 00:05:51.656 align:middle +¡sí! Vemos el "Mix 1". + +00:05:51.946 --> 00:05:53.246 align:middle +¡Qué bien! + +00:05:54.106 --> 00:05:58.186 align:middle +En realidad, nunca establecimos +el ID (lo que tiene sentido). + +00:05:58.616 --> 00:06:05.286 align:middle +Pero cuando guardamos, Doctrine cogió +el nuevo ID y lo puso en la propiedad id. + +00:06:06.156 --> 00:06:11.346 align:middle +Si refrescamos unas cuantas veces más, +obtendremos las mezclas 2, 3, 4, 5 y 6. + +00:06:11.936 --> 00:06:13.546 align:middle +Eso es súper divertido. + +00:06:14.266 --> 00:06:17.516 align:middle +Lo único que hemos tenido que hacer +es persistir y vaciar el objeto. + +00:06:18.046 --> 00:06:21.466 align:middle +Doctrine se encarga de todas +las consultas por nosotros. + +00:06:22.376 --> 00:06:25.056 align:middle +Otra forma de demostrar que +esto funciona es ejecutando + +00:06:25.196 --> 00:06:28.926 align:middle +symfony console doctrine:query:sql 'SELECT * + +00:06:28.926 --> 00:06:33.906 align:middle +FROM vinyl_mix' Esta vez +sí vemos los resultados. + +00:06:34.466 --> 00:06:40.296 align:middle +¡Genial! Bien, ahora que tenemos cosas en +la base de datos, ¿cómo las consultamos? + +00:06:40.846 --> 00:06:42.526 align:middle +Vamos a abordar eso a continuación diff --git a/sfcasts/ep3-doctrine/es/queries.md b/sfcasts/ep3-doctrine/es/queries.md new file mode 100644 index 0000000..9fc55a7 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/queries.md @@ -0,0 +1,63 @@ +# Consulta de la base de datos + +Ahora que hemos guardado algunas cosas en la base de datos, ¿cómo podemos leerlas o consultarlas? Una vez más, al menos para las cosas sencillas, Doctrine no quiere que te preocupes de consultarlas. En lugar de eso, simplemente pedimos a Doctrine los objetos que queremos. + +Dirígete a `src/Controller/VinylController.php` y busca la acción `browse()`. + +[[[ code('0ff369b74b') ]]] + +Aquí, estamos cargando todo el `$mixes` en nuestro proyecto... y actualmente lo estamos haciendo a través de esta clase de servicio `MixRepository` que creamos en el último episodio. Esta clase habla con un repositorio de GitHub y lee desde un archivo de texto codificado. + +Vamos a dejar de usar este `MixRepository` y en su lugar cargaremos estos `$mixes`desde la base de datos. + +## Consulta a través del Gestor de Entidades + +Bien: para guardar los objetos, aprovechamos el servicio `EntityManagerInterface`, que es el más importante con diferencia en Doctrine. Además, este servicio puede consultar los objetos. Aprovechemos eso. Añade un nuevo argumento a `browse()`, de tipo `EntityManagerInterface`... y llámalo `$entityManager`. + +[[[ code('58ed044c51') ]]] + +A continuación, sustituye la línea `$mixes` por dos líneas. Empieza con`$mixRepository = $entityManager->getRepository()` pasándole el nombre de la clase desde la que queremos consultar. Sí, pensamos en consultar desde una clase de entidad, no desde una tabla. En este caso, queremos consultar desde `VinylMix::class`. + +Hablaremos más sobre este concepto de repositorio en un minuto. A continuación, para obtener las mezclas en sí, digamos `$mixes = $mixRepository->` y llamemos a uno de los métodos de la misma: `findAll()`. + +Para ver qué nos da esto, vamos a `dd($mixes)`. + +[[[ code('c3c83f32f3') ]]] + +Bien, ¡es hora de probar! Gira, vuelve a la página de inicio, haz clic en "Examinar las mezclas" para realizar esa acción, y... ¡voilá! ¡Obtenemos seis resultados! Y cada uno de ellos, lo más importante, es un objeto `VinylMix`. + +Entre bastidores, Doctrine consultó la tabla y las columnas. Pero en lugar de darnos esos datos en bruto, los puso en objetos y nos los dio, lo cual es mucho más agradable. + +## Trabajar con objetos en Twig + +Si eliminamos el `dd()`... este array de objetos `VinylMix` se pasará a la plantilla, en lugar del array de datos que teníamos antes. Pero... la página sigue funcionando. Sin embargo, estas imágenes están rotas porque, al parecer, el servicio que estoy utilizando para cargarlas no funciona en este momento. Ah... las alegrías de la grabación de vídeo. ¡Pero eso no nos detendrá! + +El hecho de que todos los datos se sigan renderizando sin errores es... en realidad un poco por suerte. Cuando renderizamos la plantilla - `templates/vinyl/browse.html.twig` - hacemos un bucle sobre todos los `mixes`. La plantilla funciona porque el antiguo archivo de texto del repositorio de GitHub tenía las mismas claves (como `title`, `trackCount` y `genre`) que nuestra clase`VinylMix`. + +[[[ code('542a2ad0f1') ]]] + +Sin embargo, aquí ocurre algo interesante. Cuando decimos `mix.genre`,`mix` es ahora un objeto... y esta propiedad `genre` es privada. Eso significa que no podemos acceder a ella directamente. Pero Twig es inteligente. Se da cuenta de que es privada y busca un método `getGenre()`. Así que en nuestra plantilla, decimos `mix.genre`, pero en realidad, llama al método `getGenre()`. Eso es bastante asombroso. + +## Visualización de las consultas de la página + +¿Sabes qué más es impresionante? ¡Podemos ver las consultas que hace cualquier página! En la barra de herramientas de depuración de la web, Doctrine nos ofrece un nuevo y elegante icono. Oooo. Y si hacemos clic en él... ¡tah dah! Hay una consulta a la base de datos... e incluso podemos ver de qué se trata. También se puede ver una versión formateada de la misma... aunque tengo que actualizar la página para que esto funcione... porque la biblioteca Turbo JavaScript que instalamos en el primer tutorial no siempre se lleva bien con esta zona del perfilador. De todos modos, también podemos ver una versión ejecutable de la consulta o ejecutar "Explicar" sobre ella. + +## El "Repositorio" + +Muy bien, de vuelta al controlador, aunque podemos consultar a través de`EntityManagerInterface`, normalmente consultamos a través de algo llamado repositorio.`dd()` este objeto `$mixRepository` para obtener más información sobre él + +[[[ code('0c8f951058') ]]] + +Luego vuelve a la página `/browse` y... es un objeto`App\Repository\VinylMixRepository`. Oye, ¡conocemos esa clase! Vive en nuestro código, en el directorio `src/Repository/`. Fue generada por MakerBundle. + +Dentro del atributo `ORM\Entity` sobre nuestra clase de entidad, MakerBundle generó una opción `repositoryClass` que apunta a esto. Gracias a esta configuración, nuestra entidad, `VinylMix`, está vinculada a `VinylMixRepository`. Así que cuando le pides a Doctrine que nos dé el repositorio de la clase `VinylMix`, sabe que debe devolver el objeto`VinylMixRepository`. + +El repositorio de una entidad lo sabe todo sobre cómo consultar sus datos. Y, sin que nosotros hagamos nada, ya tiene un montón de métodos útiles para las consultas básicas, como `findAll()`, `findOneBy()` y varios más. Dentro de un rato, aprenderemos a añadir nuevos métodos al repositorio para realizar consultas personalizadas. + +De todos modos, `VinylMixRepository` es en realidad un servicio en el contenedor... así que podemos obtenerlo más fácilmente autoconectándolo directamente. Añade un argumento`VinylMixRepository $mixRepository`... y entonces no necesitaremos esta línea. Esto es más sencillo... ¡y sigue funcionando! + +[[[ code('a79c813794') ]]] + +La conclusión es ésta: si quieres consultar una tabla, lo harás a través del repositorio de la entidad cuyos datos necesitas. + +Siguiente: El hecho de que hayamos cambiado nuestro código para cargarlo desde la base de datos y no hayamos tenido que actualizar nuestra plantilla Twig en absoluto fue algo impresionante Y por cortesía de un poco de magia Twig. Vamos a hablar más de esa magia y a crear una propiedad virtual que podemos imprimir en la plantilla. diff --git a/sfcasts/ep3-doctrine/es/queries.vtt b/sfcasts/ep3-doctrine/es/queries.vtt new file mode 100644 index 0000000..6d3d4b5 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/queries.vtt @@ -0,0 +1,346 @@ +WEBVTT + +00:00:01.006 --> 00:00:06.086 align:middle +Ahora que hemos guardado algunas cosas en la base +de datos, ¿cómo podemos leerlas o consultarlas? + +00:00:06.906 --> 00:00:12.816 align:middle +Una vez más, al menos para las cosas sencillas, +Doctrine no quiere que te preocupes de hacer consultas. + +00:00:13.346 --> 00:00:17.806 align:middle +En lugar de eso, simplemente pedimos +a Doctrine los objetos que queremos. + +00:00:18.646 --> 00:00:23.656 align:middle +Dirígete a src/Controller/VinylController.php +y busca la acción browse(). + +00:00:24.386 --> 00:00:27.816 align:middle +Aquí, estamos cargando todos +los $mixes de nuestro proyecto... + +00:00:28.406 --> 00:00:32.836 align:middle +y lo estamos haciendo a través de +esta clase de servicio MixRepository + +00:00:33.116 --> 00:00:35.396 align:middle +que creamos en el último episodio. + +00:00:36.146 --> 00:00:42.286 align:middle +Esta clase habla con un repositorio de GitHub +y lee desde un archivo de texto codificado. + +00:00:43.116 --> 00:00:50.286 align:middle +Vamos a dejar de usar este MixRepository y en su +lugar cargaremos estos $mixes desde la base de datos. + +00:00:51.236 --> 00:00:56.736 align:middle +Bien: para guardar los objetos, aprovechamos +el servicio EntityManagerInterface, + +00:00:57.176 --> 00:01:01.716 align:middle +que es el más importante +con diferencia en Doctrine. + +00:01:02.646 --> 00:01:06.336 align:middle +Además, este servicio también +puede consultar los objetos. + +00:01:06.886 --> 00:01:08.366 align:middle +Aprovechemos eso. + +00:01:09.026 --> 00:01:13.526 align:middle +Añade un nuevo argumento a browse(), +de tipo EntityManagerInterface... + +00:01:14.186 --> 00:01:15.826 align:middle +y llámalo $entityManager. + +00:01:16.686 --> 00:01:21.336 align:middle +A continuación, sustituye la +línea $mixes por dos líneas. + +00:01:21.976 --> 00:01:28.446 align:middle +Empieza con $mixRepository = +$entityManager->getRepository() pasándole el + +00:01:28.446 --> 00:01:32.156 align:middle +nombre de la clase desde +la que queremos consultar. + +00:01:32.806 --> 00:01:38.026 align:middle +Sí, pensamos en consultar desde una +clase de entidad, no desde una tabla. + +00:01:38.836 --> 00:01:42.326 align:middle +En este caso, queremos +consultar desde VinylMix::class. + +00:01:42.326 --> 00:01:47.496 align:middle +Hablaremos más sobre este concepto +de repositorio en un minuto. + +00:01:48.246 --> 00:01:53.686 align:middle +A continuación, para obtener las mezclas +en sí, digamos $mixes = $mixRepository-> + +00:01:53.746 --> 00:01:57.786 align:middle +y llamemos a uno de los +métodos de la misma: findAll(). + +00:01:58.646 --> 00:02:02.516 align:middle +Para ver qué nos da esto, vamos a dd($mixes). + +00:02:03.446 --> 00:02:04.616 align:middle +Bien, ¡es hora de probar! + +00:02:04.906 --> 00:02:12.286 align:middle +Gira, vuelve a la página de inicio, haz clic en +"Examinar las mezclas" para acceder a esa acción, y... + +00:02:12.606 --> 00:02:15.656 align:middle +¡voilá! ¡Obtenemos seis resultados! + +00:02:16.216 --> 00:02:21.966 align:middle +Y cada uno de ellos, lo más +importante, es un objeto VinylMix. + +00:02:22.516 --> 00:02:26.626 align:middle +Entre bastidores, Doctrine ha +consultado la tabla y las columnas. + +00:02:27.046 --> 00:02:32.306 align:middle +Pero en lugar de darnos esos datos +en bruto, los puso en objetos + +00:02:32.376 --> 00:02:36.196 align:middle +y nos los dio, lo cual es mucho más bonito. + +00:02:37.006 --> 00:02:38.646 align:middle +Si eliminamos el dd()... + +00:02:39.046 --> 00:02:45.896 align:middle +esta matriz de objetos VinylMix se pasará +a la plantilla, en lugar de la matriz + +00:02:45.896 --> 00:02:48.486 align:middle +de datos que teníamos antes. + +00:02:49.386 --> 00:02:52.346 align:middle +Pero... la página sigue funcionando. + +00:02:52.846 --> 00:02:55.016 align:middle +Aunque, estas imágenes están rotas + +00:02:55.356 --> 00:03:00.506 align:middle +porque aparentemente el servicio que estoy usando +para cargarlas está caído en este momento. + +00:03:01.176 --> 00:03:03.886 align:middle +Ah... las alegrías de la grabación de vídeo. + +00:03:04.516 --> 00:03:06.026 align:middle +¡Pero eso no nos detendrá! + +00:03:06.676 --> 00:03:11.446 align:middle +El hecho de que todos los datos se +sigan mostrando sin errores es... + +00:03:11.706 --> 00:03:13.786 align:middle +en realidad es un poco por suerte. + +00:03:14.576 --> 00:03:15.966 align:middle +Cuando renderizamos la plantilla - + +00:03:16.286 --> 00:03:22.226 align:middle +templates/vinyl/browse.html.twig - +hacemos un bucle sobre todos los mixes. + +00:03:23.016 --> 00:03:29.866 align:middle +La plantilla funciona porque el antiguo archivo de texto +del repositorio de GitHub tenía las mismas claves (como + +00:03:29.946 --> 00:03:34.876 align:middle +title, trackCount y genre) +que nuestra clase VinylMix. + +00:03:35.586 --> 00:03:38.276 align:middle +Sin embargo, aquí ocurre algo interesante. + +00:03:38.986 --> 00:03:43.516 align:middle +Cuando decimos mix.genre, +mix es ahora un objeto... + +00:03:44.026 --> 00:03:47.166 align:middle +y esta propiedad genre es privada. + +00:03:47.846 --> 00:03:50.876 align:middle +Eso significa que no podemos +acceder a ella directamente. + +00:03:51.476 --> 00:03:53.356 align:middle +Pero Twig es inteligente. + +00:03:54.256 --> 00:03:59.656 align:middle +Se da cuenta de que es privada +y busca un método getGenre(). + +00:04:00.406 --> 00:04:08.586 align:middle +Así que en nuestra plantilla, decimos mix.genre, +pero en realidad, llama al método getGenre(). + +00:04:09.146 --> 00:04:10.716 align:middle +Eso es bastante asombroso. + +00:04:11.506 --> 00:04:13.726 align:middle +¿Sabes qué más es impresionante? + +00:04:14.146 --> 00:04:17.226 align:middle +¡Podemos ver las consultas +que hace cualquier página! + +00:04:18.046 --> 00:04:22.646 align:middle +En la barra de herramientas de depuración de la +web, Doctrine nos ofrece un nuevo y elegante icono. + +00:04:22.646 --> 00:04:26.306 align:middle +Oooo. Y si hacemos clic en él... + +00:04:26.706 --> 00:04:29.686 align:middle +¡tah dah! Hay una consulta +a la base de datos... + +00:04:30.086 --> 00:04:32.346 align:middle +e incluso podemos ver de qué se trata. + +00:04:33.136 --> 00:04:35.616 align:middle +También puedes ver una versión +formateada de la misma... + +00:04:35.776 --> 00:04:38.756 align:middle +aunque tengo que actualizar la +página para que esto funcione... + +00:04:39.236 --> 00:04:42.446 align:middle +porque la librería Turbo +JavaScript que instalamos + +00:04:42.446 --> 00:04:47.656 align:middle +en el primer tutorial no siempre se +lleva bien con esta zona del perfilador. + +00:04:48.536 --> 00:04:53.956 align:middle +De todos modos, también podemos ver una versión +ejecutable de la consulta o ejecutar "Explicar" sobre ella. + +00:04:54.846 --> 00:05:00.226 align:middle +Muy bien, de vuelta en el controlador, aunque podemos +consultar a través del EntityManagerInterface, + +00:05:00.446 --> 00:05:05.286 align:middle +normalmente consultamos a través +de algo llamado el repositorio + +00:05:06.146 --> 00:05:10.366 align:middle +dd() este objeto $mixRepository para +obtener más información sobre él. + +00:05:11.136 --> 00:05:14.496 align:middle +Entonces vuelve a la página /browse y... + +00:05:15.156 --> 00:05:20.286 align:middle +es un objeto App\Repository\VinylMixRepository. + +00:05:20.746 --> 00:05:23.036 align:middle +¡Ya conocemos esa clase! + +00:05:23.596 --> 00:05:28.386 align:middle +Vive en nuestro código, en +el directorio src/Repository/. + +00:05:29.036 --> 00:05:31.216 align:middle +Fue generada por MakerBundle. + +00:05:32.046 --> 00:05:35.926 align:middle +Dentro del atributo ORM\Entity +sobre nuestra clase de entidad, + +00:05:36.346 --> 00:05:42.216 align:middle +MakerBundle generó una opción +repositoryClass que apunta a esto. + +00:05:43.146 --> 00:05:49.856 align:middle +Gracias a esta configuración, nuestra entidad, +VinylMix, está vinculada a VinylMixRepository. + +00:05:50.536 --> 00:05:55.706 align:middle +Así que cuando le pides a Doctrine que nos +dé el repositorio de la clase VinylMix, + +00:05:56.196 --> 00:06:00.156 align:middle +sabe que debe devolver el +objeto VinylMixRepository. + +00:06:00.956 --> 00:06:06.856 align:middle +El repositorio de una entidad lo sabe +todo sobre cómo consultar sus datos. + +00:06:07.406 --> 00:06:15.076 align:middle +Y, sin que nosotros hagamos nada, ya tiene un montón +de métodos útiles para las consultas básicas, + +00:06:15.586 --> 00:06:20.336 align:middle +como findAll(), findOneBy() y varios más. + +00:06:21.096 --> 00:06:27.416 align:middle +Dentro de un rato, aprenderemos a añadir nuevos métodos +al repositorio para realizar consultas personalizadas. + +00:06:28.336 --> 00:06:33.346 align:middle +De todos modos, VinylMixRepository es en +realidad un servicio del cont enedor... + +00:06:33.816 --> 00:06:38.486 align:middle +así que podemos obtenerlo más +fácilmente autoconectándolo directamente. + +00:06:39.266 --> 00:06:43.006 align:middle +Añade un argumento +VinylMixRepository $mixRepository... + +00:06:43.586 --> 00:06:46.596 align:middle +y entonces no necesitaremos +esta línea en absoluto. + +00:06:47.506 --> 00:06:48.726 align:middle +Esto es más sencillo... + +00:06:50.126 --> 00:06:52.156 align:middle +¡y sigue funcionando! La conclusión + +00:06:53.006 --> 00:07:01.106 align:middle +es ésta: si quieres consultar una tabla, +lo harás a través del repositorio + +00:07:01.106 --> 00:07:03.896 align:middle +de la entidad cuyos datos necesitas. + +00:07:04.766 --> 00:07:10.446 align:middle +Siguiente: El hecho de que hayamos cambiado nuestro código +para cargarlo desde la base de datos y no hayamos tenido + +00:07:10.446 --> 00:07:14.856 align:middle +que actualizar nuestra plantilla Twig +en absoluto fue algo impresionante + +00:07:15.276 --> 00:07:18.086 align:middle +Y por cortesía de un poco +de magia de Twig. Vamos a + +00:07:18.786 --> 00:07:22.986 align:middle +hablar más de esa magia y a +crear una propiedad virtual + +00:07:23.216 --> 00:07:24.896 align:middle +que podemos imprimir en la plantilla diff --git a/sfcasts/ep3-doctrine/es/query-builder.md b/sfcasts/ep3-doctrine/es/query-builder.md new file mode 100644 index 0000000..02c9e26 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/query-builder.md @@ -0,0 +1,109 @@ +# El generador de consultas + +La página `/browse` funciona... ¿pero qué pasa si hacemos clic en uno de estos géneros? Bueno... eso funciona más o menos. Muestra el nombre del género... pero obtenemos una lista de todas las mezclas. Lo que realmente queremos es filtrarlas para que sólo se muestren las mezclas de ese género concreto. + +Ahora mismo, todas las mezclas de la base de datos están en el género "Pop". Vuelve a`MixController` y encuentra el método falso que crea nuevas mezclas para que podamos hacer algunos datos ficticios más interesantes. Añade una variable `$genres` con "Pop" y "Rock" incluidos... Luego selecciona una al azar con `$genres[array_rand($genres)]`. + +[[[ code('54c99ad8bd') ]]] + +¡Genial! Ahora ve a `/mix/new` y actualiza unas cuantas veces... hasta que tengamos unas 15 mezclas. Volvemos a `/browse`... ¡yup! Tenemos una mezcla de géneros "Rock" y "Pop"... sólo que aún no se filtran. + +Así que nuestra misión está clara: personalizar la consulta de la base de datos para que sólo devuelva los resultados de un género concreto. Bien, en realidad podemos hacerlo de forma súper sencilla en `VinylController`a través del método `findBy()`. El género está en la URL como el comodín `$slug`. + +Así que podríamos añadir una sentencia "if" en la que, si hay un género, devolvamos todos los resultados en los que `genre` coincida con `$slug`. Pero esta es una gran oportunidad para aprender a crear una consulta personalizada. Así que vamos a deshacerlo. + +## Método del repositorio personalizado + +La mejor manera de hacer una consulta personalizada, es crear un nuevo método en el repositorio para la entidad de la que quieras obtener datos. En este caso, eso significa`VinylMixRepository`. Esto contiene algunos métodos de ejemplo. Descomenta el primero... y empieza de forma sencilla + +[[[ code('e4d05ab545') ]]] + +Llámalo `findAllOrderedByVotes()`. No nos preocuparemos todavía del género: Sólo quiero hacer una consulta que devuelva todas las mezclas ordenadas por votos. Quitando el argumento, esto devolverá un array y el PHPdoc anterior ayuda a mi editor a saber que será un array de objetos `VinylMix` + +[[[ code('fb27ae3d1b') ]]] + +## DQL y el QueryBuilder + +Hay varias formas de ejecutar una consulta personalizada en Doctrine. Doctrine, por supuesto, acaba realizando consultas SQL. Pero Doctrine trabaja con MySQL, Postgres y otros motores de bases de datos... y el SQL necesario para cada uno de ellos es ligeramente diferente. + +Para manejar esto, internamente, Doctrine tiene su propio lenguaje de consulta llamado Doctrine Query Language o "DQL", Tiene un aspecto similar a + +> SELECT v FROM App\Entity\VinylMix v WHERE v.genre = 'pop'; + +Puedes escribir estas cadenas a mano, pero yo aprovecho el "QueryBuilder" de Doctrine: un bonito objeto que ayuda... ya sabes... ¡a construir esa consulta! + +## Crear el QueryBuilder + +Para utilizarlo, empieza con `$this->createQueryBuilder()` y pasa un alias que se utilizará para identificar esta clase dentro de la consulta. Hazlo corto, pero único entre tus entidades - algo como `mix`. + +[[[ code('d129b30819') ]]] + +Como estamos llamando a esto desde dentro de `VinylMixRepository`, el QueryBuilder ya sabe que hay que consultar desde la entidad `VinylMix`... y utilizará `mix` como alias. Si ejecutáramos este query builder ahora mismo, sería básicamente: + +> SELECT * FROM vinyl_mix AS mix + +El constructor de consultas está cargado de métodos para controlar la consulta. Por ejemplo, llama a `->orderBy()` y pasa a `mix` -ya que es nuestro alias- `.votes` y luego`DESC`. + +[[[ code('1cb0df88e1') ]]] + +Ya está Ahora que nuestra consulta está construida, para ejecutarla llama a `->getQuery()` (que la convierte en un objeto `Query` ) y luego a `->getResult()`. + +[[[ code('ce9df562c0') ]]] + +Bueno, en realidad, hay varios métodos a los que puedes llamar para obtener los resultados. Los dos principales son `getResult()` -que devuelve una matriz de los objetos coincidentes- o `getOneOrNullResult()`, que es el que utilizarías si estuvieras consultando un `VinylMix` específico o nulo. Como queremos devolver una matriz de mezclas coincidentes, utiliza `getResult()`. + +Ahora podemos utilizar este método. En `VinylController` (déjame cerrar `MixController`...), en lugar de `findBy()`, llama a `findAllOrderedByVotes()`. + +[[[ code('b6870c6a31') ]]] + +Me encanta lo claro que es este método: hace que sea súper obvio lo que estamos consultando exactamente. Y cuando lo probamos... ¡sigue funcionando! Todavía no está filtrando, pero el orden es correcto. + +## Añadir la sentencia WHERE + +Bien, volvamos a nuestro nuevo método. Añade un argumento opcional `string $genre = null`. Si se pasa un género, tenemos que añadir una sentencia "where". Para hacer espacio para ello, divide esto en varias líneas... y sustituye `return` por `$queryBuilder =`. A continuación, `return $queryBuilder` por `->getQuery()`, y `->getResult()`. + +[[[ code('4d59046ba3') ]]] + +Ahora podemos decir `if ($genre)`, y añadir la declaración "where". ¿Cómo? Apuesto a que puedes adivinar: `$queryBuilder->andWhere()`. + +Pero una advertencia. También hay un método `where()`... pero yo nunca lo uso. Cuando llames a `where()`, borrará cualquier sentencia "where" existente que pueda tener el constructor de consultas... por lo que podrías eliminar accidentalmente algo que hayas añadido antes. Por tanto, utiliza siempre `andWhere()`. Doctrine es lo suficientemente inteligente como para darse cuenta de que, al ser el primer WHERE, no necesita añadir el`AND`. + +Dentro de `andWhere()`, pasa `mix.genre =`... pero no pongas el género dinámico justo en la cadena. Eso es un gran no-no: nunca lo hagas. Eso te abre a los ataques de inyección SQL. En su lugar, siempre que necesites poner un valor dinámico en una consulta, utiliza una "sentencia preparada"... que es una forma elegante de decir que pones un marcador de posición aquí, como `:genre`. El nombre de esto puede ser cualquier cosa... como "dinosaurio" si quieres. Pero lo llames como lo llames, luego rellenarás el marcador de posición diciendo `->setParameter()` con el nombre del parámetro -así que`genre` - y luego el valor: `$genre`. + +[[[ code('d847ca105b') ]]] + +¡Qué bonito! De nuevo en `VinylController`, pasa `$slug` como género. + +¡Vamos a probar esto! Vuelve a la página de navegación primero. ¡Genial! Obtenemos todos los resultados. Ahora haz clic en "Rock" y... ¡bien! Menos resultados y todos los géneros muestran "Rock"! Si filtro por "Pop"... ¡lo tengo! Incluso podemos ver la consulta para esto... aquí está. Tiene la sentencia "where" para el género igual a "Pop". ¡Guau! + +## Reutilización de la lógica del generador de consultas + +A medida que tu proyecto se hace más y más grande, vas a crear más y más métodos en tu repositorio para las consultas personalizadas. Y puede que empieces a repetir la misma lógica de consulta una y otra vez. Por ejemplo, podríamos ordenar por los votos en un montón de métodos diferentes de esta clase. + +Para evitar la duplicación, podemos aislar esa lógica en un método privado. ¡Compruébalo! Añade `private function addOrderByVotesQueryBuilder()`. Esto aceptará un argumento`QueryBuilder` (queremos el de `Doctrine\ORM`), pero hagámoslo opcional. Y también devolveremos un `QueryBuilder`. + +[[[ code('0b62ed3f29') ]]] + +El trabajo de este método es añadir esta línea `->orderBy()`. Y por comodidad, si no pasamos un `$queryBuilder`, crearemos uno nuevo. + +Para permitirlo, empieza con`$queryBuilder = $queryBuilder ?? $this->createQueryBuilder('mix')`. Vuelvo a utilizar a propósito `mix` para el alias. Para simplificar la vida, elige un alias para una entidad y utilízalo sistemáticamente en todas partes. + +[[[ code('64c4b20a01') ]]] + +En cualquier caso, esta línea puede parecer extraña, pero básicamente dice + +> Si existe un QueryBuilder, utilízalo. Si no, crea uno nuevo. + +Debajo de `return $queryBuilder`... ve a robar la lógica de `->orderBy()` de aquí arriba y... pega. ¡Impresionante! + +[[[ code('642635ef7b') ]]] + +PhpStorm está un poco enfadado conmigo... pero eso es sólo porque está teniendo una mañana dura y necesita reiniciarse: nuestro código está, esperemos, bien. + +Vuelve al método original, simplifica a`$queryBuilder = $this->addOrderByVotesQueryBuilder()` y no le pases nada. + +[[[ code('7e289b4487') ]]] + +¿No es bonito? Cuando actualizamos... ¡no está roto! ¡Toma ese PhpStorm! + +A continuación, vamos a añadir una página de "espectáculo de mezclas" en la que podamos ver una única mezcla de vinilos. Por primera vez, consultaremos un único objeto de la base de datos y nos ocuparemos de lo que ocurre si no se encuentra ninguna mezcla que coincida. diff --git a/sfcasts/ep3-doctrine/es/query-builder.vtt b/sfcasts/ep3-doctrine/es/query-builder.vtt new file mode 100644 index 0000000..2f5d6d0 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/query-builder.vtt @@ -0,0 +1,512 @@ +WEBVTT + +00:00:01.016 --> 00:00:03.356 align:middle +La página /browse funciona... + +00:00:03.716 --> 00:00:06.296 align:middle +¿pero qué pasa si hacemos +clic en uno de estos géneros? + +00:00:06.816 --> 00:00:08.626 align:middle +Bueno... eso funciona más o menos. + +00:00:08.916 --> 00:00:10.746 align:middle +Muestra el nombre del género... + +00:00:11.176 --> 00:00:13.536 align:middle +pero obtenemos una lista de todas las mezclas. + +00:00:14.046 --> 00:00:19.626 align:middle +Lo que realmente queremos es filtrarlas para que +sólo se muestren las mezclas de ese género concreto. + +00:00:20.286 --> 00:00:23.956 align:middle +Ahora mismo, todas las mezclas de la +base de datos están en el género "Pop". + +00:00:24.576 --> 00:00:29.556 align:middle +Vuelve a MixController y encuentra el método +falso que crea nuevas mezclas para que + +00:00:29.976 --> 00:00:32.966 align:middle +podamos hacer algunos datos +ficticios más interesantes. + +00:00:33.776 --> 00:00:37.456 align:middle +Añade una variable $genres +con "Pop" y "Rock" incluidos... + +00:00:38.046 --> 00:00:43.436 align:middle +Luego selecciona una al azar con +$genres[array_rand($genres)]. + +00:00:44.116 --> 00:00:49.216 align:middle +¡Genial! Ahora ve a /mix/new y +actualiza unas cuantas veces... + +00:00:49.606 --> 00:00:51.976 align:middle +hasta que tengamos unas 15 mezclas. + +00:00:52.816 --> 00:00:54.146 align:middle +Vuelve a /browse... + +00:00:55.176 --> 00:00:58.816 align:middle +¡sí! Tenemos una mezcla de +géneros "Rock" y "Pop"... + +00:00:59.376 --> 00:01:01.156 align:middle +sólo que aún no se filtran. + +00:01:01.976 --> 00:01:06.136 align:middle +Así que nuestra misión está clara: +personalizar la consulta de la base de datos + +00:01:06.136 --> 00:01:10.146 align:middle +para que sólo devuelva los +resultados de un género concreto. + +00:01:11.046 --> 00:01:18.066 align:middle +Bien, en realidad podemos hacerlo de forma súper sencilla +en VinylController a través del método findBy(). + +00:01:18.806 --> 00:01:22.256 align:middle +El género está en la URL +como el comodín $slug. + +00:01:22.996 --> 00:01:28.116 align:middle +Así que podríamos añadir una sentencia +"if" en la que, si hay un género, + +00:01:28.516 --> 00:01:32.426 align:middle +devolvamos todos los resultados en +los que genre coincida con $slug. + +00:01:33.146 --> 00:01:38.456 align:middle +Pero esta es una gran oportunidad para +aprender a crear una consulta personalizada. + +00:01:38.956 --> 00:01:40.516 align:middle +Así que vamos a deshacerlo. La + +00:01:41.306 --> 00:01:47.186 align:middle +mejor manera de hacer una consulta personalizada, +es crear un nuevo método en el repositorio + +00:01:47.346 --> 00:01:50.066 align:middle +para la entidad de la que +quieras obtener datos. + +00:01:50.796 --> 00:01:53.966 align:middle +En este caso, eso significa VinylMixRepository. + +00:01:54.676 --> 00:01:56.756 align:middle +Esto contiene algunos métodos de ejemplo. + +00:01:57.206 --> 00:01:58.446 align:middle +Descomenta el primero... + +00:01:58.936 --> 00:02:00.586 align:middle +y empieza por el más sencillo. + +00:02:01.176 --> 00:02:03.846 align:middle +Llámalo findAllOrderedByVotes(). + +00:02:04.656 --> 00:02:09.216 align:middle +No nos preocuparemos todavía del +género: Sólo quiero hacer una consulta + +00:02:09.216 --> 00:02:12.436 align:middle +que devuelva todas las +mezclas ordenadas por votos. + +00:02:13.346 --> 00:02:20.646 align:middle +Quitando el argumento, esto devolverá un array +y el PHPdoc de arriba ayuda a mi editor a saber + +00:02:20.736 --> 00:02:26.416 align:middle +que se tratará de un array de objetos +VinylMix Hay algunas formas diferentes + +00:02:26.416 --> 00:02:28.826 align:middle +de ejecutar una consulta +personalizada en Doctrine. + +00:02:29.606 --> 00:02:33.816 align:middle +Doctrine, por supuesto, acaba +realizando consultas SQL. + +00:02:34.516 --> 00:02:39.786 align:middle +Pero Doctrine trabaja con MySQL, Postgres +y otros motores de bases de datos... + +00:02:40.226 --> 00:02:44.696 align:middle +y el SQL necesario para cada uno +de ellos es ligeramente diferente. + +00:02:45.346 --> 00:02:51.886 align:middle +Para manejar esto, internamente, Doctrine tiene su propio +lenguaje de consulta llamado Doctrine Query Language + +00:02:52.056 --> 00:03:03.106 align:middle +o "DQL", Tiene un aspecto similar a SELECT +v FROM App\Entity\VinylMix v WHERE v.genre = 'pop'; + +00:03:03.936 --> 00:03:08.436 align:middle +Puedes escribir estas cadenas a mano, pero +yo aprovecho el "QueryBuilder" de Doctrine: + +00:03:08.436 --> 00:03:11.636 align:middle +un bonito objeto que ayuda... + +00:03:11.916 --> 00:03:12.266 align:middle +ya sabes... + +00:03:12.576 --> 00:03:13.896 align:middle +¡a construir esa consulta! + +00:03:14.836 --> 00:03:20.246 align:middle +Para utilizarlo, empieza con +$this->createQueryBuilder() y pasa un alias + +00:03:20.246 --> 00:03:25.026 align:middle +que se utilizará para identificar +esta clase dentro de la consulta. + +00:03:25.946 --> 00:03:31.186 align:middle +Hazlo corto, pero único entre +tus entidades - algo como mix. + +00:03:32.146 --> 00:03:35.926 align:middle +Como estamos llamando a esto desde +dentro de VinylMixRepository, + +00:03:36.346 --> 00:03:40.886 align:middle +el QueryBuilder ya sabe que debe +consultar la entidad VinylMix... + +00:03:41.316 --> 00:03:43.696 align:middle +y utilizará mix como alias. + +00:03:44.376 --> 00:03:50.886 align:middle +Si ejecutáramos este constructor de consultas ahora +mismo, sería básicamente SELECT * FROM vinyl_mix AS mix + +00:03:51.026 --> 00:03:56.086 align:middle +El constructor de consultas está +cargado de métodos para controlar la consulta. + +00:03:56.696 --> 00:04:05.136 align:middle +Por ejemplo, llama a ->orderBy() y pasa a mix +-ya que es nuestro alias- .votes y luego DESC. + +00:04:06.026 --> 00:04:13.306 align:middle +Ya está Ahora que nuestra consulta está construida, +para ejecutarla llama a ->getQuery() (que la convierte + +00:04:13.306 --> 00:04:17.656 align:middle +en un objeto Query ) y +luego a ->getResult(). + +00:04:18.386 --> 00:04:23.066 align:middle +Bueno, en realidad, hay varios métodos a los +que puedes llamar para obtener los resultados. + +00:04:23.606 --> 00:04:29.596 align:middle +Los dos principales son getResult() -que +devuelve un array de los objetos coincidentes- + +00:04:30.086 --> 00:04:36.216 align:middle +o getOneOrNullResult(), que es el que +utilizarías si estuvieras consultando + +00:04:36.286 --> 00:04:39.696 align:middle +un VinylMix específico o nulo. + +00:04:40.586 --> 00:04:45.486 align:middle +Como queremos devolver una matriz de +mezclas coincidentes, utiliza getResult(). + +00:04:46.076 --> 00:04:47.886 align:middle +Ahora podemos utilizar este método. En + +00:04:48.686 --> 00:04:52.016 align:middle +VinylController (déjame +cerrar MixController...), + +00:04:52.876 --> 00:04:57.476 align:middle +en lugar de findBy(), llama +a findAllOrderedByVotes(). + +00:04:58.176 --> 00:05:05.006 align:middle +Me encanta lo claro que es este método: hace que sea +súper obvio lo que estamos consultando exactamente. + +00:05:05.916 --> 00:05:07.216 align:middle +Y cuando lo probamos... + +00:05:09.236 --> 00:05:10.366 align:middle +¡sigue funcionando! + +00:05:10.816 --> 00:05:14.086 align:middle +Todavía no está filtrando, +pero el orden es correcto. + +00:05:14.906 --> 00:05:16.586 align:middle +Bien, volvamos a nuestro nuevo método. + +00:05:17.356 --> 00:05:20.756 align:middle +Añade un argumento opcional +string $genre = null. + +00:05:21.506 --> 00:05:25.256 align:middle +Si se pasa un género, tenemos que +añadir una declaración "where". + +00:05:25.846 --> 00:05:28.686 align:middle +Para dejar espacio para ello, +divide esto en varias líneas... + +00:05:29.316 --> 00:05:37.176 align:middle +y sustituye return por $queryBuilder =. +A continuación , return $queryBuilder + +00:05:37.176 --> 00:05:38.826 align:middle +con ->getQuery(), y ->getResult(). + +00:05:39.646 --> 00:05:44.146 align:middle +Ahora podemos decir if ($genre), +y añadir la declaración "where". + +00:05:44.746 --> 00:05:49.456 align:middle +¿Cómo? Apuesto a que puedes +adivinar: $queryBuilder->andWhere(). + +00:05:50.046 --> 00:05:51.486 align:middle +Pero una advertencia. + +00:05:51.876 --> 00:05:54.556 align:middle +También existe el método where()... + +00:05:55.146 --> 00:05:56.866 align:middle +pero yo nunca lo uso. + +00:05:57.646 --> 00:06:02.306 align:middle +Cuando llames a where(), borrará +cualquier sentencia "where" existente + +00:06:02.456 --> 00:06:04.476 align:middle +que pueda tener el constructor de consultas... + +00:06:05.016 --> 00:06:09.316 align:middle +por lo que podrías eliminar accidentalmente +algo que hayas añadido antes. + +00:06:09.866 --> 00:06:12.296 align:middle +Por tanto, utiliza siempre andWhere(). + +00:06:13.046 --> 00:06:17.576 align:middle +Doctrine es lo suficientemente inteligente como +para darse cuenta de que, al ser el primer WHERE, + +00:06:17.846 --> 00:06:20.796 align:middle +no necesita añadir el AND. + +00:06:21.546 --> 00:06:25.076 align:middle +Dentro de andWhere(), pasa mix.genre =. + +00:06:25.446 --> 00:06:29.516 align:middle +pero no pongas el género +dinámico justo en la cadena. + +00:06:30.076 --> 00:06:33.126 align:middle +Eso es un gran no-no: nunca lo hagas. + +00:06:33.646 --> 00:06:37.056 align:middle +Eso te abre a los ataques de inyección SQL. + +00:06:37.706 --> 00:06:41.046 align:middle +En su lugar, siempre que +necesites poner un valor dinámico + +00:06:41.046 --> 00:06:44.516 align:middle +en una consulta, utiliza +una "sentencia preparada"... + +00:06:44.846 --> 00:06:50.986 align:middle +que es una forma elegante de decir que pones +un marcador de posición aquí, como :genre. + +00:06:51.816 --> 00:06:53.886 align:middle +El nombre de éste puede ser cualquier cosa... + +00:06:53.996 --> 00:06:55.506 align:middle +como "dinosaurio" si quieres. + +00:06:56.056 --> 00:07:01.856 align:middle +Pero lo llames como lo llames, luego rellenarás el +marcador de posición diciendo ->setParameter() + +00:07:02.246 --> 00:07:06.916 align:middle +con el nombre del parámetro -así +que genre - y luego el valor: $genre. + +00:07:07.876 --> 00:07:08.476 align:middle +¡Qué bonito! + +00:07:09.066 --> 00:07:13.396 align:middle +De nuevo en VinylController, +pasa $slug como género. + +00:07:14.246 --> 00:07:15.086 align:middle +¡Vamos a probar esto! + +00:07:15.646 --> 00:07:17.756 align:middle +Vuelve a la página de navegación primero. + +00:07:18.916 --> 00:07:21.266 align:middle +¡Genial! Obtenemos todos los resultados. + +00:07:22.076 --> 00:07:24.086 align:middle +Ahora haz clic en "Rock" y... + +00:07:24.406 --> 00:07:28.956 align:middle +¡bien! ¡Menos resultados y todos +los géneros muestran "Rock"! + +00:07:29.776 --> 00:07:31.366 align:middle +Si filtro por "Pop"... + +00:07:31.976 --> 00:07:35.916 align:middle +¡lo tengo! Incluso podemos +ver la consulta para esto... + +00:07:37.006 --> 00:07:37.636 align:middle +aquí está. + +00:07:38.106 --> 00:07:42.066 align:middle +Tiene la sentencia "where" +para el género igual a "Pop". + +00:07:42.576 --> 00:07:48.376 align:middle +¡Vaya! A medida que tu proyecto se haga más +y más grande, vas a crear más y más métodos + +00:07:48.376 --> 00:07:50.556 align:middle +en tu repositorio para las +consultas personalizadas. + +00:07:51.186 --> 00:07:55.426 align:middle +Y puede que empieces a repetir la misma +lógica de consulta una y otra vez. + +00:07:56.176 --> 00:08:01.846 align:middle +Por ejemplo, podríamos ordenar por los votos en +un montón de métodos diferentes de esta clase. + +00:08:02.746 --> 00:08:07.256 align:middle +Para evitar la duplicación, podemos +aislar esa lógica en un método privado. + +00:08:08.076 --> 00:08:08.466 align:middle +¡Compruébalo! + +00:08:08.846 --> 00:08:13.386 align:middle +Añade private function +addOrderByVotesQueryBuilder(). + +00:08:14.246 --> 00:08:18.176 align:middle +Esto aceptará un argumento +QueryBuilder (queremos el + +00:08:18.176 --> 00:08:23.356 align:middle +de Doctrine\ORM), pero hagámoslo opcional. + +00:08:24.136 --> 00:08:27.006 align:middle +Y también devolveremos un QueryBuilder. + +00:08:28.606 --> 00:08:32.456 align:middle +El trabajo de este método es +añadir esta línea ->orderBy(). + +00:08:33.116 --> 00:08:38.526 align:middle +Y por comodidad, si no pasamos un +$queryBuilder, crearemos uno nuevo. + +00:08:39.306 --> 00:08:44.186 align:middle +Para permitirlo, empieza con +$queryBuilder = $queryBuilder ?? + +00:08:44.186 --> 00:08:47.836 align:middle +$this->createQueryBuilder('mix'). + +00:08:48.466 --> 00:08:52.616 align:middle +Vuelvo a utilizar a +propósito mix para el alias. + +00:08:53.446 --> 00:08:59.776 align:middle +Para simplificar la vida, elige un alias para una +entidad y utilízalo sistemáticamente en todas partes. + +00:09:00.716 --> 00:09:05.096 align:middle +En cualquier caso, esta línea puede +parecer extraña, pero básicamente dice + +00:09:05.386 --> 00:09:08.366 align:middle +Si existe un QueryBuilder, utilízalo. + +00:09:08.776 --> 00:09:10.426 align:middle +Si no, crea uno nuevo. + +00:09:11.306 --> 00:09:12.996 align:middle +Por debajo de return $queryBuilder... + +00:09:13.666 --> 00:09:17.626 align:middle +ve a robar la lógica de +->orderBy() de aquí arriba y... + +00:09:17.786 --> 00:09:19.826 align:middle +pega. ¡Impresionante! + +00:09:20.636 --> 00:09:22.956 align:middle +PhpStorm está un poco enfadado conmigo... + +00:09:23.176 --> 00:09:26.866 align:middle +pero eso es sólo porque está teniendo +una mañana difícil y necesita reiniciar: + +00:09:27.296 --> 00:09:29.486 align:middle +nuestro código está, esperemos, bien. + +00:09:30.316 --> 00:09:32.776 align:middle +Vuelve al método original, simplifica + +00:09:32.776 --> 00:09:37.546 align:middle +a $queryBuilder = +$this->addOrderByVotesQueryBuilder() + +00:09:37.686 --> 00:09:39.686 align:middle +y no le pases nada. + +00:09:40.376 --> 00:09:41.286 align:middle +¿No es bonito? + +00:09:42.086 --> 00:09:43.556 align:middle +Cuando refresquemos... + +00:09:44.026 --> 00:09:45.456 align:middle +¡no está roto! + +00:09:45.786 --> 00:09:47.316 align:middle +¡Toma ese PhpStorm! + +00:09:48.256 --> 00:09:53.926 align:middle +A continuación, vamos a añadir una página de "show de +mezclas" en la que podamos ver una única mezcla de vinilos. + +00:09:54.516 --> 00:09:59.906 align:middle +Por primera vez, consultaremos un único +objeto de la base de datos y nos ocuparemos + +00:09:59.906 --> 00:10:03.266 align:middle +de lo que ocurre si no se encuentra +ninguna mezcla que coincida diff --git a/sfcasts/ep3-doctrine/es/request.md b/sfcasts/ep3-doctrine/es/request.md new file mode 100644 index 0000000..6433267 --- /dev/null +++ b/sfcasts/ep3-doctrine/es/request.md @@ -0,0 +1,91 @@ +# El objeto de petición + +Nuevo equipo objetivo: permitir que los usuarios voten a favor y en contra de una mezcla. Para conseguirlo, en la entidad `VinylMix`, cuando un usuario vota, necesitamos enviar una consulta UPDATE para cambiar la propiedad entera `$votes` en la base de datos. + +## Añadir un formulario sencillo + +Centrémonos primero en la interfaz de usuario. Abre `templates/mix/show.html.twig`. Para empezar, imprime `{{ mix.votesString }} votes` para que podamos verlo aquí. + +[[[ code('6754b906b7') ]]] + +Y... ¡perfecto! Para añadir la funcionalidad de upvote y downvote, podríamos utilizar algún JavaScript sofisticado. Pero vamos a hacerlo sencillo añadiendo un botón que publique un formulario. En realidad, esto será más elegante de lo que parece. En el primer tutorial, instalamos la biblioteca Turbo JavaScript. Así que aunque usaremos una etiqueta y un botón normales de`
`, Turbo lo enviará automáticamente vía AJAX para una experiencia fluida. + +Por cierto, Symfony tiene un componente de formulario y hablaremos de él en un futuro tutorial. Pero este formulario va a ser tan sencillo que realmente no lo necesitamos. Añade una bonita y aburrida etiqueta `` con `action` establecida en la función`path()`. + +El formulario se enviará a un nuevo controlador que... ¡todavía tenemos que crear! + +Dirígete a `MixController` y añade un nuevo `public function` llamado `vote()`. Dale el atributo `#[Route()]` con la URL `/mix/{id}/vote`. Y como tenemos que enlazar con esto, añade un nombre: `app_mix_vote`. + +[[[ code('83dd0e386e') ]]] + +El comodín de la ruta `{id}` contendrá el id del `VinylMix` específico que el usuario está votando. Para consultarlo, utiliza el truco que aprendimos antes: añade un argumento de tipo `VinylMix` y llámalo `$mix`. Ah, y aunque no es necesario, añadiré el tipo de retorno `Response`. Añadir esto es sólo una buena práctica. + +Dentro, para asegurarnos de que las cosas funcionan, `dd($mix)`. + +[[[ code('13482639eb') ]]] + +¡Genial! Copia el nombre de la ruta, vuelve a la plantilla - `show.html.twig` - y dentro de `path()`, pega. Y como esta ruta tiene un comodín `{id}`, pasa`id` a `mix.id`. También dale al formulario `method="POST"`... porque siempre que el envío de un formulario cambie los datos en tu servidor, debe enviarse con `POST`. + +[[[ code('ea16ec1820') ]]] + +Incluso podemos imponer este requisito en nuestra ruta añadiendo`methods: ['POST']`. Eso es opcional, pero ahora, si alguien, por alguna razón, va directamente a esta URL, que es una petición GET, no coincidirá con la ruta. ¡Muy útil! + +[[[ code('6664c70b9d') ]]] + +Vuelve al formulario. Este formulario... será algo extraño. En lugar de tener campos en los que el usuario pueda escribir, sólo necesitamos un botón. Añade ` + + + + + + {% for message in app.flashes('success') %} +
+ {{ message }} +
+ {% endfor %} + + + {% block body %}{% endblock %} + + +
+ +
+ + + + diff --git a/templates/mix/_recordSvg.html.twig b/templates/mix/_recordSvg.html.twig new file mode 100644 index 0000000..44ec44a --- /dev/null +++ b/templates/mix/_recordSvg.html.twig @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/mix/show.html.twig b/templates/mix/show.html.twig new file mode 100644 index 0000000..19191ea --- /dev/null +++ b/templates/mix/show.html.twig @@ -0,0 +1,34 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ mix.title }} Mix{% endblock %} + +{% block body %} +
+

{{ mix.title }}

+
+
+ {{ include('mix/_recordSvg.html.twig') }} +
+
+

{{ mix.trackCount }} songs (genre: {{ mix.genre }})

+

{{ mix.description }}

+ + {{ mix.votesString }} votes + + + + +
+
+
+{% endblock %} diff --git a/templates/vinyl/browse.html.twig b/templates/vinyl/browse.html.twig new file mode 100644 index 0000000..1d4fb0f --- /dev/null +++ b/templates/vinyl/browse.html.twig @@ -0,0 +1,55 @@ +{% extends 'base.html.twig' %} + +{% block body %} +
+

Browse {{ genre ? genre : 'All Genres' }}

+ +

Filter by Genre

+ + +
+

Mixes

+ +
+ {% for mix in pager %} + + {% endfor %} + + {% if pager.hasNextPage %} + + {% endif %} +
+
+
+
+{% endblock %} diff --git a/templates/vinyl/homepage.html.twig b/templates/vinyl/homepage.html.twig new file mode 100644 index 0000000..45d0d8b --- /dev/null +++ b/templates/vinyl/homepage.html.twig @@ -0,0 +1,36 @@ +{% extends 'base.html.twig' %} + +{% block title %}Create a new Record | {{ parent() }}{% endblock %} + +{% block body %} +
+

{{ title }}

+
+
+ {{ include('mix/_recordSvg.html.twig') }} +
+
+

10 songs (30 minutes of 60 still available)

+ {% for track in tracks %} +
+
+ + + + {{ track.song }} - {{ track.artist }} + + + + + + +
+
+ {% endfor %} + +
+
+
+{% endblock %} diff --git a/translations/.gitignore b/translations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..056b04a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,75 @@ +const Encore = require('@symfony/webpack-encore'); + +// Manually configure the runtime environment if not already configured yet by the "encore" command. +// It's useful when you use tools that rely on webpack.config.js file. +if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); +} + +Encore + // directory where compiled assets will be stored + .setOutputPath('public/build/') + // public path used by the web server to access the output path + .setPublicPath('/build') + // only needed for CDN's or sub-directory deploy + //.setManifestKeyPrefix('build/') + + /* + * ENTRY CONFIG + * + * Each entry will result in one JavaScript file (e.g. app.js) + * and one CSS file (e.g. app.css) if your JavaScript imports CSS. + */ + .addEntry('app', './assets/app.js') + + // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) + .enableStimulusBridge('./assets/controllers.json') + + // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. + .splitEntryChunks() + + // will require an extra script tag for runtime.js + // but, you probably want this, unless you're building a single-page app + .enableSingleRuntimeChunk() + + /* + * FEATURE CONFIG + * + * Enable & configure other features below. For a full + * list of features, see: + * https://symfony.com/doc/current/frontend.html#adding-more-features + */ + .cleanupOutputBeforeBuild() + .enableBuildNotifications() + .enableSourceMaps(!Encore.isProduction()) + // enables hashed filenames (e.g. app.abc123.css) + .enableVersioning(Encore.isProduction()) + + .configureBabel((config) => { + config.plugins.push('@babel/plugin-proposal-class-properties'); + }) + + // enables @babel/preset-env polyfills + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = 3; + }) + + // enables Sass/SCSS support + //.enableSassLoader() + + // uncomment if you use TypeScript + //.enableTypeScriptLoader() + + // uncomment if you use React + //.enableReactPreset() + + // uncomment to get integrity="..." attributes on your script & link tags + // requires WebpackEncoreBundle 1.4 or higher + //.enableIntegrityHashes(Encore.isProduction()) + + // uncomment if you're having problems with a jQuery plugin + //.autoProvidejQuery() +; + +module.exports = Encore.getWebpackConfig(); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..b5db895 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4303 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" + integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== + +"@babel/core@^7.17.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.6.tgz#54a107a3c298aee3fe5e1947a6464b9b6faca03d" + integrity sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.6" + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helpers" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.6", "@babel/generator@^7.18.7": + version "7.18.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd" + integrity sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A== + dependencies: + "@babel/types" "^7.18.7" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.6.tgz#f14d640ed1ee9246fb33b8255f08353acfe70e6a" + integrity sha512-KT10c1oWEpmrIRYnthbzHgoOf6B+Xd6a5yhdbNtdhtG7aO1or5HViuf1TQR36xY/QprXA5nvxO6nAjhJ4y38jw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz#18d35bfb9f83b1293c22c55b3d576c1315b6ed96" + integrity sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg== + dependencies: + "@babel/compat-data" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz#6f15f8459f3b523b39e00a99982e2c040871ed72" + integrity sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-member-expression-to-functions" "^7.18.6" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + +"@babel/helper-create-regexp-features-plugin@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz#3e35f4e04acbbf25f1b3534a657610a000543d3c" + integrity sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.1.0" + +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" + integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q== + +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-function-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83" + integrity sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-member-expression-to-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.6.tgz#44802d7d602c285e1692db0bad9396d007be2afc" + integrity sha512-CeHxqwwipekotzPDUuJOfIMtcIHBuc7WAzLmTYWctVigqS5RktNMQ5bEwQSuGewzYnCtTWa3BARXeiLxDTv+Ng== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.8.tgz#4f8408afead0188cfa48672f9d0e5787b61778c8" + integrity sha512-che3jvZwIcZxrwh63VfnFTUzcAM9v/lznYkkRxIBGMPt1SudOKHAEec0SIRCfiuIzTcF7VGj/CaTT6gY4eWxvA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.8" + "@babel/types" "^7.18.8" + +"@babel/helper-optimise-call-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" + integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d" + integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg== + +"@babel/helper-remap-async-to-generator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.6.tgz#fa1f81acd19daee9d73de297c0308783cd3cfc23" + integrity sha512-z5wbmV55TveUPZlCLZvxWHtrjuJd+8inFhk7DG0WW87/oJuGDcjDiu7HIvGcpf5464L6xKCg3vNkmlVVz9hwyQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-wrap-function" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-replace-supers@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.6.tgz#efedf51cfccea7b7b8c0f00002ab317e7abfe420" + integrity sha512-fTf7zoXnUGl9gF25fXCWE26t7Tvtyn6H4hkLSYhATwJvw2uYxd3aoXplMSe0g9XbwK7bmxNes7+FGO0rB/xC0g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-member-expression-to-functions" "^7.18.6" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.6.tgz#7dff00a5320ca4cf63270e5a0eca4b268b7380d9" + integrity sha512-4KoLhwGS9vGethZpAhYnMejWkX64wsnHPDwvOsKWU6Fg4+AlK2Jz3TyjQLMEPvz+1zemi/WBdkYxCD0bAfIkiw== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helper-wrap-function@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.6.tgz#ec44ea4ad9d8988b90c3e465ba2382f4de81a073" + integrity sha512-I5/LZfozwMNbwr/b1vhhuYD+J/mU+gfGAj5td7l5Rv9WYmH6i3Om69WGKNmlIpsVW/mF6O5bvTKbvDQZVgjqOw== + dependencies: + "@babel/helper-function-name" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helpers@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.6.tgz#4c966140eaa1fcaa3d5a8c09d7db61077d4debfd" + integrity sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.18.6", "@babel/parser@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf" + integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.6.tgz#b4e4dbc2cd1acd0133479918f7c6412961c9adb8" + integrity sha512-Udgu8ZRgrBrttVz6A0EVL0SJ1z+RLbIeqsu632SA1hf0awEppD6TvdznoH+orIF8wtFFAV/Enmw9Y+9oV8TQcw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.6" + +"@babel/plugin-proposal-async-generator-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz#aedac81e6fc12bb643374656dd5f2605bf743d17" + integrity sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w== + dependencies: + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-remap-async-to-generator" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" + integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.6.tgz#1016f0aa5ab383bbf8b3a85a2dcaedf6c8ee7491" + integrity sha512-zr/QcUlUo7GPo6+X1wC98NJADqmy5QTFWWhqeQWiki4XHafJtLl/YMGkmRB2szDD2IYJCCdBTd4ElwhId9T7Xw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.6.tgz#3b9cac6f1ffc2aa459d111df80c12020dfc6b665" + integrity sha512-zMo66azZth/0tVd7gmkxOkOjs2rpHyhpcFo565PUP37hSp6hSd9uUKIfTDFMz58BwqgQKhJ9YxtM5XddjXVn+Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.6.tgz#ec93bba06bfb3e15ebd7da73e953d84b094d5daf" + integrity sha512-9yuM6wr4rIsKa1wlUAbZEazkCrgw2sMPEXCr4Rnwetu7cEW1NydkCWytLuYletbf8vFxdJxFhwEZqMpOx2eZyw== + dependencies: + "@babel/compat-data" "^7.18.6" + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.18.6" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.6.tgz#46d4f2ffc20e87fad1d98bc4fa5d466366f6aa0b" + integrity sha512-PatI6elL5eMzoypFAiYDpYQyMtXTn+iMhuxxQt5mAXD4fEmKorpSI3PHd+i3JXBJN3xyA6MvJv7at23HffFHwA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz#a64137b232f0aca3733a67eb1a144c192389c503" + integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz#cd6190500a4fa2fe31990a963ffab4b63e4505e4" + integrity sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" + integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" + integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-remap-async-to-generator" "^7.18.6" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.6.tgz#b5f78318914615397d86a731ef2cc668796a726c" + integrity sha512-pRqwb91C42vs1ahSAWJkxOxU1RHWDn16XAa6ggQ72wjLlWyYeAcLvTtE0aM8ph3KNydy9CQF2nLYcjq1WysgxQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-classes@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.8.tgz#7e85777e622e979c85c701a095280360b818ce49" + integrity sha512-RySDoXdF6hgHSHuAW4aLGyVQdmvEX/iJtjVre52k0pxRq4hzqze+rAVP++NmNv596brBpYmaiKgTZby7ziBnVg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.6.tgz#5d15eb90e22e69604f3348344c91165c5395d032" + integrity sha512-9repI4BhNrR0KenoR9vm3/cIc1tSBIo+u1WVjKCAynahj25O8zfbiE6JtAtHPGQSs4yZ+bA8mRasRP+qc+2R5A== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-destructuring@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.6.tgz#a98b0e42c7ffbf5eefcbcf33280430f230895c6f" + integrity sha512-tgy3u6lRp17ilY8r1kP4i2+HDUwxlVqq3RTc943eAWSzGgpU1qhiKpqZ5CMyHReIYPHdo3Kg8v8edKtDqSVEyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.6.tgz#e6c94e8cd3c9dd8a88144f7b78ae22975a7ff473" + integrity sha512-NJU26U/208+sxYszf82nmGYqVF9QN8py2HFTblPT9hbawi8+1C5a9JubODLTGFuT0qlkqVinmkwOD13s0sZktg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-for-of@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-function-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.6.tgz#6a7e4ae2893d336fd1b8f64c9f92276391d0f1b4" + integrity sha512-kJha/Gbs5RjzIu0CxZwf5e3aTTSlhZnHMT8zPWnJMjNpLOUgqevg+PN5oMH68nMCXnfiMo4Bhgxqj59KHTlAnA== + dependencies: + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.6.tgz#9d6af353b5209df72960baf4492722d56f39a205" + integrity sha512-x3HEw0cJZVDoENXOp20HlypIHfl0zMIhMVZEBVTfmqbObIpsMxMbmU5nOEO8R7LYT+z5RORKPlTI5Hj4OsO9/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz#8c91f8c5115d2202f277549848874027d7172d21" + integrity sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz#afd243afba166cca69892e24a8fd8c9f2ca87883" + integrity sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.6.tgz#026511b7657d63bf5d4cf2fd4aeb963139914a54" + integrity sha512-UbPYpXxLjTw6w6yXX2BYNxF3p6QY225wcTkfQCy3OMnSlS/C3xGtwUjEzGkldb/sy6PWLiCQ3NbYfjWUTI3t4g== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz#c89bfbc7cc6805d692f3a49bc5fc1b630007246d" + integrity sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz#ee9f1a0ce6d78af58d0956a9378ea3427cccb48a" + integrity sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" + integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + regenerator-transform "^0.15.0" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.6.tgz#82b080241965f1689f0a60ecc6f1f6575dbdb9d6" + integrity sha512-ayT53rT/ENF8WWexIRg9AiV9h0aIteyWn5ptfZTZQrjk/+f3WdrJGCY4c9wcgl2+MKkKPhzbYp97FTsquZpDCw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.6.tgz#b763f4dc9d11a7cce58cf9a490d82e80547db9c2" + integrity sha512-UuqlRrQmT2SWRvahW46cGSany0uTlcj8NYOS5sRGYi8FxPYPoLd5DDmMd32ZXEj2Jq+06uGVQKHxa/hJx2EzKw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-typeof-symbol@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.6.tgz#486bb39d5a18047358e0d04dc0d2f322f0b92e92" + integrity sha512-7m71iS/QhsPk85xSjFPovHPcH3H9qeyzsujhTc+vcdnsXavoWYJ74zx0lP5RhpC5+iDnVLO+PPMHzC11qels1g== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-unicode-escapes@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.6.tgz#0d01fb7fb2243ae1c033f65f6e3b4be78db75f27" + integrity sha512-XNRwQUXYMP7VLuy54cr/KS/WeL3AZeORhrmeZ7iewgu+X2eBqmpaLI/hzqr9ZxCeUoq0ASK4GUzSM0BDhZkLFw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.16.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.18.6.tgz#953422e98a5f66bc56cd0b9074eaea127ec86ace" + integrity sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw== + dependencies: + "@babel/compat-data" "^7.18.6" + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.6" + "@babel/plugin-proposal-async-generator-functions" "^7.18.6" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.6" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.6" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.18.6" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.6" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.18.6" + "@babel/plugin-transform-classes" "^7.18.6" + "@babel/plugin-transform-computed-properties" "^7.18.6" + "@babel/plugin-transform-destructuring" "^7.18.6" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.6" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.6" + "@babel/plugin-transform-function-name" "^7.18.6" + "@babel/plugin-transform-literals" "^7.18.6" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.18.6" + "@babel/plugin-transform-modules-commonjs" "^7.18.6" + "@babel/plugin-transform-modules-systemjs" "^7.18.6" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.18.6" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.18.6" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.18.6" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.6" + "@babel/plugin-transform-typeof-symbol" "^7.18.6" + "@babel/plugin-transform-unicode-escapes" "^7.18.6" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.18.6" + babel-plugin-polyfill-corejs2 "^0.3.1" + babel-plugin-polyfill-corejs3 "^0.5.2" + babel-plugin-polyfill-regenerator "^0.3.1" + core-js-compat "^3.22.1" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime@^7.8.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" + integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" + integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.18.6", "@babel/traverse@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.8.tgz#f095e62ab46abf1da35e5a2011f43aee72d8d5b0" + integrity sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.7" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.8" + "@babel/types" "^7.18.8" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.18.8", "@babel/types@^7.4.4": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.8.tgz#c5af199951bf41ba4a6a9a6d0d8ad722b30cd42f" + integrity sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@fontsource/roboto-condensed@^4.5.2": + version "4.5.8" + resolved "https://registry.yarnpkg.com/@fontsource/roboto-condensed/-/roboto-condensed-4.5.8.tgz#9dff693f5287070f74c199a324e9f1b044cd807b" + integrity sha512-HCuf1rVSOsXnl/KgHNRLCr8XS/Dunzn10BjhliJiEZ5qPynXCWH4RRBFupIODHamhj2Uyp/iZkSQp574luKp6A== + +"@fortawesome/fontawesome-free@^6.0.0": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz#bf5d45611ab74890be386712a0e5d998c65ee2a1" + integrity sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg== + +"@hotwired/stimulus-webpack-helpers@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd" + integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ== + +"@hotwired/stimulus@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.0.1.tgz#141f15645acaa3b133b7c247cad58ae252ffae85" + integrity sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA== + +"@hotwired/turbo@^7.0.1": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.1.0.tgz#27e44e0e3dc5bd1d4bda0766d579cf5a14091cd7" + integrity sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA== + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@nuxt/friendly-errors-webpack-plugin@^2.5.1": + version "2.5.2" + resolved "https://registry.yarnpkg.com/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.5.2.tgz#982a43ee2da61611f7396439e57038392d3944d5" + integrity sha512-LLc+90lnxVbpKkMqk5z1EWpXoODhc6gRkqqXJCInJwF5xabHAE7biFvbULfvTRmtaTzAaP8IV4HQDLUgeAUTTw== + dependencies: + chalk "^2.3.2" + consola "^2.6.0" + error-stack-parser "^2.0.0" + string-width "^4.2.3" + +"@symfony/stimulus-bridge@^3.2.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@symfony/stimulus-bridge/-/stimulus-bridge-3.2.1.tgz#b9c261ad72830fd17898cf27c97862d1cc15b46a" + integrity sha512-eawmVu+tLVkiTz7ewkcsxFvaSZKxFWXmdWxIsxr2jIfQ64PSJg7PIcd7GsPMDxX8sLwg3LGxXfJv5bASL1es+A== + dependencies: + "@hotwired/stimulus-webpack-helpers" "^1.0.1" + "@types/webpack-env" "^1.16.4" + acorn "^8.0.5" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +"@symfony/ux-turbo@file:vendor/symfony/ux-turbo/Resources/assets": + version "0.1.0" + +"@symfony/webpack-encore@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-3.0.0.tgz#e3ee8d8258e6412b0adbce471e3ae35acc3b9236" + integrity sha512-59BTz9glnvcGSFeTaYWtNzgfA26wLwh0TSbVQk8I9hmCnFYOFp+LgnUN+T4MyXA4WWg6+DAFIf+CuOD3o1XwoA== + dependencies: + "@babel/core" "^7.17.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/preset-env" "^7.16.0" + "@nuxt/friendly-errors-webpack-plugin" "^2.5.1" + assets-webpack-plugin "7.0.*" + babel-loader "^8.2.5" + chalk "^4.0.0" + clean-webpack-plugin "^4.0.0" + css-loader "^6.7.0" + css-minimizer-webpack-plugin "^4.0.0" + fast-levenshtein "^3.0.0" + mini-css-extract-plugin "^2.6.0" + pkg-up "^3.1.0" + pretty-error "^4.0.0" + resolve-url-loader "^5.0.0" + semver "^7.3.2" + style-loader "^3.3.0" + sync-rpc "^1.3.6" + terser-webpack-plugin "^5.3.0" + tmp "^0.2.1" + webpack "^5.72" + webpack-cli "^4.9.1" + webpack-dev-server "^4.8.0" + yargs-parser "^21.0.0" + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.5" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" + integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.52" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.52.tgz#7f1f57ad5b741f3d5b210d3b1f145640d89bf8fe" + integrity sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.29" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/glob@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/http-proxy@^1.17.8": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" + integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/minimatch@*": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/node@*": + version "18.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" + integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/webpack-env@^1.16.4": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" + integrity sha512-eHSaNYEyxRA5IAG0Ym/yCyf86niZUIF/TpWKofQI/CVfh5HsMEUyfE2kwFxha4ow0s5g0LfISQxpDKjbRDrizw== + +"@types/ws@^8.5.1": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn@^8.0.5, acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.8.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +assets-webpack-plugin@7.0.*: + version "7.0.0" + resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-7.0.0.tgz#c61ed7466f35ff7a4d90d7070948736f471b8804" + integrity sha512-DMZ9r6HFxynWeONRMhSOFTvTrmit5dovdoUKdJgCG03M6CC7XiwNImPH+Ad1jaVrQ2n59e05lBhte52xPt4MSA== + dependencies: + camelcase "^6.0.0" + escape-string-regexp "^4.0.0" + lodash "^4.17.20" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^0.27.0: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +babel-loader@^8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e" + integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-polyfill-corejs2@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" + integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.3.1" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.21.0" + +babel-plugin-polyfill-regenerator@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" + integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.0.13" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.13.tgz#4ac003dc1626023252d58adf2946f57e5da450c1" + integrity sha512-LWKRU/7EqDUC9CTAQtuZl5HzBALoCYwtLhffW3et7vZMwv3bWLpJf8bRYlMD5OCcDpTfnPgNCV4yo9ZIaJGMiA== + dependencies: + array-flatten "^2.1.2" + dns-equal "^1.0.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +bootstrap@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" + integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.20.2, browserslist@^4.20.3, browserslist@^4.21.1: + version "4.21.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.1.tgz#c9b9b0a54c7607e8dc3e01a0d311727188011a00" + integrity sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ== + dependencies: + caniuse-lite "^1.0.30001359" + electron-to-chromium "^1.4.172" + node-releases "^2.0.5" + update-browserslist-db "^1.0.4" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001359: + version "1.0.30001364" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001364.tgz#1e118f0e933ed2b79f8d461796b8ce45398014a0" + integrity sha512-9O0xzV3wVyX0SlegIQ6knz+okhBB5pE0PC40MNdwcipjwpxoUEHL24uJ+gG42cgklPjfO5ZjZPme9FTSN3QT2Q== + +chalk@^2.0.0, chalk@^2.3.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clean-webpack-plugin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" + integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== + dependencies: + del "^4.1.1" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" + integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^2.6.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-js-compat@^3.21.0, core-js-compat@^3.22.1: + version "3.23.4" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.23.4.tgz#56ad4a352884317a15f6b04548ff7139d23b917f" + integrity sha512-RkSRPe+JYEoflcsuxJWaiMPhnZoFS51FcIxm53k4KzhISCBTmaGlto9dTIrYuk0hnJc3G6pKufAKepHnBq6B6Q== + dependencies: + browserslist "^4.21.1" + semver "7.0.0" + +core-js@^3.23.0: + version "3.23.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.4.tgz#92d640faa7f48b90bbd5da239986602cfc402aa6" + integrity sha512-vjsKqRc1RyAJC3Ye2kYqgfdThb3zYnx9CrqoCcjMOENMtQPC7ZViBvlDxwYU/2z2NI/IPuiXw5mT4hWhddqjzQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-declaration-sorter@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz#72ebd995c8f4532ff0036631f7365cce9759df14" + integrity sha512-OGT677UGHJTAVMRhPO+HJ4oKln3wkBTwtDFH0ojbqm+MJm6xuDMHp2nkhh/ThaBqq20IbraBQSWKfSLNHQO9Og== + +css-loader@^6.7.0: + version "6.7.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" + integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.7" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.5" + +css-minimizer-webpack-plugin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.0.0.tgz#e11800388c19c2b7442c39cc78ac8ae3675c9605" + integrity sha512-7ZXXRzRHvofv3Uac5Y+RkWRNo0ZMlcg8e9/OtrqUYmwDWJo+qs67GvdeFrXLsFb7czKNwjQhPkM0avlIYl+1nA== + dependencies: + cssnano "^5.1.8" + jest-worker "^27.5.1" + postcss "^8.4.13" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.2.12: + version "5.2.12" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.12.tgz#ebe6596ec7030e62c3eb2b3c09f533c0644a9a97" + integrity sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew== + dependencies: + css-declaration-sorter "^6.3.0" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.0" + postcss-convert-values "^5.1.2" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.6" + postcss-merge-rules "^5.1.2" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.3" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.0" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.0" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.1.8: + version "5.1.12" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.12.tgz#bcd0b64d6be8692de79332c501daa7ece969816c" + integrity sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ== + dependencies: + cssnano-preset-default "^5.2.12" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^5.2.2: + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.172: + version "1.4.185" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.185.tgz#3432d7944f1c5fe20664bb45d9cced2151405ce2" + integrity sha512-9kV/isoOGpKkBt04yYNaSWIBn3187Q5VZRtoReq8oz5NY/A4XmU6cAoqgQlDp7kKJCZMRjWZ8nsQyxfpFHvfyw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.9.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +error-stack-parser@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.0" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.10.3" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz#37b899ae47e1090e40e3fd2318e4d5f0142ca912" + integrity sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ== + dependencies: + fastest-levenshtein "^1.0.7" + +fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.7: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +follow-redirects@^1.0.0, follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-port@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.3, glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" + integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5, jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.1.2, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lilconfig@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" + integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.4.7" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.7.tgz#e5252ad2242a724f938cb937e3c4f7ceb1f70e5a" + integrity sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw== + dependencies: + fs-monkey "^1.0.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz#9a1251d15f2035c342d99a468ab9da7a0451b71e" + integrity sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-notifier@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4" + integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +node-releases@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" + integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz#31586df4e184c2e8890e8b34a0b9355313f503ab" + integrity sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g== + dependencies: + browserslist "^4.20.3" + postcss-value-parser "^4.2.0" + +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-merge-longhand@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.6.tgz#f378a8a7e55766b7b644f48e5d8c789ed7ed51ce" + integrity sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.0" + +postcss-merge-rules@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz#7049a14d4211045412116d79b751def4484473a5" + integrity sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.3.tgz#ac41a6465be2db735099bbd1798d85079a6dc1f9" + integrity sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg== + dependencies: + browserslist "^4.16.6" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz#3d23aede35e160089a285e27bf715de11dc9db75" + integrity sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ== + dependencies: + browserslist "^4.16.6" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-reduce-initial@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz#fc31659ea6e85c492fb2a7b545370c215822c5d6" + integrity sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.2.14, postcss@^8.4.13, postcss@^8.4.7: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexpu-core@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d" + integrity sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== + +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== + dependencies: + jsesc "~0.5.0" + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== + dependencies: + adjust-sourcemap-loader "^4.0.0" + convert-source-map "^1.7.0" + loader-utils "^2.0.0" + postcss "^8.2.14" + source-map "0.6.1" + +resolve@^1.14.2, resolve@^1.9.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56" + integrity sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ== + dependencies: + node-forge "^1" + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.2, semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +style-loader@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + +stylehacks@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" + integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q== + dependencies: + browserslist "^4.16.6" + postcss-selector-parser "^6.0.4" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +sync-rpc@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/sync-rpc/-/sync-rpc-1.3.6.tgz#b2e8b2550a12ccbc71df8644810529deb68665a7" + integrity sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw== + dependencies: + get-port "^3.1.0" + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90" + integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.7" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.7.2" + +terser@^5.7.2: + version "5.14.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.1.tgz#7c95eec36436cb11cf1902cc79ac564741d19eca" + integrity sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz#dbfc5a789caa26b1db8990796c2c8ebbce304824" + integrity sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watchpack@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.9.1: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.8.0: + version "4.9.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz#2360a5d6d532acb5410a668417ad549ee3b8a3c9" + integrity sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.1" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.0.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-notifier@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/webpack-notifier/-/webpack-notifier-1.15.0.tgz#72644a1a4ec96b3528704d28f79da5e70048e8ee" + integrity sha512-N2V8UMgRB5komdXQRavBsRpw0hPhJq2/SWNOGuhrXpIgRhcMexzkGQysUyGStHLV5hkUlgpRiF7IUXoBqyMmzQ== + dependencies: + node-notifier "^9.0.0" + strip-ansi "^6.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.72: + version "5.73.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38" + integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.9.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.3.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.4.2: + version "8.8.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" + integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==