{"id":788,"date":"2020-09-22T14:35:16","date_gmt":"2020-09-22T12:35:16","guid":{"rendered":"https:\/\/wiki.ad-libitum.info\/?page_id=788"},"modified":"2026-07-06T17:04:12","modified_gmt":"2026-07-06T15:04:12","slug":"schritt-fuer-schritt","status":"publish","type":"page","link":"https:\/\/wiki.ad-libitum.info\/?page_id=788","title":{"rendered":"Schritt f\u00fcr Schritt"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\" id=\"projekt-einrichten\">Projekt einrichten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um anzufangen, muss das Projekt eingerichtet werden. Dazu auf DOS-Ebene in das Verzeichnis <code>htdocs<\/code> gehen und<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>composer create-project symfony\/skeleton zielpfad<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">eingeben. <code>Zielpfad<\/code> entspricht dabei dem Verzeichnis, in dem das Projekt installiert werden soll.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Im Verzeichnis <code>zielpfad\\public<\/code> muss eine <code>.htaccess<\/code> erstellt werden. Die bekommt folgenden Inhalt:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;IfModule mod_rewrite.c&gt;\n    RewriteEngine On\n\n    # Determine the _RewriteBase automatically and set is \n    # as environment variable\n    RewriteCond %{REQUEST_URI}::$1 ^(\/.+)\/(.*)::\\2$\n    RewriteRule ^(.*) - [E=BASE:%1]\n\n    # If the requested filename existes, simply serve it.\n    # We only want to let Apache serve files and not directories.\n    RewriteCond %{REQUEST_FILENAME} -f\n    RewriteRule .? - [L]\n\n    # Rewrite all other queries to the front controller.\n    RewriteRule .? %{ENV:BASE}\/index.php [L]\n&lt;\/IfModule&gt;<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Symfony selbst schl\u00e4gt einen <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/symfony\/recipes-contrib\/blob\/main\/symfony\/apache-pack\/1.0\/public\/.htaccess\" target=\"_blank\">anderen Inhalt<\/a> vor:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># Use the front controller as index file. It serves as a fallback solution when\n# every other rewrite\/redirect fails (e.g. in an aliased environment without\n# mod_rewrite). Additionally, this reduces the matching process for the\n# start page (path \"\/\") because otherwise Apache will apply the rewriting rules\n# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).\nDirectoryIndex index.php\n\n# By default, Apache does not evaluate symbolic links if you did not enable this\n# feature in your server configuration. Uncomment the following line if you\n# install assets as symlinks or if you experience problems related to symlinks\n# when compiling LESS\/Sass\/CoffeScript assets.\n# Options +FollowSymlinks\n\n# Disabling MultiViews prevents unwanted negotiation, e.g. \"\/index\" should not resolve\n# to the front controller \"\/index.php\" but be rewritten to \"\/index.php\/index\".\n&lt;IfModule mod_negotiation.c&gt;\n    Options -MultiViews\n&lt;\/IfModule&gt;\n\n&lt;IfModule mod_rewrite.c&gt;\n    # This Option needs to be enabled for RewriteRule, otherwise it will show an error like\n    # 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'\n    Options +FollowSymlinks\n\n    RewriteEngine On\n\n    # Determine the RewriteBase automatically and set it as environment variable.\n    # If you are using Apache aliases to do mass virtual hosting or installed the\n    # project in a subdirectory, the base path will be prepended to allow proper\n    # resolution of the index.php file and to redirect to the correct URI. It will\n    # work in environments without path prefix as well, providing a safe, one-size\n    # fits all solution. But as you do not need it in this case, you can comment\n    # the following 2 lines to eliminate the overhead.\n    RewriteCond %{REQUEST_URI}::$0 ^(\/.+)\/(.*)::\\2$\n    RewriteRule .* - [E=BASE:%1]\n\n    # Sets the HTTP_AUTHORIZATION header removed by Apache\n    RewriteCond %{HTTP:Authorization} .+\n    RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]\n\n    # Redirect to URI without front controller to prevent duplicate content\n    # (with and without `\/index.php`). Only do this redirect on the initial\n    # rewrite by Apache and not on subsequent cycles. Otherwise we would get an\n    # endless redirect loop (request -&gt; rewrite to front controller -&gt;\n    # redirect -&gt; request -&gt; ...).\n    # So in case you get a \"too many redirects\" error or you always get redirected\n    # to the start page because your Apache does not expose the REDIRECT_STATUS\n    # environment variable, you have 2 choices:\n    # - disable this feature by commenting the following 2 lines or\n    # - use Apache &gt;= 2.3.9 and replace all L flags by END flags and remove the\n    #   following RewriteCond (best solution)\n    RewriteCond %{ENV:REDIRECT_STATUS} =\"\"\n    RewriteRule ^index\\.php(?:\/(.*)|$) %{ENV:BASE}\/$1 [R=301,L]\n\n    # If the requested filename exists, simply serve it.\n    # We only want to let Apache serve files and not directories.\n    # Rewrite all other queries to the front controller.\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ %{ENV:BASE}\/index.php [L]\n&lt;\/IfModule&gt;\n\n&lt;IfModule !mod_rewrite.c&gt;\n    &lt;IfModule mod_alias.c&gt;\n        # When mod_rewrite is not available, we instruct a temporary redirect of\n        # the start page to the front controller explicitly so that the website\n        # and the generated links can still be used.\n        RedirectMatch 307 ^\/$ \/index.php\/\n        # RedirectTemp cannot be used instead\n    &lt;\/IfModule&gt;\n&lt;\/IfModule&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"subversion\">Subversion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Damit die Versionierung klappt, muss Subversion eingerichtet werden. Zuerst einmal unter den Einstellungen unter Subversion die Option <code>Enable Interactive Mode<\/code> <strong>aus<\/strong>schalten. In Subversion auf dem Server das gew\u00fcnschte Verzeichnis erstellen. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt im Dateiexplorer \u00fcber das Kontextmen\u00fc die nicht erforderlichen Verzeichnisse von Subversion ausnehmen: .idea, var, vendor, public\/bundles, \/config\/secrets\/prod\/prod.decrypt.private.php, .env.local, .env.local.php, .env.*.local.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann in PHPStorm unter <code>VCS \/ Import into Version Control<\/code> aufrufen und in das zuvor erstellte Repository exportieren. Anschlie\u00dfend gleich wieder mit <code>VCS \/ Get from Version Control <\/code>importieren, damit \u00c4nderungen erkannt werden. Schlie\u00dflich \u00fcber <code>VCS<\/code> noch die Option <code>Enable Version Control Integration<\/code> ausw\u00e4hlen und die passenden Einstellungen w\u00e4hlen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Git<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Im Men\u00fc von PHPStorm unter <code>VCS \/ Share Project on GitHub<\/code> ausw\u00e4hlen. Anschlie\u00dfend kommt die Meldung, dass eine dubiose Eigent\u00fcmergeschichte f\u00fcr das Verzeichnis festgestellt worden sei. Um das zu beheben, auf XAMPP im Projektverzeichnis den Befehl <code>git config --global --add safe.directory '%(prefix)\/\/\/XAMPP\/htdocs\/messagetest'<\/code> ausf\u00fchren. <code>messagetest<\/code> muss durch das entsprechende Verzeichnis ersetzt werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"zugriff-auf-die-neue-site\">Zugriff auf die neue Site<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr den Zugriff auf die Site sind zwei Anpassungen erforderlich:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In der <code>xampp\\apache\\conf\\extra\\httpd-vhosts.conf<\/code> m\u00fcssen die folgenden Zeilen eingef\u00fcgt werden, wobei die Parameter hinter <code>DocumentRoot<\/code> und <code>ServerName<\/code> nat\u00fcrlich <strong>angepasst<\/strong> werden m\u00fcssen.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;virtualhost *:80=\"\"&gt;\n&nbsp;&nbsp;&nbsp;&nbsp;DocumentRoot \"C:\\xampp\\htdocs\\symfony\\public\"\n&nbsp;&nbsp;&nbsp;&nbsp;ServerName symphony.test\n&lt;\/virtualhost&gt;<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Anschlie\u00dfend muss XAMPP neu gestartet werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die zweite \u00c4nderung muss auf denjenigen PCs innerhalb des lokalen Netzes durchgef\u00fchrt werden, auf denen die Seite angezeigt werden soll. Und zwar in der Datei <code>C:\\Windows\\System32\\drivers\\etc\\hosts<\/code>. Dort muss f\u00fcr die neue Site eine neue Zeile eingef\u00fcgt werden: <code>192.168.0.12 adlibitum.test<\/code>. Die IP ist dabei die IP des XAMPP-Servers und der zweite Parameter derjenige, der in der <code>httpd-vhosts.conf<\/code> als <code>ServerName<\/code> eingetragen worden ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In PHPStorm in den Einstellungen nach Symfony suchen und das Plugin f\u00fcr dieses Projekt aktivieren. Anschlie\u00dfend noch nach Composer suchen und pr\u00fcfen, ob der Pfad zu composer.json unter <code>Path to composer.json<\/code> korrekt eingetragen ist.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"maker-bundle\">Maker-Bundle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mit Hilfe des Maker-Bundles lassen sich zahlreiche Arbeiten in Synfony erheblich abk\u00fcrzen &#8211; erstellen von Kontrollern, Klassen Tests und mehr. Zur Installation:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require maker --dev<\/pre>\n\n\n<p>Um Unit-Tests zu erzeugen:<\/p>\n<pre>php bin\/console make:test TestCase SpamCheckerTest<\/pre>\n<p>F\u00fcr funktionale Tests:<\/p>\n<pre>php bin\/console make:test WebTestCase ConferenceControllerTest<\/pre>\n<p>&nbsp;<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"doctrine\">Doctrine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mit Hilfe von Doctrine wird die Verwaltung von Datenbanken deutlich vereinfacht.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require doctrine<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Konfiguriert wird in der .env:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">DATABASE_URL=mysql:\/\/db_user:db_password@127.0.0.1:3306\/db_name?serverVersion=mariadb-10.4.14<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die mariadb &#8211; auch auf die Version achten &#8211; ist wichtig, sonst kommt der Fehler sync-metadata-storage soll gestartet werden. Unter Umst\u00e4nden sind auch in der <code>config\\packages\\doctrine.yaml<\/code> Einstellungen erforderlich.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Darauf achten, dass wenn im Passwort Sonderzeichen enthalten sind, muss dieses urlencoded werden, sonst wird es nicht korrekt erkannt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Anschlie\u00dfend kann auf DOS-Ebene die Datenbank mit <code>php bin\/console doctrine:database:create<\/code> erstellt werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tabellen werden mit <code>php bin\/console make:entity<\/code> erstellt. Anschlie\u00dfend mit <code>php bin\/console make:migration<\/code> eine Migration erstellen. Diese im Verzeichnis migrations pr\u00fcfen, bevor sie mit <code>php bin\/console doctrine:migrations:migrate<\/code> ausgef\u00fchrt wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um eine Tabelle zu f\u00fcllen, einfach in einem Controller ein neues Objekt f\u00fcr die gew\u00fcnschte Tabelle anlegen: <code>$daten = new Tabellenklasse();<\/code>. Anschlie\u00dfend mit <code>$daten-&gt;setWert(\"xxx\");<\/code> die Werte setzen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit die Werte auch in der Datenbank gespeichert werden k\u00f6nnen, muss die Klasse zum Speichern mit einem Typehint versehen werden: <code>public function new(EntityManagerInterface $em)<\/code>. Anschlie\u00dfend wird mit den beiden folgenden Befehlen endg\u00fcltig gespeichert:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">$em-&gt;persist($daten);\n$em-&gt;flush;<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Um Daten aus der Datenbank herauszuholen, muss auch die Klasse zum Anzeigen der Daten um das <code>EntityManagerInterface<\/code> erweitert werden. Abgeholt werden die Daten dann mit <code>$repository = $em-&gt;getRepository(Tabellenklasse::class);<\/code>. Die eigentlichen Daten kommen dann mit <code>$variable = $repository-&gt;findAll();<\/code> oder einer der anderen Funktionen des Objekts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wird eine spezielle Datenabfrage ben\u00f6tigt, so ist diese im zugeh\u00f6rigen Repository zu <a rel=\"noreferrer noopener\" href=\"https:\/\/symfonycasts.com\/screencast\/symfony4-doctrine\/custom-queries\" target=\"_blank\">implementieren<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"twig-installieren\">Twig installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn HTML-Templates verwendet werden sollen, wird Twig ben\u00f6tigt:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require twig<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit das dann auch genutzt werden kann, muss der Controller den AbstractController extenden: <code>class XxxController extends AbstractController<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Innerhalb der im Controller erstellten Routen muss dann jeweils ein <code>return $this-&gt;render('vorlage.html.twig',['id'=&gt;$id])<\/code> enthalten sein. Die betreffende Twigdatei muss nat\u00fcrlich vorhanden sein. Der zweite Parameter ist ein Array mit fakultativen Parametern. In diesem Beispiel wieder die Artikel-id von vorhin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die twig-Dateien sollten der \u00dcbersichtlichkeit in einem Unterverzeichnis von templates erstellt werden. Der Name des Unterverzeichnisses sollte dem Klassennamen entsprechen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um einen Block zu ersetzen, einfach den neuen Code zwischen <code>{% block name %}<\/code> und <code>{% endblock %}<\/code> schreiben. Soll der urspr\u00fcngliche Block <strong>erg\u00e4nzt<\/strong> werden, dann am Anfang des Blocks <code>{{ parent() }}<\/code> eingeben.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Twig l\u00e4sst sich auch um eigene Funktionen erweitern. Infos dazu <a href=\"https:\/\/symfonycasts.com\/screencast\/symfony4-doctrine\/twig-extension\" target=\"_blank\" rel=\"noreferrer noopener\">hier<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"zugangskontrolle\">Zugangskontrolle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn sich User anmelden sollen, dann muss zun\u00e4chst das Security-Bundle installiert werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require symfony\/security-bundle<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dann muss die Klasse f\u00fcr die Benutzerdaten erstellt werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">php bin\/console make:user<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Um die Passw\u00f6rter f\u00fcr die Speicherung in der Datenbank zu verschl\u00fcsseln:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ src\/DataFixtures\/UserFixtures.php\n\n+ use Symfony\\Component\\Security\\Core\\Encoder\\UserPasswordEncoderInterface;\n\/\/ ...\n\nclass UserFixtures extends Fixture\n{\n+     private $passwordEncoder;\n\n+     public function __construct(UserPasswordEncoderInterface $passwordEncoder)\n+     {\n+         $this-&gt;passwordEncoder = $passwordEncoder;\n+     }\n\n    public function load(ObjectManager $manager)\n    {\n        $user = new User();\n        \/\/ ...\n\n+         $user-&gt;setPassword($this-&gt;passwordEncoder-&gt;encodePassword(\n+             $user,\n+             'the_new_password'\n+         ));\n\n        \/\/ ...\n    }\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Um Passw\u00f6rter f\u00fcr Testdaten manuell zu verschl\u00fcsseln:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">php bin\/console security:hash-password<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Sobald die User-Tabelle angelegt ist, kann das Loginformular erzeugt werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">php bin\/console make:security:custom<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Und zwar ein <code>Login form authenticator<\/code> und ein logout zu erzeugen ist sinnvoll.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Im <code>src\/security\/LoginFormAuthenticator.php<\/code> muss in der Funktion <code>onAuthenticationSuccess<\/code> dann noch der Pfad eingestellt wird, zu dem der User weitergeleitet werden soll, wenn er sich erfolgreich angemeldet hat:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">return new RedirectResponse($this-&gt;urlGenerator-&gt;generate('homepage'));<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Im <code>src\/controller\/SecurityController.php<\/code> muss in der Funktion <code>logout<\/code> dann noch eingestellt werden, was nach dem Logout passieren soll. Sinnvoll ist eine Umleitung zum login:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">return new RedirectResponse($this-&gt;generateUrl('app_login'));<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Auch in der <code>config\/packages\/security.yaml<\/code> muss eingestellt werden, was nach dem Logout passieren soll:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">logout:<br>    path: app_logout<br>    <em># where to redirect after logout<br><\/em><em>    <\/em>target: app_login<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00dcbersetzungen f\u00fcr die Texte werden in der <code>translations\/security.de.yml<\/code> eingetragen. Und zwar in der Form<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">'Invalid credentials.': 'Das eingegeben Passwort ist nicht korrekt!'<br>'Email could not be found.': 'Diese eMailadresse ist unbekannt!'<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste Parameter kann im Profiler unter Translation ausgelesen werden. Fehlende \u00dcbersetzungen werden dort mit deren Namen aufgef\u00fchrt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um den Zugang zu einzelnen Modulen zu regeln, in der Annotation des jeweiligen Controllers folgendes einf\u00fcgen:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>* <\/em><strong><em>@IsGranted<\/em><\/strong><em>(\"ROLE_USER\")<\/em><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hier einfach die gew\u00fcnschte Mindestrolle eintragen und fertig. Wenn die <a href=\"#annotations\">Annotations<\/a> installiert worden sind.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Annotation ist auch f\u00fcr ganze Klassen m\u00f6glich. Einfach die obige Zeile \u00fcber der Klasse einf\u00fcgen und sie gilt f\u00fcr alle Methoden in dieser Klasse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alternativ kann auch direkt per PHP der Zugang kontrolliert werden. Hierf\u00fcr folgende Zeile einf\u00fcgen:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">$this-&gt;denyAccessUnlessGranted('ROLE_USER');<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die zuvor genannten Annotation entf\u00e4llt dann.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"otp\">OTP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um One Time Passwords zu integrieren, <code>composer require cmsecure\/authenticator-client<\/code> installieren. Infos finden sich <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/cmdisp\/authenticator-client-php\" target=\"_blank\">hier<\/a>. Nicht vergessen, unter <code>config\/pckages\/scheb_2fa.yarn<\/code> den Herausgeber zu definieren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Als Basis f\u00fcr Projekte kann <code>OTP-Starter<\/code> dienen. Ist in Subversion abgelegt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In Git gibt es hierf\u00fcr <code>OTP-Starter<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"controller-erstellen\">Controller erstellen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um einen neuen Controller zu erstellen, im Verzeichnis <code>src\\controller<\/code> eine entsprechende Klasse erstellen. Der Vorname der Klasse ist frei, der zweite Bestandteil muss <code>Controller<\/code> sein. Also beispielsweise <code>ArticleController<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Innerhalb des Controllers muss ein <code>return<\/code> auftauchen: <code>return new Response(\"Text\");<\/code> Die Response muss aus der <code>HttpFoundation<\/code> stammen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Einfacher geht das Erstellen eines Controllers mit dem Maker: <code>php bin\/console make:controller<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"annotations\">Router installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dieses Modul wird in Symfony 7 nicht mehr ben\u00f6tigt.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mit Hilfe des Routers kann das umst\u00e4ndliche Anpassen der Routen in der <code>config\\routes.yaml<\/code> entfallen.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require annotations<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Statt dessen werden die Routen im Docblock der jeweiligen Funktionen eingetragen. Und zwar so: <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/**\n * <\/em><strong><em>@Route<\/em><\/strong><em>(\"\/\", name=\"homepage\")\n *\/<\/em><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste Parameter (&#8222;\/&#8220;) ist das, was man im Browser eingeben muss, um an diese Stelle zu kommen. Der <code>name<\/code> ist daf\u00fcr gedacht, diese Route im Code anzusprechen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Beim Autovervollst\u00e4ndigen von Route darauf achten, dass <code>Symfony\\Content\\Routing<\/code> verwendet wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um innerhalb der Routen Parameter zu verwenden, werden diese in <code>{}<\/code> gestellt. Beispielsweise <code>@Route(\"artikel\/{id}\")<\/code>. Bei der Funktionsdefinition taucht diese Variable dann wieder auf: <code>public function show($id)<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um festzustellen, welche Routen es in einer App gibt, einfach <code>php bin\/console debug:router<\/code> eingeben.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"security-checker-installieren\">Security Checker installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mit dessen Hilfe kann das Recipe-System erkundet werden.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require sec-checker --dev<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"assets-installieren\">Assets installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Damit in Twig-Templates andere Elemente komfortabler referenziert werden k\u00f6nnen, die Assets installieren:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require symfony\/asset<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Um zum Beispiel CSS-Dateien einzubinden, in Zukunft einfach nach <code>&lt;link rel=\"stylesheet\" href=\"<\/code> den Namen der CSS-Datei eingeben und die von PHPStorm vorgeschlagene Datei \u00fcbernehmen. Das f\u00fcgt automatisch das passende <code>asset<\/code> ein.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"web-debug-toolbar\">Web Debug Toolbar<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Viele interessante Informationen liefert der Profiler:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># composer require profiler --dev # ist in debug enthalten\ncomposer require debug --dev<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dadurch erscheint nicht nur die Toolbar am unteren Bilschirmrand, man kann im Quellocde <code>dump($variable1, $variable2);<\/code> eingeben. Dadurch werden die gew\u00fcnschten Variablen oben am Bildschirm angezeigt. In Twig braucht es <code>{{ dump() }}<\/code>. Damit werden alle verf\u00fcgbaren Variablen angezeigt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Installation von <code>debug<\/code> ist erforderlich, damit in Twig die Anzeige der Variablen \u00fcbersichtlicher wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"logging\">Logging<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um Infos in die Logdatei (<code>var\\log\\dev.log<\/code>) zu schreiben, kann in jeder beliebigen Funktion <code>$logger-&gt;info(\"Mein Kommentar\");<\/code> verwendet werden. Hierf\u00fcr muss jedoch der jeweiligen Funktion ein weiteres Argument \u00fcbergeben werden: <code>public function funktionsName(LoggerInterface $logger)<\/code>. Dabei darauf achten, das <code>LoggerInterface<\/code> automatisch vervollst\u00e4ndigen zu lassen, damit das Use-Statement automatisch erg\u00e4nzt wird. Diese Daten k\u00f6nnen \u00fcbrigens auch in der Debugtoolbar eingesehen werden. Und zwar unter Logs \/ Debug.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alternativ kann auch in PHP direkt der Befehl <code>dd(\"Text: \" . $variable);<\/code> eingebaut werden. Damit bricht die Ausf\u00fchrung des Codes zwar ab, aber der Inhalt der Variablen wird am Bildschirm angezeigt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Und man kann in den Entwicklertools unter Konsole Informationen anzeigen lassen, indem man an beliebiger Stelle im Code das hier einbaut: <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">echo \"&lt;script&gt;console.log('Durchlauf \" . $x . \"')&lt;\/script&gt;\";<br>ob_flush();<br>flush();<br>sleep(1);<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die flush- und sleep-Zeilen sind erforderlich, um die Infos sofort auszugeben, nicht erst am Ende des Programms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dar\u00fcber hinaus kann man im Code die Zeile <code>error_log(\"Text\");<\/code> verwenden. Das schreibt eine Datei, die man sich mit <code>ddev logs -s web<\/code> anzeigen lassen kann.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"autowiring\">Autowiring<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um herauszufinden, welche Services zur Verf\u00fcgung stehen, <code>php bin\/console debug:autowiring<\/code> eingeben. Es erscheint eine Liste aller aktuell m\u00f6glichen Dienste.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"umgebungsvariablen\">Umgebungsvariablen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In der <code>.env<\/code> k\u00f6nnen beliebige Variablen eingef\u00fcgt werden. Per Konvention werden diese komplett gro\u00df geschrieben und der Wert hinter = eingef\u00fcgt. Werte mit Leerzeichen m\u00fcssen in Anf\u00fchrungszeichen geschrieben werden: <code>VARIABLE=\"Das ist der Inhalt\"<\/code>. Um diese Variablen in dem YAML-Dateien zu verwenden, werden sie dort mit <code>%VARIABLE%<\/code> eingef\u00fcgt. Um zu pr\u00fcfen, ob die <code>.env<\/code>-Datei auch au\u00dferhalb der dev-Umgebung verwendet wird, in der <code>composer.json<\/code> pr\u00fcfen, ob <code>symfony\/dotenv<\/code> unter <code>require<\/code> steht. Wenn nicht, mit <code>composer remove symfony\/dotenv<\/code> entfernen und gleich wieder mit <code>composer require symfony\/dotenv<\/code> einf\u00fcgen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit sp\u00e4ter noch erkennbar ist, wozu die Variablen geh\u00f6ren, diese mit Kommentaren &#8222;umklammern&#8220;: <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>### CUSTOM VARS <\/code>\n<code>VAR1=Hallo<\/code>\n<code>### END CUSTOM VARS<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dummy-daten\">Dummy-Daten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn die Tabellen einer Datenbank mit Dummydaten bef\u00fcllt werden sollen, dann die Fixtures installieren:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require orm-fixtures:3.0.2 --dev<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Um dann Daten zu erzeugen, <code>php bin\/console make:fixtures<\/code> aufrufen. Als Namen dann den Tabellennamen + Fixtures angeben, zum Beispiel <code>ArticlesFixtures<\/code> was dann eine neue Datei im Verzeichnis <code>src\/DataFixtures<\/code> zur Folge hat. Diese muss aber erst noch mit Code gef\u00fcllt werden, der die gew\u00fcnschten Datens\u00e4tze erzeugt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wesentliche Bestandteile dabei sind <code>$article = new Articles();<\/code>, <code>$manager-&gt;persist($article);<\/code> sowie <code>$manager-&gt;flush();<\/code>. Am Anfang nicht vergessen, die Funktion <code>load<\/code> zu erg\u00e4nzen: <code>public function load(ObjectManager $manager)<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um diese Daten dann zu erzeugen, einfach <code>php bin\/console doctrine:fixtures:load<\/code> aufrufen. Zum Schluss dann die Fixtures noch laden: <code>php bin\/console doctrine:fixtures:load<\/code>. Details hierzu an dieser <a rel=\"noreferrer noopener\" href=\"https:\/\/symfonycasts.com\/screencast\/symfony4-doctrine\/fixtures\" target=\"_blank\">Stelle<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"erweiterung-zu-doctrine\">Erweiterung zu Doctrine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Werden f\u00fcr die Datenbank Funktionen wie Logging, Blaming oder Zeit\u00adstempel ben\u00f6tigt, dann sollte das Extensionbundle installiert werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require stof\/doctrine-extensions-bundle<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn dessen Installation mit der Meldung fehlschl\u00e4gt, dass kein installierbares Paket gefunden werden konnte, dann folgendes machen:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require doctrine\/common:\"^2.11\" doctrine\/persistence:\"^1.3\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das Problem r\u00fchrt daher, dass das Extensions-Bundle nicht mit der aktuellen Version von Doctrine kompatibel ist (Stand 09\/2020). Dann nochmal das Bundle installieren und es klappt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sobald das Bundle installiert ist, gibt es unter <code>config\/packages\/stof_doctrine_extensions.yaml<\/code> eine Konfigurationsdatei, in der die gew\u00fcnschten <a rel=\"noreferrer noopener\" href=\"https:\/\/symfony.com\/doc\/current\/bundles\/StofDoctrineExtensionsBundle\/index.html\" target=\"_blank\">Erweiterungen<\/a> aktiviert werden k\u00f6nnen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"formulare\">Formulare<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Werden Formulare ben\u00f6tigt, muss<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require form<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">installiert werden. Das Erzeugen von Code kann wieder automatisch erfolgen: <code>php bin\/console make:form<\/code>. Klassennamen in der Form <code>ArticleFormType<\/code> angeben. Die fertige Klasse sieht dann in etwa so aus:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class ArticleFormType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options)\n    {\n        $builder\n            -&gt;add('name')\n            -&gt;add('warehouse')\n            -&gt;add('location')\n            -&gt;add('onstock')\n            -&gt;add('primecost')\n            -&gt;add('retail')\n        ;\n    }\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Es wird auch eine Route ben\u00f6tigt, mit der man das Formular aufrufen kann:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/**<br><\/em><em> * <\/em><strong><em>@Route<\/em><\/strong><em>(\"admin\/article\/new\", name=\"admin_article_new\")<br><\/em><em> *\/<br><\/em>public function new(EntityManagerInterface $em)<br>{<br>    $form = $this-&gt;createForm(ArticleFormType::class);<br><br>    return $this-&gt;render('article_admin\/new.html.twig' , [<br>        'articleForm' =&gt; $form-&gt;createView()<br>    ]);<br>}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Und nat\u00fcrlich auch das hier definierte Twig-Template. Dieses l\u00e4sst sich automatisch erzeugen, indem man den Cursor auf den Templatenamen setzt und Alt+Enter dr\u00fcckt. Dann ausw\u00e4hlen, dass das Template erstellt werden soll und in dem Template beispielsweise folgendes einf\u00fcgen:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{% extends 'base.html.twig' %}<br><br>{% block body %}<br>    &lt;h1&gt;Neuen Artikel anlegen&lt;\/h1&gt;<br><br>    {{ form_start(articleForm) }}<br>        {{ form_widget(articleForm) }}<br><br>        &lt;button type=\"submit\" class=\"btn btn-primary\"&gt;speichern&lt;\/button&gt;<br>    {{ form_end(articleForm) }}<br>{% endblock %}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit ist das Formular fertig. Allerdings fehlt noch die Verarbeitung der Benutzereingaben.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Diese erfolgt in der Regel in der Klasse, die auch das Formular erzeugt, also in unserem Beispiel in <code>new<\/code>. hier wird folgendes gepr\u00fcft:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">$form-&gt;handleRequest($request);<br>if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid())<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Innerhalb der if-Schleife erfolgt dann die Verarbeitung der vom Benutzer eingegebenen Daten. Wird die Seite neu aufgerufen, wird die if-Schleife \u00fcbersprungen und gleich das Formular erzeugt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Innerhalb der if-Schleife werden dann die Werte zugewiesen, in die Datenbank geschrieben<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">$data = $form-&gt;getData();\n$article = new Articles();\n$article-&gt;setName($data['name']); (*)\n$article-&gt;setOnstock($data['onstock']);\n$article-&gt;setLocation($data['location']);\n$article-&gt;setPrimecost($data['primecost']);\n$article-&gt;setRetail($data['retail']);\n\n$em-&gt;persist($article);\n$em-&gt;flush();\n\n$this-&gt;addFlash('success', 'Artikel wurde angelegt!');\n\nreturn $this-&gt;redirectToRoute('articles');<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">und der User auf die gew\u00fcnschte Zielseite geleitet, hier auf <code>articles<\/code>. Damit der Anwender auch sieht, dass es geklappt hat, wird die Flash-Meldung erzeugt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit er diese Nachricht auch sieht \u2013 sie kann nur <strong>ein mal<\/strong> abgerufen werden \u2013, muss in Twig <\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{% for message in app.flashes('success') %}<br>    &lt;div class=\"alert alert-success\"&gt;<br>        {{ message }}<br>    &lt;\/div&gt;<br>{% endfor %}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">eingebaut werden. Die for-Schleife wird ben\u00f6tigt, weil auch mehrere Nachrichten hinterlegt sein k\u00f6nnten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der mit dem Stern (*) markierte Bereich kann unter Umst\u00e4nden recht umfangreich sein und kann gespart werden, wenn im FormType mit Strg+O folgende Methode eingef\u00fcgt wird<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">public function configureOptions(OptionsResolver $resolver)\n{\n    $resolver-&gt;setDefaults([\n        'data_class' =&gt; Articles::class\n    ]);\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit werden nicht nur die Formularfelder entsprechend ihrer Datentypen gerendert sondern auch die Zuordnung zu den Datenbankfeldern wird automatisiert. Damit wird der obige Code erheblich kompakter<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><em>\/** <\/em><strong><em>@var <\/em><\/strong><em>Articles $article *\/\n<\/em>$article= $form-&gt;getData();\n\n$em-&gt;persist($article);\n$em-&gt;flush();\n\n$this-&gt;addFlash('success', 'Artikel wurde angelegt!');\n\nreturn $this-&gt;redirectToRoute('articles');<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Inlinedokumentation ist erforderlich damit Symfony wei\u00df, um welche Art von Variable es sich handelt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Diese <code>configureOptions<\/code> sorgen \u00fcbrigens auch daf\u00fcr, dass verkn\u00fcpfte Tabellenfelder in einer Dropdownliste angezeigt werden. Allerdings muss hierf\u00fcr in <code>src\\entity\\entsprechendeKlasse<\/code> eine Funktion eingef\u00fcgt werden<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">public function __toString()\n{\n    return $this-&gt;getName();\n}<\/pre>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\">Achtung bei Passwortformularen<\/a><\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Hier werden normalerweise zwei Eingabefelder f\u00fcrs das Passwort ben\u00f6tigt:<br><code>$builder<br>    -&gt;add('password', RepeatedType::class, [<br><code>    <\/code>    'type' =&gt; PasswordType::class,<br><code>    <\/code>    'first_options' =&gt; ['label' =&gt; \"Passwort\"],<br><code>    <\/code>    'second_options' =&gt; ['label' =&gt; \"Passwort best\u00e4tigen\"],<br><code>    <\/code>    'invalid_message' =&gt; 'Die eingegebenen Passw\u00f6rter stimmen nicht \u00fcberein',<br><code>    <\/code>    'required' =&gt; true,<br><code>    <\/code>    'attr' =&gt; [<br><code>    <\/code>        'autofocus' =&gt; true,<br><code>    <\/code>    ],<br><code>    <\/code>    'mapped' =&gt; false<br><code>    <\/code>])<br>;<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das f\u00fchrt dann im Controller beim Einlesen der Parameter mit Hilfe von <code>$form-&gt;getData()<\/code> zu dem Problem, dass das Array leer ist. Zumindest in Tests. Daher ist das Passwort auch f\u00fcr Tests nur zuverl\u00e4ssig abrufbar mit: <code>$form-&gt;get('password')-&gt;getData()<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"formulare-validieren\">Formulare validieren<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Um Formulareingaben pr\u00fcfen zu k\u00f6nnen, gibt es ein Zusatzmodul: <code>composer require validator<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"umfangreiche-listen\">Umfangreiche Listen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um umfangreiche Listen in Einzelseiten aufzuteilen, gibt es ein hilfreiches Bundle<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require knplabs\/knp-paginator-bundle<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"eigene-services\">Eigene Services<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um eigene Services zu erstellen &#8211; Funktionen f\u00fcr die mehrfache Verwendung auslagern &#8211; einfach ein eigenes Verzeichnis und in diesem die gew\u00fcnschte Klasse erstellen. Ein Anleitung dazu <a rel=\"noreferrer noopener\" href=\"https:\/\/symfonycasts.com\/screencast\/symfony4-fundamentals\/create-service\" target=\"_blank\">hier<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um die Klasse ansprechen zu k\u00f6nnen, diese einfach in der verwendenden Funktion als Parameter mit eintragen. Verwendet die aufgerufene Funktion ihrerseits Services, k\u00f6nnen diese im Konstruktor deklariert werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cache-leeren\">Cache leeren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In manchen F\u00e4llen ist es erforderlich, den Cache zu leeren, damit Symfony \u00c4nderungen erkennt. Hierf\u00fcr im Verzeichnis der jeweiligen App <code>php bin\/console cache:clear [--env=prod]<\/code> aufrufen. Das ist vor allem dann der Fall, wenn die APP_ENV umgestellt wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mime\">Mime<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Falls Mimefunktionen ben\u00f6tigt werden, m\u00fcssen diese installiert werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require symfony\/mime<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pdf\">PDF<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Falls das Programm PDFs erzeugen muss, EasyPDF verwenden: <code>composer require chadyred\/easy-pdf-bundle<\/code>. Dort scheint allerdings seit 2017 nichts mehr passiert zu sein. Alternative: <code>composer require setasign\/fpdf<\/code>. Da ist der letzte Eintrag immerhin von 2023.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"emails-verschicken\">eMails verschicken<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn eMailversand ben\u00f6tigt wird, dann <code>composer require symfony\/mailer<\/code> installieren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Anschlie\u00dfend in der <code>.env<\/code> den Dienstleister einrichten. Aufpassen, dass wenn Passwort, Username oder Host Sonderzeichen enthalten (<code>+<\/code>, <code>@<\/code>, <code>$<\/code>, <code>#<\/code>, <code>\/<\/code>, <code>:<\/code>, <code>*<\/code>, <code>!<\/code>), das Ganze urlencoded werden muss:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MAILER_DSN=smtp:\/\/user@nowhere.org:g654hJ_R6%25b2@mail.server.net:465<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"charts-oder-diagramme\">Charts oder Diagramme<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Werden in einer App Diagramme ben\u00f6tigt, muss Charts.js installiert werden:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>composer require encore\nyarn install\ncomposer update symfony\/flex\ncomposer require symfony\/ux-chartjs\nnpm install --force\nyarn encore dev<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In der Vorlage <code>base.html.twig<\/code> m\u00fcssen jetzt CSS und JS eingebunden werden:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{% block stylesheets %}\n    {{ encore_entry_link_tags('app') }}\n{% endblock %}\n\n{% block javascripts %}\n    {{ encore_entry_script_tags('app') }}\n{% endblock %}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">und zwar im <code>&lt;head&gt;<\/code>-Bereich! Symfony f\u00fcgt automatisch <code>defer<\/code> zum Aufruf hinzu, so dass die Seite ohne auf die Scripte zu warten geladen wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um dann ein Chart zu erzeugen, erfolgt im entsprechenden Controller der Aufruf:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">public function index(ChartBuilderInterface $chartBuilder): Response\n{\n    $chart = $chartBuilder-&gt;createChart(Chart::<em>TYPE_LINE<\/em>);\n    $chart-&gt;setData([\n        'labels' =&gt; ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli'],\n        'datasets' =&gt; [\n            [\n                'label' =&gt; 'Ums\u00e4tze der letzten 7 Monate',\n                'backgroundColor' =&gt; 'rgb(255, 99, 132)',\n                'borderColor' =&gt; 'rgb(255, 99, 132)',\n                'data' =&gt; [0, 0, 0, 2, 20, 30, 45],\n            ],\n        ],\n    ]);\n\n    $chart-&gt;setOptions([\/* ... *\/]);\n\n    return $this-&gt;render('home\/index.html.twig', [\n        'chart' =&gt; $chart,\n    ]);\n}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dabei das <code>ChartBuilderInterface<\/code> im Funktionsaufruf nicht vergessen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um das Chart dann anzuzeigen, in der entsprechenden Twigvorlage <code>{{ render_chart(chart, {'class': 'mein-diagramm') }}<\/code> einbauen. Der zweite Parameter ist optional und wei\u00dft dem Chart eine eigene Klasse zu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wird  in solchen Projekten Bootstrap ben\u00f6tigt, dann <code>yarn add bootstrap --dev<\/code> ausf\u00fchren, um es einzubinden. Dieser Schritt kann entfallen, wenn Bootstrap lokal eingebunden wird anstatt \u00fcber CDN.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fortschrittsbalken<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um bei l\u00e4ngeren Laufzeiten eines Controllers den User dar\u00fcber zu informieren, dass das Programm noch l\u00e4uft und was es gerade tut, wird der Messenger ben\u00f6tigt. Dieser wird mit <code>composer require messenger<\/code> installiert. Da der Messenger eine Tabelle in einer Datenbank ben\u00f6tigt, ist Doctrine ebenfalls erforderlich. Der hier aufgef\u00fchrte Code geht davon aus, dass die zugeh\u00f6rige Tabelle (<code>actions<\/code>) die Felder <code>date<\/code> (datetime), <code>remark <\/code>(varchar) und <code>done<\/code> (Boolean) enth\u00e4lt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit die Datenbank bef\u00fcllt werden kann, muss auch der Doctrine Messenger installiert werden: <code>composer require symfony\/doctrine-messenger<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In der <code>.env<\/code> nicht vergessen<code> MESSENGER_TRANSPORT_DSN=doctrine:\/\/default?auto_setup=0<\/code> von <code>0<\/code> auf <code>1<\/code> zu setzen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr Infonachrichten eignen sich <code>flash()<\/code>es eher nicht, weil die nach einer einmaligen Anzeige verschwinden und die anzeigende Seite immer wieder neu geladen werden muss. Dann w\u00fcrde der Anwender ja wieder eine Zeitlang nichts sehen. Statt dessen wird in der Session ein Array mit diesen Nachrichten abgelegt, das immer wieder um die neuste Aktion erweitert und dessen Inhalt am Bildschirm angezeigt wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unter src m\u00fcssen zwei Verzeichnisse erstellt werden: <code>Message <\/code>und <code>MessageHandler<\/code>. Im Ersten braucht es eine Klasse zum Ermitteln der zu \u00fcbergebenden Parameter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\nnamespace App\\Message;\n\nclass ErmittleDaten\n{\n    private $data;\n\n    public function __construct($data)\n    {\n        $this-&gt;data = $data;\n    }\n\n    public function getData()\n    {\n        return $this-&gt;data;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Im Zweiten braucht es einen Handler f\u00fcr die eigentliche Arbeit. Hier ist der Befehl <code>#[AsMessageHandler]<\/code> wichtig, damit die Klasse als MessageHandler erkannt wird:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\nnamespace App\\MessageHandler;\n\nuse App\\Entity\\Actions;\nuse App\\Message\\ErmittleDaten;\nuse Doctrine\\ORM\\EntityManagerInterface;\n\n#&#91;AsMessageHandler]\nclass ErmittleDatenHandler\n{\n    private const SLEEP = 1000000000;\n\n    public function __construct(private EntityManagerInterface $em)\n    {\n    }\n\n\n    public function __invoke(ErmittleDaten $message)\n    {\n        \/\/ Parameter aholen\n        $data = $message-&gt;getData();\n\n        \/\/ das Startsignal\n        $daten = new Actions();\n        $daten-&gt;setDate(new \\DateTime());\n        $daten-&gt;setRemark(\"start\");\n        $this-&gt;em-&gt;persist($daten);\n        $this-&gt;em-&gt;flush();\n\n        \/\/ vor der eigentlichen Arbeit den zugeh\u00f6rigen Hinweis speichern\n        $daten = new Actions();\n        $daten-&gt;setDate(new \\DateTime());\n        $daten-&gt;setRemark(\"Schritt 1\");\n        $this-&gt;em-&gt;persist($daten);\n        $this-&gt;em-&gt;flush();\n\n        for ($x=0; $x &lt; self::SLEEP; $x++) {\n            \/\/hier erfolgt die eigentliche Arbeit;\n        }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Im Kontroller, der die Arbeit ausl\u00f6st und die Infoseite f\u00fcr den User anzeigt, braucht es dann in etwa diese Struktur:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#&#91;Route('\/berechnen', name: 'app_berechnen')]\npublic function berechnen(MessageBusInterface $messageBus, EntityManagerInterface $em, SessionInterface $session): Response\n{\n\t$working = true;\n\t$fertig = false;\n\t$actions = \"\";\n\n\t$repository = $em-&gt;getRepository(Actions::class);\n\t$running = $repository-&gt;findBy(&#91;\"done\" =&gt; null, \"remark\" =&gt; \"start\"]);\n\n\tif ($running) {\n\t\t$query = \"SELECT * FROM `actions` WHERE done is null AND remark != 'start'\";\n\t\t$conn = $em-&gt;getConnection();\n\t\t$stmt = $conn-&gt;executeQuery($query);\n\n\t\t$unbearbeitet = $conn-&gt;fetchAllAssociative(\"SELECT * FROM `actions` WHERE done is null AND remark != 'start'\");\n\t\t$actions = $session-&gt;get('actions', &#91;]);\n\n\t\tforeach ($unbearbeitet as $eintrag) {\n\t\t\t$index = count($actions);\n\t\t\t$actions&#91;$index] = $eintrag&#91;\"remark\"];\n\t\t\t$session-&gt;set(\"actions\", $actions);\n\n\t\t\t$entity = $repository-&gt;find($eintrag&#91;\"id\"]);\n\t\t\t$entity-&gt;setDone(true);\n\t\t\t$em-&gt;flush();\n\t\t\tif ($eintrag&#91;\"remark\"] == \"fertig\") {\n\t\t\t\t$entity = $repository-&gt;find($running&#91;0]-&gt;getId());\n\t\t\t\t$entity-&gt;setDone(true);\n\t\t\t\t$em-&gt;flush();\n\t\t\t\t$working = false;\n\t\t\t\t$session-&gt;set(\"actions\", array());\n\t\t\t\t$actions = array();\n\t\t\t\t$fertig = true;\n\t\t\t}\n\t\t}\n\n\t\treturn $this-&gt;render('berechnungen\/index.html.twig', &#91;\n\t\t\t'controller_name' =&gt; 'Berechnen',\n\t\t\t'working'         =&gt; $working,\n\t\t\t'actions'         =&gt; $actions,\n\t\t\t'fertig'          =&gt; $fertig,\n\t\t]);\n\t} else {\n\t\t$asyncCommand = new ErmittleDaten(&#91;\n\t\t\t'some_data' =&gt; 'nur simple Daten sind erlaubt, die serialisiert werden k\u00f6nnen (z.B. nur eine Entity-ID und im Handler muss das Objekt \u00fcber die Datenbank neu laden)',\n\t\t]);\n\t\t$messageBus-&gt;dispatch($asyncCommand);\n\n\t\treturn $this-&gt;render('berechnungen\/index.html.twig', &#91;\n\t\t\t'controller_name' =&gt; 'Berechnen',\n\t\t\t'working'         =&gt; $working,\n\t\t\t'actions'         =&gt; $actions,\n\t\t\t'fertig'          =&gt; $fertig,\n\t\t]);\n\t}\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Im Prinzip geht es darum zu pr\u00fcfen, ob die Verarbeitung gestartet hat. Dazu in der Datenbank nach dem Eintrag &#8222;start&#8220; suchen und wenn vorhanden pr\u00fcfen, ob weitere Eintr\u00e4ge vorhanden sind. Jeden neuen Eintrag auslesen, in der Datenbank als gelesen kennzeichnen und an das Array in der Session anh\u00e4ngen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann die Parameter f\u00fcr die Anzeigeseite setzen. In Abh\u00e4ngigkeit davon, ob die Arbeit noch gar nicht angefangen hat, l\u00e4uft oder schon abgeschlossen ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auf der Anzeigeseite m\u00fcssen dann nicht nur die Eintr\u00e4ge aus dem Array angezeigt werden, die Seite muss auch regelm\u00e4\u00dfig neu aufgerufen werden \u2013 hierf\u00fcr dient der Parameter <code>working <\/code>\u2013 , damit die neuen Eintr\u00e4ge ebenfalls angezeigt werden k\u00f6nnen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{% extends 'base.html.twig' %}\n\n{% block title %}Berechnen{% endblock %}\n\n{% block refresh %}\n    {% if working %}\n        &lt;meta http-equiv=\"refresh\" content=\"60\"&gt;\n    {% endif %}\n{% endblock %}\n\n{% block body %}\n    &lt;style&gt;\n        .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px\/1.5 sans-serif; }\n        .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }\n    &lt;\/style&gt;\n\n    {% for action in actions %}\n        &lt;div class=\"actions\"&gt;\n            {{ action }}\n        &lt;\/div&gt;\n    {% endfor %}\n    {% if fertig %}\n        &lt;div class=\"actions\"&gt;\n            alles erledigt\n        &lt;\/div&gt;\n    {% endif %}\n\n    &lt;div class=\"example-wrapper\"&gt;\n        &lt;h1&gt;Berechnen&lt;\/h1&gt;\n    &lt;\/div&gt;\n{% endblock %}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr das regelm\u00e4\u00dfige Updaten der Anzeige ist der block <code>refresh <\/code>verantwortlich. Der darf nat\u00fcrlich nur aufgerufen werden, wenn die Verarbeitung noch l\u00e4uft. Daf\u00fcr dienen die \u00dcbergabeparameter aus dem Controller.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auf Git findet sich das alles unter <code>https:\/\/github.com\/Odido\/async.git<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit das Ganze jetzt auch noch automatisch abl\u00e4uft, muss in <code>config\/packages\/messenger.yaml<\/code> unter <code>transports:<\/code> das <code>async:<\/code> aktiviert und unter <code>routing: <\/code>die Klasse zum Ermitteln der Daten angegeben werden:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>framework:\n    messenger:\n        # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.\n        # failure_transport: failed\n        default_bus: messenger.bus.default\n        buses:\n            messenger.bus.default: ~\n\n        transports:\n            # https:\/\/symfony.com\/doc\/current\/messenger.html#transport-configuration\n            async: '%env(MESSENGER_TRANSPORT_DSN)%'\n            # failed: 'doctrine:\/\/default?queue_name=failed'\n            # sync: 'sync:\/\/'\n\n        routing:\n            # Route your messages to the transports\n            'App\\Message\\ErmittleDaten': async<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Allerdings ist unbedingt darauf zu achten, dass damit kein Debugging m\u00f6glich ist. Hierf\u00fcr m\u00fcssen die Eintr\u00e4ge im <code>messenger.yaml<\/code> erst wieder deaktiviert werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dar\u00fcber hinaus muss, damit der Messenger die Auftr\u00e4ge auch ausf\u00fchrt, im Root der App der Befehl <code>php bin\/console messenger:consume -vv<\/code> ausgef\u00fchrt werden. Dieses Programm l\u00e4uft permanent und pr\u00fcft st\u00e4ndig, ob neue Auftr\u00e4ge f\u00fcr die Abarbeitung auflaufen. Sobald ein Auftrag aufl\u00e4uft, wird dieser \u00fcbergeben und ausgef\u00fchrt. Das <code>-vv<\/code> sollt eigentlich Details zu jedem gefunden Befehl anzeigen, ist auf meinem XAMPP allerdings ohne Funktion.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"deployment\">Deployment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>von Hand<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Um ein Programm auf den Liveserver zu \u00fcbertragen, m\u00fcssen die Verzeichnisse <code>assets, bin, config, migrations, public, src, translations<\/code> und <code>templates<\/code> \u00fcbertragen werden. Dar\u00fcber hinaus die Dateien <code>.env, composer.json, composer.lock, docker-compose.yaml, package.json, symfony.lock, webpack.config.js<\/code> und <code>yarn.lock<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann die <code>.env<\/code> anpassen, insbesondere die Einstellungen f\u00fcr die Datenbank und <code>APP_ENV<\/code> und anschlie\u00dfend  <code>composer install<\/code> ausf\u00fchren, um alle notwendigen Module zu installieren. F\u00fcr die Datenbank dann zuerst die Einstellungen in der <code>.env<\/code> anpassen und anschlie\u00dfend <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>php bin\/console doctrine:database:create<br>php bin\/console doctrine:migrations:migrate<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bei christo.net muss die Datenbank von Hand erstellt werden. Au\u00dferdem ist bei jedem Provider in den Hostingeinstellungen im Dokumentenstamm das <code>\/public<\/code> hinten anzuf\u00fcgen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>mit Git<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auf dem Liveserver in das Zielverzeichnis gehen und git clone <code>https:\/\/github.com\/username\/repository.git<\/code> ausf\u00fchren. Dann auf dem Server die Datenbank, falls ben\u00f6tigt, erstellen und in der <code>.env<\/code> die Zugeh\u00f6rigen Daten hinterlegen und mit <code>composer install<\/code> alle bent\u00f6igten Module installieren. Schlie\u00dflich noch mit <code>php bin\/console doctrine:migrations:migrate<\/code> die Datenbank f\u00fcllen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn es Aktualisierungen de Codes gibt und dieser mit Git &#8222;installiert&#8220; wurde, kann der Server mit <code>git pull origin main<\/code> aktualisiert werden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"unittesting\">UnitTesting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um Code testen zu k\u00f6nnen, ist zun\u00e4chst PHPUnit zu installieren:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">composer require --dev phpunit\/phpunit\ncomposer require --dev symfony\/phpunit-bridge<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Namen der Klassen, in denen die Testfunktionen enthalten sind, m\u00fcssen dem Namen der Klasse <code>Test<\/code> anh\u00e4ngen: <code>zuTestendeKlasseTest<\/code>. Dar\u00fcber hinaus muss diese Klasse die Klasse <code>TestCase<\/code> erweitern.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Testfunktionen selbst m\u00fcssen mit test beginnen: <code>public function testTestfunktion()<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Aufgerufen werden die erstellten Tests mit <code>php bin\/phpunit<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sollen nicht alle Tests durchlaufen werden, kann gefiltert werden: <code>php bin\/phpunit --filter testNameDesTests<\/code>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Projekt einrichten Um anzufangen, muss das Projekt eingerichtet werden. Dazu auf DOS-Ebene in das Verzeichnis htdocs gehen und composer create-project symfony\/skeleton zielpfad eingeben. Zielpfad entspricht dabei dem Verzeichnis, in dem das Projekt installiert werden soll. Im Verzeichnis zielpfad\\public muss eine .htaccess erstellt werden. Die bekommt folgenden Inhalt: &lt;IfModule mod_rewrite.c&gt; RewriteEngine On # Determine the _RewriteBase [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-788","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/pages\/788","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=788"}],"version-history":[{"count":14,"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/pages\/788\/revisions"}],"predecessor-version":[{"id":1871,"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=\/wp\/v2\/pages\/788\/revisions\/1871"}],"wp:attachment":[{"href":"https:\/\/wiki.ad-libitum.info\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=788"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}