{"id":165,"date":"2019-03-12T18:18:22","date_gmt":"2019-03-12T18:18:22","guid":{"rendered":"https:\/\/privateblog.nobytesleft.net\/?p=165"},"modified":"2019-03-14T18:00:58","modified_gmt":"2019-03-14T18:00:58","slug":"symfony-notes","status":"publish","type":"post","link":"https:\/\/privateblog.nobytesleft.net\/?p=165","title":{"rendered":"Symfony notes &#8211; building the Download Portal"},"content":{"rendered":"<h1>Preliminary considerations<\/h1>\n<p>We use PHPStorm with PHP 7.2, apache installed, php-xdebug installed in Ubuntu.<\/p>\n<h1>Setup<\/h1>\n<p>We read the <a href=\"https:\/\/symfony.com\/doc\/current\/setup.html\">setup guide<\/a>. A good source probably will be also <a href=\"https:\/\/codereviewvideos.com\/course\/symfony-3-for-beginners\">Symfony 3 For Beginners<\/a>.<\/p>\n<p class=\"\">We are on a Ubuntu 18.10 x64 VM, with a Windows 10 professional host. After installing PHPStorm, it seemed everything was set up, but creating a new project with<\/p>\n<pre class=\"\">$ composer create-project symfony\/skeleton d1\r\n$ composer create-project symfony\/website-skeleton d2<\/pre>\n<p>would create empty, or almost empty, projects (obviously non-working).<\/p>\n<pre class=\"\">$ composer require symfony\/requirements-checker<\/pre>\n<p>did not solve the problem, which was solved installing missing packages:<\/p>\n<pre class=\"\">$ sudo apt install php libapache2-mod-php php-mbstring php-xmlrpc php-soap php-gd php-xml php-cli php-zip php-mysql php-curl<\/pre>\n<p>Then everything worked. We start with<\/p>\n<pre class=\"\">$ composer create-project symfony\/website-skeleton download_portal<\/pre>\n<p>Another thing that will be useful is the command line interface.<\/p>\n<pre><span class=\"content_262017036_2084667371\">$ wget https:\/\/get.symfony.com\/cli\/installer -O - | bash<\/span><\/pre>\n<p>We install it globally with the adviced command<\/p>\n<pre class=\"\">$ sudo mv \/home\/filippo\/.symfony\/bin\/symfony \/usr\/local\/bin\/symfony<\/pre>\n<p class=\"\">We run also the secuity check with this, as adviced by https:\/\/github.com\/sensiolabs\/security-checker#integration, by running the following from the project base directory.<\/p>\n<pre class=\"\">$ symfony security:check<\/pre>\n<p>This will have to be executed periodically (or maybe as part of a final check before release).<\/p>\n<p>Another useful read is the <a href=\"https:\/\/symfony.com\/doc\/current\/contributing\/code\/standards.html\">coding standard<\/a> document.<\/p>\n<h2>Enabling Symfony plugin in PHPStorm<\/h2>\n<p>Just remember to do it in Settings \u2192 Language &amp; Frameworks \u2192 PHP \u2192 Symfony. You&#8217;ll have to restart PHPStorm (manually).<\/p>\n<h1>Starting<\/h1>\n<pre class=\"\">$ composer bin\/console server:start<\/pre>\n<p>This starts the server, listening on 127.0.0.1:8000. Navigation to the first page shows the Symfony web debug bar &#8211; it will be very useful. The &#8220;404&#8221; error is expected since there is no route.<\/p>\n<p>We build also a first page for our self-training, following approximately <a href=\"https:\/\/symfony.com\/doc\/current\/page_creation.html\">the docs here<\/a>. The example shows how to route using the file routes.yaml, then it mentions that you can &#8220;also&#8221; route via annotations. The best practice, according to the Symfony docs (https:\/\/symfony.com\/doc\/master\/best_practices\/controllers.html), is to <em>route via annotations<\/em>. So we do it.<\/p>\n<p>The tutorial says we should execute<\/p>\n<pre class=\"\">$ composer require annotations<\/pre>\n<p>and we do it. Then we create the file src\/Controller\/TestController.php. We don&#8217;t need the lucky numbers example. It is enough to say &#8220;hello world&#8221; here.<\/p>\n<pre class=\"lang:php decode:true\">&lt;?php\r\n\/\/ src\/Controller\/TestController.php\r\nnamespace App\\Controller;\r\n\r\nuse Symfony\\Component\\HttpFoundation\\Response;\r\nuse Symfony\\Component\\Routing\\Annotation\\Route;\r\n\r\nclass TestController\r\n{\r\n    \/**\r\n    * @Route(\"\/test\/test\", name=\"test\")\r\n    *\/\r\n    public function test()\r\n    {\r\n        return new Response(\r\n            '&lt;html&gt;&lt;body&gt;Hello world!&lt;\/body&gt;&lt;\/html&gt;'\r\n        );\r\n    }\r\n}<\/pre>\n<p>This works, and also the Symfony web debug bar shows that we are authenticated as an anonymous user.<\/p>\n<p>The name=&#8221;test&#8221; will be needed later, keep it.<\/p>\n<p>The guide says a little about the installation of packages via composer and the usage of <em>flex<\/em>, but it seems to be unnecessary to study this now. We instead try to make a template, which will be very useful later. We create a template:<\/p>\n<pre><span class=\"c\">{# templates\/test\/test.html.twig #}<\/span>\r\n<code>&lt;body&gt;<\/code> <span class=\"x\">&lt;h1&gt;{{ salute }} world!<\/span><span class=\"x\">&lt;\/h1&gt;<code>\r\n&lt;\/body&gt;<\/code><\/span><\/pre>\n<p>and change the controller to:<\/p>\n<pre class=\"\">&lt;?php\r\n\/\/ src\/Controller\/TestController.php\r\nnamespace App\\Controller;\r\n\r\nuse Symfony\\Component\\HttpFoundation\\Response;\r\nuse Symfony\\Component\\Routing\\Annotation\\Route;\r\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\r\n\r\nclass TestController extends AbstractController\r\n{\r\n     \/**\r\n      * @Route(\"\/test\/test\")\r\n      *\/\r\n    public function test()\r\n    {\r\n        return $this-&gt;render('test\/test.html.twig', [\r\n            'salute' =&gt; 'Greetings',\r\n        ]);\r\n    }\r\n}<\/pre>\n<h2>Further reading<\/h2>\n<p>Simply read the <a href=\"https:\/\/symfony.com\/doc\/current\/routing.html\">routing docs<\/a> (no need to implement anything, it is very straightforward). Then, the <a href=\"https:\/\/symfony.com\/doc\/current\/controller.html\">controller docs<\/a> show things we already know about, and says the important thing that extending <em>AbstractController<\/em> provides you with the additional methods <em>render<\/em> (already used to render the template), generateUrl(), redirectToRoute() and redirect(). <strong>Fetching Services<\/strong> could be useful. It is also mentioned that you can <strong>generate controllers<\/strong><\/p>\n<pre class=\"\">$ php bin\/console make:controller BrandNewController<\/pre>\n<p>and <strong>generate an entire CRUD<\/strong> from a Doctrine entity (which we&#8217;ll find is the ORM).<\/p>\n<pre class=\"\">$ php bin\/console make:crud Product<\/pre>\n<p>In the controller docs we learn also how to generate 404 errors via exceptions.<\/p>\n<pre class=\"\"><span class=\"k\">use<\/span> <span class=\"nx\">Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException<\/span><span class=\"p\">;<\/span>\r\n\r\n<span class=\"c1\">\/\/ generate 404<\/span>\r\n        <span class=\"k\">throw<\/span> <span class=\"nv\">$this<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">createNotFoundException<\/span><span class=\"p\">(<\/span><span class=\"s1\">'The item does not exist'<\/span><span class=\"p\">);\r\n<\/span><span class=\"c1\">\/\/ this exception ultimately generates a 500 status error<\/span>\r\n<span class=\"k\">        throw<\/span> <span class=\"k\">new<\/span> <span class=\"nx\">\\Exception<\/span><span class=\"p\">(<\/span><span class=\"s1\">'Something went wrong!'<\/span><span class=\"p\">);<\/span><\/pre>\n<p>In the controller we can also access the request object information, by type-hinting an argument with the\u00a0<strong>Request <\/strong>type,\u00a0 the session by type-hinting an argument with the <strong>SessionInterface<\/strong> type:<\/p>\n<pre class=\"\"><span class=\"k\">use<\/span> <span class=\"nx\">Symfony\\Component\\HttpFoundation\\Request<\/span><span class=\"p\">;<\/span>\r\nuse Symfony\\Component\\HttpFoundation\\Session\\SessionInterface;\r\n\r\n<span class=\"k\">\/\/ ... in the controller\r\n    public<\/span> <span class=\"k\">function<\/span> <span class=\"nf\">index<\/span><span class=\"p\">(SessionInterface $session, <\/span><span class=\"nx\">Request<\/span> <span class=\"nv\">$request<\/span><span class=\"p\">,<\/span> <span class=\"nv\">$firstName<\/span><span class=\"p\">,<\/span> <span class=\"nv\">$lastName<\/span><span class=\"p\">)<\/span>\r\n<span class=\"p\">    {\r\n        \/\/ access a request parameter<\/span>\r\n        <span class=\"nv\">$page<\/span> <span class=\"o\">=<\/span> <span class=\"nv\">$request<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">query<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">get<\/span><span class=\"p\">(<\/span><span class=\"s1\">'page'<\/span><span class=\"p\">,<\/span> <span class=\"mi\">1<\/span><span class=\"p\">);<\/span>\r\n        \/\/ stores an attribute for reuse during a later user request\r\n        $session-&gt;set('foo', 'bar');\r\n        \/\/ gets the attribute set by another controller in another request\r\n        $foobar = $session-&gt;get('foobar');\r\n        \/\/ uses a default value if the attribute doesn't exist\r\n        $filters = $session-&gt;get('filters', []);\r\n<span class=\"p\">    }<\/span><\/pre>\n<p>Sessions can be used to store information about the user between requests. Session is enabled by default, but will only be started if you read or write from it. Session storage and other configuration can be controlled under the framework.session configuration in config\/packages\/framework.yaml. Stored attributes remain in the session for the remainder of that user&#8217;s session. There is a specific documentation.<\/p>\n<p>You can also store special messages, called <strong>flash messages<\/strong>, on the user&#8217;s session. By design, flash messages are meant to be used exactly once: they vanish from the session automatically as soon as you retrieve them. This feature makes them useful for storing user notifications.<\/p>\n<p>Also there are more tools for responses.\u00a0Like the Request, the Response object has also a public headers property. This is a <strong>ResponseHeaderBag<\/strong> that has some nice methods for getting and setting response headers. The header names are normalized so that using Content-Type is equivalent to content-type or even content_type.<\/p>\n<p>The only requirement for a controller is to return a <code class=\"notranslate\">Response<\/code> object.<\/p>\n<pre class=\"\"><span class=\"k\">use<\/span> <span class=\"nx\">Symfony\\Component\\HttpFoundation\\Response<\/span><span class=\"p\">;<\/span>\r\n\r\n<span class=\"c1\">\/\/ creates a simple Response with a 200 status code (the default)<\/span>\r\n<span class=\"nv\">$response<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nx\">Response<\/span><span class=\"p\">(<\/span><span class=\"s1\">'Hello '<\/span><span class=\"o\">.<\/span><span class=\"nv\">$name<\/span><span class=\"p\">,<\/span> <span class=\"nx\">Response<\/span><span class=\"o\">::<\/span><span class=\"na\">HTTP_OK<\/span><span class=\"p\">);<\/span>\r\n\r\n<span class=\"c1\">\/\/ creates a CSS-response with a 200 status code<\/span>\r\n<span class=\"nv\">$response<\/span> <span class=\"o\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nx\">Response<\/span><span class=\"p\">(<\/span><span class=\"s1\">'&lt;style&gt; ... &lt;\/style&gt;'<\/span><span class=\"p\">);<\/span>\r\n<span class=\"nv\">$response<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">headers<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">set<\/span><span class=\"p\">(<\/span><span class=\"s1\">'Content-Type'<\/span><span class=\"p\">,<\/span> <span class=\"s1\">'text\/css'<\/span><span class=\"p\">);\r\n\r\n<\/span><span class=\"c1\">\/\/ JSON - returns '{\"username\":\"jane.doe\"}' and sets the proper Content-Type header<\/span>\r\n<span class=\"k\">return<\/span> <span class=\"nv\">$this<\/span><span class=\"o\">-&gt;<\/span><span class=\"na\">json<\/span><span class=\"p\">([<\/span><span class=\"s1\">'username'<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"s1\">'jane.doe'<\/span><span class=\"p\">]);\r\n\r\n\/\/ using <\/span>the file() helper to serve a file from inside a controller:\r\n\/\/ send the file contents and force the browser to download it\r\nreturn $this-&gt;file('\/path\/to\/some_file.pdf');<\/pre>\n<p>The file() helper provides some arguments to configure its behavior.<\/p>\n<h2>Templates<\/h2>\n<p>The documentation about templates starts <a href=\"https:\/\/symfony.com\/doc\/current\/templating.html\">here<\/a>. It is possible to use PHP as a templating language (like any PHP page in a website), or it is possible to use Twig. It is similar to the Jinja2 templating system we already use in python &#8211; there are {{ &#8230; }} (variables, results of expressions), {% &#8230; %} (statements such as for loops), {# &#8230; #} (comments not included in the generated pages), {{ title|upper }} (filters). It is possible to extend base templates with a block replacement syntax. It is possible to template HTML and CSS. There is a function to generate URLs:<\/p>\n<pre class=\"\"><span class=\"nt\">&lt;a<\/span> <span class=\"na\">href=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">path<\/span><span class=\"o\">(<\/span><span class=\"s1\">'welcome'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span><span class=\"nt\">&gt;<\/span>Home<span class=\"nt\">&lt;\/a&gt;<\/span><\/pre>\n<p class=\"\">which refers to the route named &#8216;welcome&#8217;.<\/p>\n<p>Remember that you can link to assets via a specific function, after installing the <em>asset <\/em>package:<\/p>\n<pre class=\"\">$ composer require symfony\/asset<\/pre>\n<p>and you can do<\/p>\n<pre class=\"\"><span class=\"nt\">&lt;img<\/span> <span class=\"na\">src=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">asset<\/span><span class=\"o\">(<\/span><span class=\"s1\">'images\/logo.png'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span> <span class=\"na\">alt=<\/span><span class=\"s\">\"Symfony!\"<\/span> <span class=\"nt\">\/&gt;<\/span>\r\n<span class=\"nt\">&lt;link<\/span> <span class=\"na\">href=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">asset<\/span><span class=\"o\">(<\/span><span class=\"s1\">'css\/blog.css'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span> <span class=\"na\">rel=<\/span><span class=\"s\">\"stylesheet\"<\/span> <span class=\"nt\">\/&gt;<\/span><\/pre>\n<div class=\"literal-block notranslate\"><\/div>\n<p>An advice is given about the way you link stylesheets, java libraries and so on. You define a base template where you insert things that have to be used everywhere, and add things in the extended template, using the parent() Twig function to include both the parent content and the content defined in the derived template.<\/p>\n<pre class=\"\"><span class=\"c\">{# templates\/base.html.twig #}<\/span>\r\n<span class=\"nt\">&lt;html&gt;<\/span>\r\n    <span class=\"nt\">&lt;head&gt;<\/span>\r\n        <span class=\"c\">{# ... #}<\/span>\r\n        <span class=\"cp\">{%<\/span> <span class=\"k\">block<\/span> <span class=\"nv\">stylesheets<\/span> <span class=\"cp\">%}<\/span>\r\n            <span class=\"nt\">&lt;link<\/span> <span class=\"na\">href=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">asset<\/span><span class=\"o\">(<\/span><span class=\"s1\">'css\/main.css'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span> <span class=\"na\">rel=<\/span><span class=\"s\">\"stylesheet\"<\/span> <span class=\"nt\">\/&gt;<\/span>\r\n        <span class=\"cp\">{%<\/span> <span class=\"k\">endblock<\/span> <span class=\"cp\">%}<\/span>\r\n    <span class=\"nt\">&lt;\/head&gt;<\/span>\r\n    <span class=\"nt\">&lt;body&gt;<\/span>\r\n        <span class=\"c\">{# ... #}<\/span>\r\n        <span class=\"cp\">{%<\/span> <span class=\"k\">block<\/span> <span class=\"nv\">javascripts<\/span> <span class=\"cp\">%}<\/span>\r\n            <span class=\"nt\">&lt;script <\/span><span class=\"na\">src=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">asset<\/span><span class=\"o\">(<\/span><span class=\"s1\">'js\/main.js'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span><span class=\"nt\">&gt;&lt;\/script&gt;<\/span>\r\n        <span class=\"cp\">{%<\/span> <span class=\"k\">endblock<\/span> <span class=\"cp\">%}<\/span>\r\n    <span class=\"nt\">&lt;\/body&gt;<\/span>\r\n<span class=\"nt\">&lt;\/html&gt;\r\n<\/span><\/pre>\n<pre class=\"\"><span class=\"c\">{# templates\/derived\/derived.html.twig #}<\/span>\r\n<span class=\"cp\">{%<\/span> <span class=\"k\">extends<\/span> <span class=\"s1\">'base.html.twig'<\/span> <span class=\"cp\">%}<\/span>\r\n\r\n<span class=\"cp\">{%<\/span> <span class=\"k\">block<\/span> <span class=\"nv\">stylesheets<\/span> <span class=\"cp\">%}<\/span>\r\n    <span class=\"cp\">{{<\/span> <span class=\"nb\">parent<\/span><span class=\"o\">()<\/span> <span class=\"cp\">}}<\/span>\r\n    <span class=\"nt\">&lt;link<\/span> <span class=\"na\">href=<\/span><span class=\"s\">\"<\/span><span class=\"cp\">{{<\/span> <span class=\"nv\">asset<\/span><span class=\"o\">(<\/span><span class=\"s1\">'css\/specific.css'<\/span><span class=\"o\">)<\/span> <span class=\"cp\">}}<\/span><span class=\"s\">\"<\/span> <span class=\"na\">rel=<\/span><span class=\"s\">\"stylesheet\"<\/span> <span class=\"nt\">\/&gt;<\/span>\r\n<span class=\"cp\">{%<\/span> <span class=\"k\">endblock<\/span> <span class=\"cp\">%}<\/span>\r\n\r\n<span class=\"c\">{# ... #}<\/span><\/pre>\n<p>Symfony also gives you a global <strong>app<\/strong> variable in Twig that can be used to access the current user, the Request and more.<\/p>\n<p>Output escaping is done <strong>automatically<\/strong> unless explicitly disabled:<\/p>\n<pre class=\"\"><span class=\"x\">&lt;!-- output escaping is on automatically --&gt;<\/span>\r\n<span class=\"cp\">{{<\/span> <span class=\"nv\">description<\/span> <span class=\"cp\">}}<\/span><span class=\"x\"> &lt;!-- I &amp;lt;3 this product --&gt;<\/span>\r\n\r\n<span class=\"x\">&lt;!-- disable output escaping with the raw filter --&gt;<\/span>\r\n<span class=\"cp\">{{<\/span> <span class=\"nv\">description<\/span><span class=\"o\">|<\/span><span class=\"nf\">raw<\/span> <span class=\"cp\">}}<\/span><span class=\"x\"> &lt;!-- I &lt;3 this product --&gt;<\/span><\/pre>\n<h2>Configuration<\/h2>\n<p>The configuration <a href=\"https:\/\/symfony.com\/doc\/current\/configuration.html\">docs<\/a> are straightforward. We&#8217;ll use YAML, I think. The configuration for each package can be found in <code class=\"notranslate\">config\/packages\/<\/code>. For instance, the framework bundle is configured in <code class=\"notranslate\">config\/packages\/framework.yaml<\/code>. There is also a .env file which is loaded and its contents become environment variables. This is useful during development, or if setting environment variables is difficult for your deployment. When you install packages, more environment variables are added to this file. But you can also add your own.<\/p>\n<p>Environment variables can be referenced in any other configuration files by using a special syntax. For example, if you install the doctrine package, then you will have an environment variable called DATABASE_URL in your .env file. This is referenced inside config\/packages\/doctrine.yaml.<\/p>\n<p>Applications created before November 2018 had a slightly different system, involving a .env.dist file. For information about upgrading, see <a href=\"https:\/\/symfony.com\/doc\/current\/configuration\/dot-env-changes.html\">Nov 2018 Changes to .env &amp; How to Update<\/a>. This may be useful to remember since it means that examples made before 2018-11 can be outdated.<\/p>\n<p>Some useful information we copy directly here.<\/p>\n<p>The <code class=\"notranslate\">.env<\/code> file is special, because it defines the values that usually change on each server. For example, the database credentials on your local development machine might be different from your workmates. The <code class=\"notranslate\">.env<\/code> file should contain sensible, non-secret <em>default<\/em> values for all of your environment variables and <em>should<\/em> be commited to your repository.<\/p>\n<p>To override these variables with machine-specific or sensitive values, create a <code class=\"notranslate\">.env.local<\/code> file. This file is <strong>not committed to the shared repository<\/strong> and is only stored on your machine. In fact, the <code class=\"notranslate\">.gitignore<\/code> file that comes with Symfony prevents it from being committed.<\/p>\n<p>You can also create a few other <code class=\"notranslate\">.env<\/code> files that will be loaded:<\/p>\n<ul class=\"simple\">\n<li><code class=\"notranslate\">.env.{environment}<\/code>: e.g. <code class=\"notranslate\">.env.test<\/code> will be loaded in the <code class=\"notranslate\">test<\/code> environment and committed to your repository.<\/li>\n<li><code class=\"notranslate\">.env.{environment}.local<\/code>: e.g. <code class=\"notranslate\">.env.prod.local<\/code> will be loaded in the <code class=\"notranslate\">prod<\/code> environment but will <em>not<\/em> be committed to your repository.<\/li>\n<\/ul>\n<p>If you decide to set real environment variables on production, the <code class=\"notranslate\">.env<\/code> files <em>are<\/em> still loaded, but your real environment variables will override those values.<\/p>\n<p>To learn more about <em>how<\/em> to execute and control each environment, see <a class=\"reference internal\" href=\"https:\/\/symfony.com\/doc\/current\/configuration\/environments.html\"><em>How to Master and Create new Environments<\/em><\/a>.<\/p>\n<h1>Database<\/h1>\n<p>Our application needs a database. Symfony uses <a href=\"https:\/\/symfony.com\/doc\/current\/doctrine.html\">Doctrine<\/a> as ORM. Installation:<\/p>\n<pre class=\"\">$ composer require symfony\/orm-pack\r\n$ composer require symfony\/maker-bundle --dev  # this is only an help to generate code<\/pre>\n<p>We configure the DB url in the <strong>.env<\/strong> file. We use SQLite, at least for now.<\/p>\n<p>We have to do<\/p>\n<pre class=\"\">$ sudo apt-get install php-sqlite3<\/pre>\n<p>to be able to use SQLite with PHP. We change<\/p>\n<pre class=\"\">DATABASE_URL=mysql:\/\/db_user:db_password@127.0.0.1:3306\/db_name<\/pre>\n<p>to<\/p>\n<pre class=\"\">DATABASE_URL=sqlite:\/\/\/%kernel.project_dir%\/var\/data.db<\/pre>\n<p>Now we run<\/p>\n<pre class=\"\">$ php bin\/console doctrine:database:create<\/pre>\n<p>creating the sqlite database <em>\/home\/filippo\/repositories\/download_portal\/var\/data.db<\/em>. Note that the entire \/var path is <em>not<\/em> saved in the git repository, due to the default .gitignore file.<\/p>\n<p>The first entity we create is the\u00a0<em>Project<\/em> for our download application. We use<\/p>\n<pre class=\"\">$ php bin\/console make:entity<\/pre>\n<p>The <em>id<\/em> property is created automatically, we add <em>descriptor<\/em> and\u00a0 <em>human_name<\/em>. Files src\/Entity\/Project.php and src\/Repository\/ProjectRepository.php are created automatically. To create the database we run<\/p>\n<pre class=\"\">$ php bin\/console make:migration\r\n$ php bin\/console doctrine:migrations:migrate<\/pre>\n<p>The next sections of the documentation are straightforward.<\/p>\n<h1>Authentication<\/h1>\n<p>The <a href=\"https:\/\/symfony.com\/doc\/current\/security.html\">security documentation<\/a> is the place where to start. We install the security bundle:<\/p>\n<pre class=\"\">$ composer require symfony\/security-bundle<\/pre>\n<p>We create an User entity.<\/p>\n<pre class=\"\">$ php bin\/console make:user\r\n<\/pre>\n<p>We also keep all the defaults except we use the <em>username<\/em> as unique user identifier. Files src\/Entity\/User.php and src\/Repository\/UserRepository.php are created automatically as well. Then we run again<\/p>\n<pre class=\"\">$ php bin\/console make:migration\r\n$ php bin\/console doctrine:migrations:migrate<\/pre>\n<p>The &#8220;User Provider&#8221; and how the passwords are encoded are already configured when using make:user. The file\u00a0<strong>config\/packages\/security.yaml<\/strong> is changed automatically as well.<\/p>\n<p>We skip the part about the UserFixtures, we&#8217;ll return to it later.<\/p>\n<h2>Security<\/h2>\n<p>The security system is configured in config\/packages\/security.yaml. The most important section is firewalls. A &#8220;firewall&#8221; is your authentication system: the configuration below it defines how your users will be able to authenticate (e.g. login form, API token, etc).<\/p>\n<pre class=\"\"># config\/packages\/security.yaml\r\nsecurity:\r\n\u00a0\u00a0\u00a0 firewalls:\r\n\u00a0\u00a0\u00a0   dev:\r\n        pattern: ^\/(_(profiler|wdt)|css|images|js)\/\r\n        security: false\r\n      main:\r\n        anonymous: ~<\/pre>\n<p>The documentation now tells you about the <em>Guard authenticator<\/em>. However, in the building of the login forms, the guard authenticator is generated automatically, as we will see.<\/p>\n<h3>Looking for how to build the login system<\/h3>\n<p>Now we have some conflicting information. The Symfony 4.0 documentation https:\/\/symfony.com\/doc\/4.0\/security\/form_login_setup.html advice is to use FOSUserBundle. The advice has been removed as of Symfony 4.2, and indeed it seems a <em>deprecated<\/em> way of doing it (https:\/\/github.com\/symfony\/symfony-docs\/issues\/9946, https:\/\/jolicode.com\/blog\/do-not-use-fosuserbundle). We follow the <a href=\"https:\/\/symfony.com\/doc\/current\/security\/form_login_setup.html\">4.2 documentation<\/a>.<\/p>\n<pre class=\"\">$ php bin\/console make:auth<\/pre>\n<p>And select &#8220;Login form authenticator [1]&#8221;, the name &#8221; LoginFormAuthenticator&#8221; for the class name of the authenticator, and leave the default &#8220;SecurityController&#8221; name for the controller class.<\/p>\n<p>After this, we have a login form working at http:\/\/127.0.0.1\/login.<\/p>\n<p>In src\/LoginFormAuthenticator.php, we redirect onAuthenticationSuccess:<\/p>\n<pre class=\"\">return new RedirectResponse($this-&gt;urlGenerator-&gt;generate('test'));<\/pre>\n<p>We need also some dummy users, let&#8217;s go back to the <a href=\"https:\/\/symfony.com\/doc\/current\/security.html#c-encoding-passwords\">security documentation<\/a> where we are told to use <a class=\"reference internal\" href=\"https:\/\/symfony.com\/doc\/current\/doctrine.html#doctrine-fixtures\">DoctrineFixturesBundle<\/a>:<\/p>\n<pre class=\"\">$ composer require orm-fixtures --dev\r\n$ php bin\/console make:fixtures\r\n<\/pre>\n<p>with class name <em>UserFixtures<\/em>. Then modify src\/DataFixtures\/UserFixtures.php:<\/p>\n<pre class=\"\">&lt;?php\r\nnamespace App\\DataFixtures;\r\n\r\nuse Doctrine\\Bundle\\FixturesBundle\\Fixture;\r\nuse Doctrine\\Common\\Persistence\\ObjectManager;\r\nuse Symfony\\Component\\Security\\Core\\Encoder\\UserPasswordEncoderInterface;\r\nuse App\\Entity\\User;\r\n\r\nclass UserFixtures extends Fixture\r\n{\r\n    private $passwordEncoder;\r\n\r\n    public function __construct(UserPasswordEncoderInterface $passwordEncoder)\r\n    {\r\n        $this-&gt;passwordEncoder = $passwordEncoder;\r\n    }\r\n\r\n    public function load(ObjectManager $manager)\r\n    {\r\n        $user = new User();\r\n        $user-&gt;setPassword($this-&gt;passwordEncoder-&gt;encodePassword(\r\n            $user,'test'\r\n        ));\r\n        $user-&gt;setUsername('test');\r\n        $manager-&gt;persist($user);\r\n\r\n        $manager-&gt;flush();\r\n    }\r\n}<\/pre>\n<p>Create the fixture by running:<\/p>\n<pre class=\"\">$ php bin\/console doctrine:fixtures:load<\/pre>\n<p>We can then login at http:\/\/127.0.0.1:8000\/login, being redirected at test (where we can see, in the web debug toolbar, that we are logged in as &#8220;test&#8221;).<\/p>\n<p>We add a logout function also &#8211; in the symfony 4 docs, it is said to configure a route in config\/routes.yaml. However we never used this, so it may be better to do as <a href=\"https:\/\/codereviewvideos.com\/course\/symfony-3-for-beginners\/video\/adding-logout\">here<\/a>, just for consistency. However the route option seems easier, so we&#8217;ll make an exception for the logout (since we can dispense with the controller).<\/p>\n<p>config\/routes.yaml:<\/p>\n<pre class=\"\">app_logout:\r\n  path: \/logout\r\n  methods: GET<\/pre>\n<p>config\/packages\/security.yaml: under security: -&gt; firewalls -&gt; main:<\/p>\n<pre class=\"\"> logout:\r\n    path: app_logout\r\n    # where to redirect after logout - we redirect to our test page as of now.\r\n    target: test<\/pre>\n<h2>Admin<\/h2>\n<p>Our application will require an admin interface some time in the future. We investigate a little. A starting point is <a href=\"https:\/\/symfony.com\/doc\/bundles\/\">here<\/a>. EasyAdmin seems the way to go, users say it is better documented and much easier to use, even if less powerful.<\/p>\n<pre class=\"\">$ composer require admin<\/pre>\n<pre class=\"\">We then edit config\/packages\/easy_admin.yaml:\r\neasy_admin:\r\n    site_name: 'Download portal administration'\r\n    entities:\r\n        Project:\r\n            class: App\\Entity\\Project\r\n        User:\r\n            class: App\\Entity\\User<\/pre>\n<p>Now we can already manage the entities, but without any authentication. Since we want to test security, we configure it for the admin. We go back to the <a href=\"https:\/\/symfony.com\/doc\/current\/security.html#denying-access-roles-and-other-authorization\">security document<\/a>.<\/p>\n<p>We have <strong>roles <\/strong>defining what an user can do. When a user logs in, Symfony calls the getRoles() method on your User object to determine which roles this user has. In the User class that we generated earlier, the roles are an array that&#8217;s stored in the database (this has been done automatically, if it wasn&#8217;t clear), and every user is always given at least one role: ROLE_USER (this not in the DB, but in the User entity class).<\/p>\n<p>We create a very small hierarchy, where there is also ROLE_ADMIN. Only ROLE_ADMIN users will be able to access the admin. The example says:<\/p>\n<pre class=\"\"><span class=\"c1\"># config\/packages\/security.yaml<\/span>\r\n<span class=\"l-Scalar-Plain\">security<\/span><span class=\"p-Indicator\">:<\/span>\r\n    <span class=\"c1\"># ...<\/span>\r\n    <span class=\"l-Scalar-Plain\">role_hierarchy<\/span><span class=\"p-Indicator\">:<\/span>\r\n        <span class=\"l-Scalar-Plain\">ROLE_ADMIN<\/span><span class=\"p-Indicator\">:<\/span>       <span class=\"l-Scalar-Plain\">ROLE_USER<\/span>\r\n        <span class=\"l-Scalar-Plain\">ROLE_SUPER_ADMIN<\/span><span class=\"p-Indicator\">:<\/span> <span class=\"p-Indicator\">[<\/span><span class=\"nv\">ROLE_ADMIN<\/span><span class=\"p-Indicator\">,<\/span> <span class=\"nv\">ROLE_ALLOWED_TO_SWITCH<\/span><span class=\"p-Indicator\">]<\/span><\/pre>\n<p>We keep it this way, for now.<\/p>\n<p>We change the UserFixture to add an &#8220;admin&#8221; and a &#8220;superadmin&#8221; user:<\/p>\n<p>with class name <em>UserFixtures<\/em>. Then modify src\/DataFixtures\/UserFixtures.php:<\/p>\n<pre class=\"\">&lt;?php\r\nnamespace App\\DataFixtures;\r\n\r\nuse Doctrine\\Bundle\\FixturesBundle\\Fixture;\r\nuse Doctrine\\Common\\Persistence\\ObjectManager;\r\nuse Symfony\\Component\\Security\\Core\\Encoder\\UserPasswordEncoderInterface;\r\nuse App\\Entity\\User;\r\n\r\nclass UserFixtures extends Fixture\r\n{\r\n    private $passwordEncoder;\r\n\r\n    public function __construct(UserPasswordEncoderInterface $passwordEncoder)\r\n    {\r\n        $this-&gt;passwordEncoder = $passwordEncoder;\r\n    }\r\n\r\n    public function load(ObjectManager $manager)\r\n    {\r\n        $user = new User();\r\n        $user-&gt;setUsername('test');\r\n        $user-&gt;setPassword($this-&gt;passwordEncoder-&gt;encodePassword(\r\n            $user,'test'\r\n        ));\r\n        $manager-&gt;persist($user);\r\n        $user = new User();\r\n        $user-&gt;setUsername('admin');\r\n        $user-&gt;setPassword($this-&gt;passwordEncoder-&gt;encodePassword(\r\n            $user,'admin'\r\n        ));\r\n\u00a0       $user-&gt;setRoles([\"ROLE_ADMIN\"]);\r\n        $manager-&gt;persist($user);\r\n\r\n        $user = new User();\r\n        $user-&gt;setUsername('super');\r\n        $user-&gt;setPassword($this-&gt;passwordEncoder-&gt;encodePassword(\r\n            $user,'super'\r\n        ));\r\n      \u00a0 $user-&gt;setRoles([\"ROLE_SUPER_ADMIN\"]);\r\n        $manager-&gt;persist($user);\r\n\r\n        $manager-&gt;flush();\r\n    }\r\n}<\/pre>\n<p>Then:<\/p>\n<pre class=\"\">$ php bin\/console doctrine:fixtures:load<\/pre>\n<p>As usual there are more ways to secure a part of the site. You can secure using URL matching, or in the controller, or in the template. In security.yaml:<\/p>\n<pre class=\"\">    <span class=\"l-Scalar-Plain\">access_control<\/span><span class=\"p-Indicator\">:<\/span>\r\n        <span class=\"c1\"># require ROLE_ADMIN for \/admin*<\/span>\r\n        <span class=\"p-Indicator\">-<\/span> <span class=\"p-Indicator\">{<\/span> <span class=\"nv\">path<\/span><span class=\"p-Indicator\">:<\/span> <span class=\"nv\">^\/admin<\/span><span class=\"p-Indicator\">,<\/span> <span class=\"nv\">roles<\/span><span class=\"p-Indicator\">:<\/span> <span class=\"nv\">ROLE_ADMIN<\/span> <span class=\"p-Indicator\">}<\/span><\/pre>\n<p>At this point, it will work &#8211; by throwing an &#8220;access denied&#8221; if the test user tries to access \/admin, for example.<\/p>\n<h1>FOSUserBundle<\/h1>\n<p>The <a href=\"https:\/\/symfony.com\/doc\/current\/bundles\/FOSUserBundle\/index.html\">FOSUserBundle<\/a> package seems ok to provide standard user management features (such as password reset via email). We install it. Executing immediately<\/p>\n<pre class=\"\">$ composer require friendsofsymfony\/user-bundle<\/pre>\n<p>causes an error. It is stated that &#8220;If you encounter installation errors pointing at a lack of configuration parameters, such as The child node &#8220;db_driver&#8221; at path &#8220;fos_user&#8221; must be configured, you should complete the configuration in Step 5 first and then re-run this step.&#8221; Remember this! The docs say to edit <span class=\"c1\">app\/config\/config.yml, but since we are on Symfony 4.2, we have to edit <code class=\" language-yaml\"><span class=\"token comment\" spellcheck=\"true\">config\/packages\/fos_user.yaml<\/span><\/code> instead:<br \/>\n<\/span><\/p>\n<pre class=\"language-yaml code-toolbar\"><code class=\" language-yaml\"><span class=\"token comment\" spellcheck=\"true\"># config\/packages\/fos_user.yaml<\/span>\r\n<span class=\"token key atrule\">fos_user<\/span><span class=\"token punctuation\">:<\/span>\r\n    <span class=\"token key atrule\">db_driver<\/span><span class=\"token punctuation\">:<\/span> orm <span class=\"token comment\" spellcheck=\"true\"># other valid values are 'mongodb' and 'couchdb'<\/span>\r\n    <span class=\"token key atrule\">firewall_name<\/span><span class=\"token punctuation\">:<\/span> main\r\n    <span class=\"token key atrule\">user_class<\/span><span class=\"token punctuation\">:<\/span> App\\Entity\\User\r\n    <span class=\"token key atrule\">from_email<\/span><span class=\"token punctuation\">:\r\n        <span class=\"l-Scalar-Plain\">address<\/span><span class=\"p-Indicator\">:<\/span> <span class=\"s\">\"ndp@ndp.test\" # Provide a standard email address that we will have to change later - maybe a configuration in the DB customizable by the admin?\r\n       <\/span> <span class=\"l-Scalar-Plain\">sender_name<\/span><span class=\"p-Indicator\">:<\/span> <span class=\"s\">\"ndp@ndp.test\"<\/span><\/span><\/code><\/pre>\n<p>Note that the example configuration uses <code class=\" language-yaml\"><span class=\"token key atrule\">from_email: address: \"%mailer_user%\"<\/span><\/code> which does not exist in our installation, so it would cause an error anyway. We&#8217;ll configure this later.\u00a0 Repeating<strong> $ composer require friendsofsymfony\/user-bundle<\/strong> will still fail with &#8220;The service &#8220;fos_user.resetting.controller&#8221; has a dependency on a non-existent service &#8220;templating&#8221;. &#8220;. Someone says to solve by<a href=\"https:\/\/stackoverflow.com\/a\/33410854\"> adding php to the templating engines<\/a>. This is not the solution, the solution is instead to add to config\/packages\/framework.yaml:<\/p>\n<pre class=\"\">    # Added for FOSUserBundle\r\n    templating:\r\n      engines: ['twig']<\/pre>\n<p>At this point <strong>$ composer require friendsofsymfony\/user-bundle<\/strong> works. Since we already have an User class, we have to change it in order to extend it from <span class=\"nx\">FOS\\UserBundle\\Model\\User. In src\/Entity\/User.php:<\/span><\/p>\n<pre class=\"\">&lt;?php\r\nnamespace App\\Entity;\r\n\r\nuse Doctrine\\ORM\\Mapping as ORM;\r\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\r\nuse FOS\\UserBundle\\Model\\User as BaseUser;\r\n\/**\r\n* @ORM\\Entity(repositoryClass=\"App\\Repository\\UserRepository\")\r\n*\/\r\nclass User extends BaseUser\r\n{\r\n    \/**\r\n     * @ORM\\Id()\r\n     * @ORM\\GeneratedValue()\r\n     * @ORM\\Column(type=\"integer\")\r\n     *\/\r\n    protected $id;\r\n}<\/pre>\n<p>everything else is provided by the FOSUser bundle.<\/p>\n<p>We have to clear manually the user DB table (otherwise the migration won&#8217;t work), make a migration and run it<\/p>\n<pre class=\"\">$ bin\/console make:migration\r\n$ bin\/console doctrine:migrations:migrate<\/pre>\n<p>We have to change also the user fixtures \/src\/DataFixtures\/UserFixtures.php, adding the email and enabling the accounts (otherwise we won&#8217;t be able to login due to &#8220;disabled&#8221; accounts) &#8211; for example:<\/p>\n<pre class=\"\">$user-&gt;setEmail('test@ndp.ndp');\r\n$user-&gt;setEnabled(true);<\/pre>\n<p>&nbsp;<\/p>\n<p>From now, we are leaving the &#8220;novice&#8221; realm and we continue without writing everything.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Preliminary considerations We use PHPStorm with PHP 7.2, apache installed, php-xdebug installed in Ubuntu. Setup We read the setup guide. A good source probably will be also Symfony 3 For&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-165","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/posts\/165","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=165"}],"version-history":[{"count":4,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/posts\/165\/revisions"}],"predecessor-version":[{"id":170,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=\/wp\/v2\/posts\/165\/revisions\/170"}],"wp:attachment":[{"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=165"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=165"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/privateblog.nobytesleft.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=165"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}