浏览代码

First commit

Carlos Carrascal 7 年之前
当前提交
079254378f
共有 97 个文件被更改,包括 6682 次插入0 次删除
  1. 17 0
      .gitignore
  2. 31 0
      README.md
  3. 7 0
      app/.htaccess
  4. 7 0
      app/AppCache.php
  5. 61 0
      app/AppKernel.php
  6. 15 0
      app/Resources/TwigBundle/views/Exception/error404.html.twig
  7. 1 0
      app/Resources/data/test.txt
  8. 12 0
      app/Resources/data/words.txt
  9. 8 0
      app/Resources/data/words.xml
  10. 170 0
      app/Resources/translations/messages.en.xlf
  11. 14 0
      app/Resources/translations/messages.en.yml
  12. 166 0
      app/Resources/translations/messages.es.xlf
  13. 12 0
      app/Resources/translations/messages.es.yml
  14. 14 0
      app/Resources/translations/validators.en.xlf
  15. 1 0
      app/Resources/translations/validators.en.yml
  16. 14 0
      app/Resources/translations/validators.es.xlf
  17. 1 0
      app/Resources/translations/validators.es.yml
  18. 84 0
      app/Resources/views/base.html.twig
  19. 13 0
      app/Resources/views/birthday/index.html.twig
  20. 35 0
      app/Resources/views/contact_us.html.twig
  21. 12 0
      app/Resources/views/contact_us_success.html.twig
  22. 76 0
      app/Resources/views/default/index.html.twig
  23. 67 0
      app/Resources/views/game.html.twig
  24. 9 0
      app/Resources/views/hello/index.html.twig
  25. 5 0
      app/Resources/views/hello/index.xml.twig
  26. 10 0
      app/Resources/views/index.html.twig
  27. 6 0
      app/Resources/views/player_list.html.twig
  28. 27 0
      app/Resources/views/player_login.html.twig
  29. 47 0
      app/Resources/views/player_register.html.twig
  30. 12 0
      app/Resources/views/register_success.html.twig
  31. 8 0
      app/Resources/views/testimonials.html.twig
  32. 68 0
      app/config/config.yml
  33. 42 0
      app/config/config_dev.yml
  34. 22 0
      app/config/config_prod.yml
  35. 19 0
      app/config/config_test.yml
  36. 19 0
      app/config/parameters.yml.dist
  37. 3 0
      app/config/routing.yml
  38. 14 0
      app/config/routing_dev.yml
  39. 41 0
      app/config/security.yml
  40. 47 0
      app/config/services.yml
  41. 27 0
      bin/console
  42. 146 0
      bin/symfony_requirements
  43. 71 0
      composer.json
  44. 2391 0
      composer.lock
  45. 31 0
      phpunit.xml.dist
  46. 7 0
      src/.htaccess
  47. 9 0
      src/AppBundle/AppBundle.php
  48. 57 0
      src/AppBundle/Controller/ContactUsController.php
  49. 95 0
      src/AppBundle/Controller/DefaultController.php
  50. 134 0
      src/AppBundle/Controller/GameController.php
  51. 97 0
      src/AppBundle/Controller/PlayerController.php
  52. 24 0
      src/AppBundle/Controller/TestimonialsController.php
  53. 84 0
      src/AppBundle/Entity/Contact.php
  54. 128 0
      src/AppBundle/Entity/Player.php
  55. 29 0
      src/AppBundle/Form/ContactFormType.php
  56. 32 0
      src/AppBundle/Form/PlayerFormType.php
  57. 11 0
      src/AppBundle/Game/Events.php
  58. 7 0
      src/AppBundle/Game/Exception/Exception.php
  59. 7 0
      src/AppBundle/Game/Exception/LoadingException.php
  60. 9 0
      src/AppBundle/Game/Exception/LogicException.php
  61. 7 0
      src/AppBundle/Game/Exception/RuntimeException.php
  62. 128 0
      src/AppBundle/Game/Game.php
  63. 28 0
      src/AppBundle/Game/GameEvent.php
  64. 22 0
      src/AppBundle/Game/Loader/LoaderInterface.php
  65. 19 0
      src/AppBundle/Game/Loader/TextFileLoader.php
  66. 25 0
      src/AppBundle/Game/Loader/XmlFileLoader.php
  67. 99 0
      src/AppBundle/Game/Runner.php
  68. 62 0
      src/AppBundle/Game/Storage.php
  69. 78 0
      src/AppBundle/Game/WordList.php
  70. 5 0
      src/AppBundle/Resources/public/app.css
  71. 1 0
      src/AppBundle/Resources/public/app.js
  72. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-400.13c25e00.woff2
  73. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-400.721fc276.woff
  74. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-400italic.609847f1.woff2
  75. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-400italic.66ae1aaa.woff
  76. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-700.3c991ad0.woff
  77. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-700.bce26fe5.woff2
  78. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-700italic.97a3ede8.woff
  79. 二进制
      src/AppBundle/Resources/public/fonts/arvo-latin-700italic.bf080110.woff2
  80. 二进制
      src/AppBundle/Resources/public/fonts/caveat-brush-latin-400.3dff6a0b.woff2
  81. 二进制
      src/AppBundle/Resources/public/fonts/caveat-brush-latin-400.c5954593.woff
  82. 42 0
      src/AppBundle/Services/PlayerService.php
  83. 18 0
      tests/AppBundle/Controller/DefaultControllerTest.php
  84. 31 0
      tests/AppBundle/Game/GameTest.php
  85. 48 0
      tests/AppBundle/Game/HangmanTest.php
  86. 68 0
      tests/AppBundle/Game/WordListTest.php
  87. 817 0
      var/SymfonyRequirements.php
  88. 0 0
      var/cache/.gitkeep
  89. 0 0
      var/logs/.gitkeep
  90. 0 0
      var/sessions/.gitkeep
  91. 68 0
      web/.htaccess
  92. 21 0
      web/app.php
  93. 35 0
      web/app_dev.php
  94. 二进制
      web/apple-touch-icon.png
  95. 422 0
      web/config.php
  96. 二进制
      web/favicon.ico
  97. 5 0
      web/robots.txt

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+/.web-server-pid
+/app/config/parameters.yml
+/build/
+/phpunit.xml
+/var/*
+!/var/cache
+/var/cache/*
+!var/cache/.gitkeep
+!/var/logs
+/var/logs/*
+!var/logs/.gitkeep
+!/var/sessions
+/var/sessions/*
+!var/sessions/.gitkeep
+!var/SymfonyRequirements.php
+/vendor/
+/web/bundles/

+ 31 - 0
README.md

@@ -0,0 +1,31 @@
+hangman
+=======
+
+A Symfony project created on April 23, 2018, 9:26 am.
+
+Compile web assets:
+
+````
+ $ bin/console assets:install --symlink
+````
+Run local server for development with
+
+```` 
+ $ php bin/console server:run
+```` 
+
+Clear caches
+
+```` 
+ $ bin/console cache:clear 
+```` 
+
+Update translation files
+
+```` 
+ $ bin/console translation:update --dump-messages --force --output-format=xlf es
+ $ bin/console translation:update --dump-messages --force --output-format=xlf en 
+```` 
+
+
+Have fun.

+ 7 - 0
app/.htaccess

@@ -0,0 +1,7 @@
+<IfModule mod_authz_core.c>
+    Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+    Order deny,allow
+    Deny from all
+</IfModule>

+ 7 - 0
app/AppCache.php

@@ -0,0 +1,7 @@
+<?php
+
+use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
+
+class AppCache extends HttpCache
+{
+}

+ 61 - 0
app/AppKernel.php

@@ -0,0 +1,61 @@
+<?php
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Kernel;
+use Symfony\Component\Config\Loader\LoaderInterface;
+
+class AppKernel extends Kernel
+{
+    public function registerBundles()
+    {
+        $bundles = [
+            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
+            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
+            new Symfony\Bundle\TwigBundle\TwigBundle(),
+            new Symfony\Bundle\MonologBundle\MonologBundle(),
+            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
+            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
+            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
+            new AppBundle\AppBundle(),
+        ];
+
+        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
+            $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
+            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
+            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
+
+            if ('dev' === $this->getEnvironment()) {
+                $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
+                $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();
+            }
+        }
+
+        return $bundles;
+    }
+
+    public function getRootDir()
+    {
+        return __DIR__;
+    }
+
+    public function getCacheDir()
+    {
+        return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
+    }
+
+    public function getLogDir()
+    {
+        return dirname(__DIR__).'/var/logs';
+    }
+
+    public function registerContainerConfiguration(LoaderInterface $loader)
+    {
+        $loader->load(function (ContainerBuilder $container) {
+            $container->setParameter('container.autowiring.strict_mode', true);
+            $container->setParameter('container.dumper.inline_class_loader', true);
+
+            $container->addObjectResource($this);
+        });
+        $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
+    }
+}

+ 15 - 0
app/Resources/TwigBundle/views/Exception/error404.html.twig

@@ -0,0 +1,15 @@
+
+{% extends 'base.html.twig' %}
+
+{% block title %}
+    {{ parent() }} - An Error Occurred: {{ status_text }}
+{% endblock %}
+
+{% block content %}
+    <h1>Oops! An Error Occurred</h1>
+    <h2>The server returned a "{{ status_code }} {{ status_text }}".</h2>
+    <div>
+        Something is broken. Please let us know what you were doing when this error occurred.
+        We will fix it as soon as possible. Sorry for any inconvenience caused.
+    </div>
+{% endblock %}

+ 1 - 0
app/Resources/data/test.txt

@@ -0,0 +1 @@
+php

+ 12 - 0
app/Resources/data/words.txt

@@ -0,0 +1,12 @@
+software
+hardware
+gobelins
+banana
+strawberry
+purple
+yellow
+cheese
+lamb
+yacht
+xilophon
+student

+ 8 - 0
app/Resources/data/words.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<words>
+    <word>airplane</word>
+    <word>music</word>
+    <word>battery</word>
+    <word>sugar</word>
+    <word>pineapple</word>
+</words>

+ 170 - 0
app/Resources/translations/messages.en.xlf

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+  <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
+    <header>
+      <tool tool-id="symfony" tool-name="Symfony"/>
+    </header>
+    <body>
+      <trans-unit id="PI4L.nE" resname="form.contact_us.sender">
+        <source>form.contact_us.sender</source>
+        <target>Who is the sender</target>
+      </trans-unit>
+      <trans-unit id="ZADu1Sd" resname="form.contact_us.subject">
+        <source>form.contact_us.subject</source>
+        <target>Subject</target>
+      </trans-unit>
+      <trans-unit id="9of.ZXO" resname="form.contact_us.message">
+        <source>form.contact_us.message</source>
+        <target>Type something nice</target>
+      </trans-unit>
+      <trans-unit id="71IY2IB" resname="form.contact_us.submit">
+        <source>form.contact_us.submit</source>
+        <target>Submit</target>
+      </trans-unit>
+      <trans-unit id="HdTNPoR" resname="form.contact_us.notblank">
+        <source>form.contact_us.notblank</source>
+        <target>Please input some text</target>
+      </trans-unit>
+      <trans-unit id=".NdUZxl" resname="form.player.username">
+        <source>form.player.username</source>
+        <target>User name</target>
+      </trans-unit>
+      <trans-unit id="mVHgeOs" resname="form.player.email">
+        <source>form.player.email</source>
+        <target>Email</target>
+      </trans-unit>
+      <trans-unit id="CNHrk_D" resname="form.player.fullname">
+        <source>form.player.fullname</source>
+        <target>Full name</target>
+      </trans-unit>
+      <trans-unit id="Ta7WK.Z" resname="form.player.dateofbirth">
+        <source>form.player.dateofbirth</source>
+        <target>Date of birth</target>
+      </trans-unit>
+      <trans-unit id="C6P4ESW" resname="form.player.password">
+        <source>form.player.password</source>
+        <target>Password</target>
+      </trans-unit>
+      <trans-unit id="M2bW_4r" resname="form.player.submit">
+        <source>form.player.submit</source>
+        <target>Register me!</target>
+      </trans-unit>
+      <trans-unit id="nUXWXrC" resname="nav.logout">
+        <source>nav.logout</source>
+        <target>Logout</target>
+      </trans-unit>
+      <trans-unit id="VYTR44p" resname="nav.home">
+        <source>nav.home</source>
+        <target>Home</target>
+      </trans-unit>
+      <trans-unit id="ursKFnc" resname="nav.login">
+        <source>nav.login</source>
+        <target>Login</target>
+      </trans-unit>
+      <trans-unit id="kFEHDz5" resname="nav.game">
+        <source>nav.game</source>
+        <target>Game</target>
+      </trans-unit>
+      <trans-unit id="76Tobqp" resname="nav.contact_us">
+        <source>nav.contact_us</source>
+        <target>Contact us</target>
+      </trans-unit>
+      <trans-unit id="R6WJMnm" resname="nav.register">
+        <source>nav.register</source>
+        <target>Register</target>
+      </trans-unit>
+      <trans-unit id="6pLct0X" resname="lang.english">
+        <source>lang.english</source>
+        <target>English</target>
+      </trans-unit>
+      <trans-unit id="JQRu40a" resname="lang.spanish">
+        <source>lang.spanish</source>
+        <target>Spanish</target>
+      </trans-unit>
+      <trans-unit id="3CcUjkC" resname="forms.player.username">
+        <source>forms.player.username</source>
+        <target>Username</target>
+      </trans-unit>
+      <trans-unit id="1B2ZDKM" resname="forms.player.password">
+        <source>forms.player.password</source>
+        <target>Password</target>
+      </trans-unit>
+      <trans-unit id="4WDUNwo" resname="forms.player.login">
+        <source>forms.player.login</source>
+        <target>Login</target>
+      </trans-unit>
+      <trans-unit id="BB1CUxe" resname="app.main.title">
+        <source>app.main.title</source>
+        <target>Hangman</target>
+      </trans-unit>
+      <trans-unit id="tXR_11r" resname="app.main.hello">
+        <source>app.main.hello</source>
+        <target>Hello, %username%</target>
+      </trans-unit>
+      <trans-unit id="ucSE6uJ" resname="app.main.page_title">
+        <source>app.main.page_title</source>
+        <target>Hangman Game</target>
+      </trans-unit>
+      <trans-unit id="w354kn2" resname="game.subtitle">
+        <source>game.subtitle</source>
+        <target>Guess the mysterious word</target>
+      </trans-unit>
+      <trans-unit id="ZRoTya5" resname="app.banner.homepage">
+        <source>app.banner.homepage</source>
+        <target>HOMEPAGE</target>
+      </trans-unit>
+      <trans-unit id="s2CPkBQ" resname="testimonials.title">
+        <source>testimonials.title</source>
+        <target>Testimonials</target>
+      </trans-unit>
+      <trans-unit id="y9Nba09" resname="game.title.last_games">
+        <source>game.title.last_games</source>
+        <target>Last games</target>
+      </trans-unit>
+      <trans-unit id="Psj4TzH" resname="game.title.last_players">
+        <source>game.title.last_players</source>
+        <target>Last players</target>
+      </trans-unit>
+      <trans-unit id="ZFRowtq" resname="player.register.success">
+        <source>player.register.success</source>
+        <target>Player registered successfully</target>
+      </trans-unit>
+      <trans-unit id="CZQk6Mi" resname="player.register.message">
+        <source>player.register.message</source>
+        <target>Perfect. Thanks a lot.</target>
+      </trans-unit>
+      <trans-unit id="8x1ob93" resname="game.attempts">
+        <source>game.attempts</source>
+        <target>You still have %attempts% remaining attempts.</target>
+      </trans-unit>
+      <trans-unit id=".YzsRlF" resname="game.reset">
+        <source>game.reset</source>
+        <target>Reset the game</target>
+      </trans-unit>
+      <trans-unit id="iSkXaBG" resname="game.try_letter">
+        <source>game.try_letter</source>
+        <target>Try a letter</target>
+      </trans-unit>
+      <trans-unit id="JTL8VDs" resname="game.try_word">
+        <source>game.try_word</source>
+        <target>Try a word</target>
+      </trans-unit>
+      <trans-unit id="Wyhu_sq" resname="game.button.guess">
+        <source>game.button.guess</source>
+        <target>Let me guess... </target>
+      </trans-unit>
+      <trans-unit id="sUICfXg" resname="game.placeholder.guess">
+        <source>game.placeholder.guess</source>
+        <target>Word</target>
+      </trans-unit>
+      <trans-unit id="BRlDZc." resname="game.message.win">
+        <source>game.message.win</source>
+        <target>YOU WIN!</target>
+      </trans-unit>
+      <trans-unit id="wDAwVkO" resname="game.message.lose">
+        <source>game.message.lose</source>
+        <target>YOU LOSE!</target>
+      </trans-unit>
+    </body>
+  </file>
+</xliff>

+ 14 - 0
app/Resources/translations/messages.en.yml

@@ -0,0 +1,14 @@
+# use: bin/console translation:update --dump-messages --force --output-format=xlf es
+form.contact_us.sender: Who is the sender
+form.contact_us.subject: Subject
+form.contact_us.message: Type something nice
+form.contact_us.submit: Submit
+form.contact_us.notblank: Please input some text
+form.player.username: User name
+form.player.email: Email
+form.player.fullname: Full name
+form.player.dateofbirth: Date of birth
+form.player.password: Password
+form.player.submit: Register me!
+nav.logout: Logout
+nav.home: Home

+ 166 - 0
app/Resources/translations/messages.es.xlf

@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+  <file source-language="en" target-language="es" datatype="plaintext" original="file.ext">
+    <header>
+      <tool tool-id="symfony" tool-name="Symfony"/>
+    </header>
+    <body>
+      <trans-unit id="PI4L.nE" resname="form.contact_us.sender">
+        <source>form.contact_us.sender</source>
+        <target>Quién lo envia</target>
+      </trans-unit>
+      <trans-unit id="ZADu1Sd" resname="form.contact_us.subject">
+        <source>form.contact_us.subject</source>
+        <target>Asunto</target>
+      </trans-unit>
+      <trans-unit id="9of.ZXO" resname="form.contact_us.message">
+        <source>form.contact_us.message</source>
+        <target>Escribe algo aqui</target>
+      </trans-unit>
+      <trans-unit id="71IY2IB" resname="form.contact_us.submit">
+        <source>form.contact_us.submit</source>
+        <target>Enviar</target>
+      </trans-unit>
+      <trans-unit id=".NdUZxl" resname="form.player.username">
+        <source>form.player.username</source>
+        <target>Nombre de usuario</target>
+      </trans-unit>
+      <trans-unit id="mVHgeOs" resname="form.player.email">
+        <source>form.player.email</source>
+        <target>Email</target>
+      </trans-unit>
+      <trans-unit id="CNHrk_D" resname="form.player.fullname">
+        <source>form.player.fullname</source>
+        <target>Nombre completo</target>
+      </trans-unit>
+      <trans-unit id="Ta7WK.Z" resname="form.player.dateofbirth">
+        <source>form.player.dateofbirth</source>
+        <target>Fecha de nacimiento</target>
+      </trans-unit>
+      <trans-unit id="C6P4ESW" resname="form.player.password">
+        <source>form.player.password</source>
+        <target>Contraseña</target>
+      </trans-unit>
+      <trans-unit id="M2bW_4r" resname="form.player.submit">
+        <source>form.player.submit</source>
+        <target>Registrame!</target>
+      </trans-unit>
+      <trans-unit id="nUXWXrC" resname="nav.logout">
+        <source>nav.logout</source>
+        <target>Logout</target>
+      </trans-unit>
+      <trans-unit id="VYTR44p" resname="nav.home">
+        <source>nav.home</source>
+        <target>Inicio</target>
+      </trans-unit>
+      <trans-unit id="kFEHDz5" resname="nav.game">
+        <source>nav.game</source>
+        <target>Juego</target>
+      </trans-unit>
+      <trans-unit id="R6WJMnm" resname="nav.register">
+        <source>nav.register</source>
+        <target>Registro</target>
+      </trans-unit>
+      <trans-unit id="ursKFnc" resname="nav.login">
+        <source>nav.login</source>
+        <target>Login</target>
+      </trans-unit>
+      <trans-unit id="76Tobqp" resname="nav.contact_us">
+        <source>nav.contact_us</source>
+        <target>Contacto</target>
+      </trans-unit>
+      <trans-unit id="6pLct0X" resname="lang.english">
+        <source>lang.english</source>
+        <target>Inglés</target>
+      </trans-unit>
+      <trans-unit id="JQRu40a" resname="lang.spanish">
+        <source>lang.spanish</source>
+        <target>Español</target>
+      </trans-unit>
+      <trans-unit id="3CcUjkC" resname="forms.player.username">
+        <source>forms.player.username</source>
+        <target>Usuario</target>
+      </trans-unit>
+      <trans-unit id="1B2ZDKM" resname="forms.player.password">
+        <source>forms.player.password</source>
+        <target>Contraseña</target>
+      </trans-unit>
+      <trans-unit id="4WDUNwo" resname="forms.player.login">
+        <source>forms.player.login</source>
+        <target>Login</target>
+      </trans-unit>
+      <trans-unit id="BB1CUxe" resname="app.main.title">
+        <source>app.main.title</source>
+        <target>Ahorcado</target>
+      </trans-unit>
+      <trans-unit id="tXR_11r" resname="app.main.hello">
+        <source>app.main.hello</source>
+        <target>Hola, %username%</target>
+      </trans-unit>
+      <trans-unit id="ucSE6uJ" resname="app.main.page_title">
+        <source>app.main.page_title</source>
+        <target>Juego del Ahoracado</target>
+      </trans-unit>
+      <trans-unit id="w354kn2" resname="game.subtitle">
+        <source>game.subtitle</source>
+        <target>Adivina la palabra misteriosa</target>
+      </trans-unit>
+      <trans-unit id="ZRoTya5" resname="app.banner.homepage">
+        <source>app.banner.homepage</source>
+        <target>PÁGINA DE INICIO</target>
+      </trans-unit>
+      <trans-unit id="s2CPkBQ" resname="testimonials.title">
+        <source>testimonials.title</source>
+        <target>Testimonios</target>
+      </trans-unit>
+      <trans-unit id="y9Nba09" resname="game.title.last_games">
+        <source>game.title.last_games</source>
+        <target>Últimas partidas</target>
+      </trans-unit>
+      <trans-unit id="Psj4TzH" resname="game.title.last_players">
+        <source>game.title.last_players</source>
+        <target>Últimos jugadores</target>
+      </trans-unit>
+      <trans-unit id="ZFRowtq" resname="player.register.success">
+        <source>player.register.success</source>
+        <target>Se ha registrado correctamente</target>
+      </trans-unit>
+      <trans-unit id="CZQk6Mi" resname="player.register.message">
+        <source>player.register.message</source>
+        <target>Muchas gracias.</target>
+      </trans-unit>
+      <trans-unit id="8x1ob93" resname="game.attempts">
+        <source>game.attempts</source>
+        <target>Te quedan %attempts% intentos.</target>
+      </trans-unit>
+      <trans-unit id=".YzsRlF" resname="game.reset">
+        <source>game.reset</source>
+        <target>Comenzar nuevo juego</target>
+      </trans-unit>
+      <trans-unit id="iSkXaBG" resname="game.try_letter">
+        <source>game.try_letter</source>
+        <target>Probar letra</target>
+      </trans-unit>
+      <trans-unit id="JTL8VDs" resname="game.try_word">
+        <source>game.try_word</source>
+        <target>Probar palabra</target>
+      </trans-unit>
+      <trans-unit id="Wyhu_sq" resname="game.button.guess">
+        <source>game.button.guess</source>
+        <target>Déjame adivinar...</target>
+      </trans-unit>
+      <trans-unit id="sUICfXg" resname="game.placeholder.guess">
+        <source>game.placeholder.guess</source>
+        <target>Palabra</target>
+      </trans-unit>
+      <trans-unit id="BRlDZc." resname="game.message.win">
+        <source>game.message.win</source>
+        <target>HAS GANADO</target>
+      </trans-unit>
+      <trans-unit id="wDAwVkO" resname="game.message.lose">
+        <source>game.message.lose</source>
+        <target>HAS PERDIDO</target>
+      </trans-unit>
+    </body>
+  </file>
+</xliff>

+ 12 - 0
app/Resources/translations/messages.es.yml

@@ -0,0 +1,12 @@
+form.contact_us.sender: Quién lo envia
+form.contact_us.subject: Asunto
+form.contact_us.message: Escribe algo aqui
+form.contact_us.submit: Enviar
+form.player.username: Nombre de usuario
+form.player.email: Email
+form.player.fullname: Nombre completo
+form.player.dateofbirth: Fecha de nacimiento
+form.player.password: Contraseña
+form.player.submit: Registrame!
+nav.logout: Logout
+nav.home: Inicio

+ 14 - 0
app/Resources/translations/validators.en.xlf

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+  <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
+    <header>
+      <tool tool-id="symfony" tool-name="Symfony"/>
+    </header>
+    <body>
+      <trans-unit id="HdTNPoR" resname="form.contact_us.notblank">
+        <source>form.contact_us.notblank</source>
+        <target>Please type some text</target>
+      </trans-unit>
+    </body>
+  </file>
+</xliff>

+ 1 - 0
app/Resources/translations/validators.en.yml

@@ -0,0 +1 @@
+form.contact_us.notblank: Please type some text

+ 14 - 0
app/Resources/translations/validators.es.xlf

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+  <file source-language="en" target-language="es" datatype="plaintext" original="file.ext">
+    <header>
+      <tool tool-id="symfony" tool-name="Symfony"/>
+    </header>
+    <body>
+      <trans-unit id="HdTNPoR" resname="form.contact_us.notblank">
+        <source>form.contact_us.notblank</source>
+        <target>Por favor escribe algo</target>
+      </trans-unit>
+    </body>
+  </file>
+</xliff>

+ 1 - 0
app/Resources/translations/validators.es.yml

@@ -0,0 +1 @@
+form.contact_us.notblank: Por favor escribe algo

+ 84 - 0
app/Resources/views/base.html.twig

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>{% block title %}{{ 'app.main.page_title'|trans }}{% endblock %}</title>
+    <link rel="stylesheet" href="{{  asset('bundles/app/app.css') }}">
+    <link rel="shortcut icon" href="/favicon.ico" />
+</head>
+<body>
+<div class="container">
+    <header class="row">
+        <div class="col-md-4">
+            <div id="logo">
+                <h1>
+                    <a href="{{ path('homepage') }}">{{ 'app.main.title'|trans }}</a>
+                </h1>
+            </div>
+        </div>
+        <div class="col-md-8">
+            <ul class="text-right">
+                {% if is_granted('IS_AUTHENTICATED_FULLY') %}
+                    <li>{{ 'app.main.hello'|trans({'%username%' : app.user.fullname})  }}</li>
+                {% endif %}
+                <li {% if (app.request.locale == 'en') %} class="active" {% endif %}>
+                    <a href="{{ path('homepage_locale', {'_locale' : 'en'}) }}">{{ 'lang.english'|trans }}</a>
+                </li>
+                <li {% if (app.request.locale == 'es') %} class="active" {% endif %}>
+                    <a href="{{ path('homepage_locale', {'_locale' : 'es'}) }}">{{ 'lang.spanish'|trans }}</a>
+                </li>
+            </ul>
+            <nav>
+                <ul>
+                    <li>
+                        <a href="{{ path('homepage_locale', {'_locale' : app.request.locale}) }}">{{ 'nav.home'|trans }}</a>
+                    </li>
+                    <li>
+                        <a href="{{ path('game_locale', {'_locale' : app.request.locale}) }}">{{ 'nav.game'|trans }}</a>
+                    </li>
+                    <li>
+                        <a href="{{ path('player_register', {'_locale' : app.request.locale}) }}">{{ 'nav.register'|trans }}</a>
+                    </li>
+                    <li>
+                        <a href="{{ path('contact_us_locale', {'_locale' : app.request.locale}) }}">{{ 'nav.contact_us'|trans }}</a>
+                    </li>
+                    <li>
+                        {% if is_granted('IS_AUTHENTICATED_FULLY') %}
+                            <a href="{{ path('player_logout', {'_locale' : app.request.locale}) }}">{{ 'nav.logout'|trans }}</a>
+                        {% else %}
+                            <a href="{{ path('player_signin', {'_locale' : app.request.locale}) }}">{{ 'nav.login'|trans }}</a>
+                        {% endif %}
+                    </li>
+                </ul>
+            </nav>
+        </div>
+    </header>
+    <div class="row">
+        <div class="col-md-8">
+
+        {%  block content %}{% endblock %}
+
+        </div>
+        <div class="col-md-4">
+            <div id="sidebar">
+                <div class="box">
+                    <h3>{{ 'game.title.last_games'|trans }}</h3>
+                    <div class="date-list">
+                        <ul class="list date-list">
+                            <li class="first"><span class="date">Jan 13</span> <a href="#">Ultrices quisque molestie</a></li>
+                            <li><span class="date">Jan 7</span> <a href="#">Neque dolor eget</a></li>
+                            <li><span class="date">Jan 1</span> <a href="#">Sollicitudin interdum</a></li>
+                            <li class="last"><span class="date">Dec 26</span> <a href="#">Varius dignissim</a></li>
+                        </ul>
+                    </div>
+                </div>
+                <div class="box">
+                    {{ render(controller('AppBundle:Player:list')) }}
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 13 - 0
app/Resources/views/birthday/index.html.twig

@@ -0,0 +1,13 @@
+<html>
+    <header>
+        <title>This is for birthdays</title>
+    </header>
+    <body>
+    This is the day of the week for the current year: {{ result_current }}<br/>
+    This is the day of the week for the previous year: {{ result_last }}
+    <br/>
+    {% for year, result in  results %}
+        This is the day for {{ year }}: {{ result }}<br/>
+    {% endfor %}
+    </body>
+</html>

+ 35 - 0
app/Resources/views/contact_us.html.twig

@@ -0,0 +1,35 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ parent() }} - {{ 'nav.contact_us'|trans }}{% endblock %}
+
+{% block content %}
+    <div class="box">
+        <h2>{{ 'nav.contact_us'|trans }}</h2>
+
+
+    {{ form_start(contact_us_form, {'attr': {'novalidate':''}}) }}
+
+        {{ form_errors(contact_us_form) }}
+
+        <div class="form-group">
+            {{ form_label(contact_us_form.sender) }}
+            {{ form_errors(contact_us_form.sender) }}
+            {{ form_widget(contact_us_form.sender, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(contact_us_form.subject) }}
+            {{ form_errors(contact_us_form.subject) }}
+            {{ form_widget(contact_us_form.subject, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(contact_us_form.message) }}
+            {{ form_errors(contact_us_form.message) }}
+            {{ form_widget(contact_us_form.message, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+    {{ form_end(contact_us_form) }}
+
+    </div>
+{% endblock %}

+ 12 - 0
app/Resources/views/contact_us_success.html.twig

@@ -0,0 +1,12 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ parent() }} - {{ 'nav.contact_us'|trans }}{% endblock %}
+
+{% block content %}
+<div class="box">
+
+    <h2>Your message was sent successfully</h2>
+
+    <p>Thanks a lot.</p>
+</div>
+{% endblock %}

+ 76 - 0
app/Resources/views/default/index.html.twig

@@ -0,0 +1,76 @@
+{% extends 'base.html.twig' %}
+
+{% block body %}
+    <div id="wrapper">
+        <div id="container">
+            <div id="welcome">
+                <h1><span>Welcome to</span> Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}</h1>
+            </div>
+
+            <div id="status">
+                <p>
+                    <svg id="icon-status" width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z" fill="#759E1A"/></svg>
+
+                    Your application is now ready. You can start working on it at:
+                    <code>{{ base_dir }}</code>
+                </p>
+            </div>
+
+            <div id="next">
+                <h2>What's next?</h2>
+                <p>
+                    <svg id="icon-book" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="-12.5 9 64 64" enable-background="new -12.5 9 64 64" xml:space="preserve">
+                        <path fill="#AAA" d="M6.8,40.8c2.4,0.8,4.5-0.7,4.9-2.5c0.2-1.2-0.3-2.1-1.3-3.2l-0.8-0.8c-0.4-0.5-0.6-1.3-0.2-1.9
+                            c0.4-0.5,0.9-0.8,1.8-0.5c1.3,0.4,1.9,1.3,2.9,2.2c-0.4,1.4-0.7,2.9-0.9,4.2l-0.2,1c-0.7,4-1.3,6.2-2.7,7.5
+                            c-0.3,0.3-0.7,0.5-1.3,0.6c-0.3,0-0.4-0.3-0.4-0.3c0-0.3,0.2-0.3,0.3-0.4c0.2-0.1,0.5-0.3,0.4-0.8c0-0.7-0.6-1.3-1.3-1.3
+                            c-0.6,0-1.4,0.6-1.4,1.7s1,1.9,2.4,1.8c0.8,0,2.5-0.3,4.2-2.5c2-2.5,2.5-5.4,2.9-7.4l0.5-2.8c0.3,0,0.5,0.1,0.8,0.1
+                            c2.4,0.1,3.7-1.3,3.7-2.3c0-0.6-0.3-1.2-0.9-1.2c-0.4,0-0.8,0.3-1,0.8c-0.1,0.6,0.8,1.1,0.1,1.5c-0.5,0.3-1.4,0.6-2.7,0.4l0.3-1.3
+                            c0.5-2.6,1-5.7,3.2-5.8c0.2,0,0.8,0,0.8,0.4c0,0.2,0,0.2-0.2,0.5c-0.2,0.3-0.3,0.4-0.2,0.7c0,0.7,0.5,1.1,1.2,1.1
+                            c0.9,0,1.2-1,1.2-1.4c0-1.2-1.2-1.8-2.6-1.8c-1.5,0.1-2.8,0.9-3.7,2.1c-1.1,1.3-1.8,2.9-2.3,4.5c-0.9-0.8-1.6-1.8-3.1-2.3
+                            c-1.1-0.7-2.3-0.5-3.4,0.3c-0.5,0.4-0.8,1-1,1.6c-0.4,1.5,0.4,2.9,0.8,3.4l0.9,1c0.2,0.2,0.6,0.8,0.4,1.5c-0.3,0.8-1.2,1.3-2.1,1
+                            c-0.4-0.2-1-0.5-0.9-0.9c0.1-0.2,0.2-0.3,0.3-0.5s0.1-0.3,0.1-0.3c0.2-0.6-0.1-1.4-0.7-1.6c-0.6-0.2-1.2,0-1.3,0.8
+                            C4.3,38.4,4.7,40,6.8,40.8z M46.1,20.9c0-4.2-3.2-7.5-7.1-7.5h-3.8C34.8,10.8,32.7,9,30.2,9L-2.3,9.1c-2.8,0.1-4.9,2.4-4.9,5.4
+                            L-7,58.6c0,4.8,8.1,13.9,11.6,14.1l34.7-0.1c3.9,0,7-3.4,7-7.6L46.1,20.9z M-0.3,36.4c0-8.6,6.5-15.6,14.5-15.6
+                            c8,0,14.5,7,14.5,15.6S22.1,52,14.2,52C6.1,52-0.3,45-0.3,36.4z M42.1,65.1c0,1.8-1.5,3.1-3.1,3.1H4.6c-0.7,0-3-1.8-4.5-4.4h30.4
+                            c2.8,0,5-2.4,5-5.4V17.9h3.7c1.6,0,2.9,1.4,2.9,3.1V65.1L42.1,65.1z"/>
+                    </svg>
+
+                    Read the documentation to learn
+                    <a href="https://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION')[:3] }}/page_creation.html">
+                        How to create your first page in Symfony
+                    </a>
+                </p>
+            </div>
+
+        </div>
+    </div>
+{% endblock %}
+
+{% block stylesheets %}
+<style>
+    body { background: #F5F5F5; font: 18px/1.5 sans-serif; }
+    h1, h2 { line-height: 1.2; margin: 0 0 .5em; }
+    h1 { font-size: 36px; }
+    h2 { font-size: 21px; margin-bottom: 1em; }
+    p { margin: 0 0 1em 0; }
+    a { color: #0000F0; }
+    a:hover { text-decoration: none; }
+    code { background: #F5F5F5; max-width: 100px; padding: 2px 6px; word-wrap: break-word; }
+    #wrapper { background: #FFF; margin: 1em auto; max-width: 800px; width: 95%; }
+    #container { padding: 2em; }
+    #welcome, #status { margin-bottom: 2em; }
+    #welcome h1 span { display: block; font-size: 75%; }
+    #icon-status, #icon-book { float: left; height: 64px; margin-right: 1em; margin-top: -4px; width: 64px; }
+    #icon-book { display: none; }
+
+    @media (min-width: 768px) {
+        #wrapper { width: 80%; margin: 2em auto; }
+        #icon-book { display: inline-block; }
+        #status a, #next a { display: block; }
+
+        @-webkit-keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
+        @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
+        .sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards;}
+    }
+</style>
+{% endblock %}

+ 67 - 0
app/Resources/views/game.html.twig

@@ -0,0 +1,67 @@
+{% extends 'base.html.twig' %}
+
+    {% block title %}{{ parent() }} - Game{% endblock %}
+    {% block content %}
+        <div id="game" class="box">
+
+            <a href="{{ path('homepage_locale', {'_locale' : app.request.locale}) }}">{{ 'nav.home'|trans }}</a>
+            <hr />
+
+            <h2>{{ 'game.subtitle'|trans }}</h2>
+
+            {% if game.iswon %}
+            <div class="alert">
+                <h3 class="alert alert-success text-center">{{ 'game.message.win'|trans }}</h3>
+            </div>
+            {% endif %}
+
+            {% if game.ishanged %}
+                <div class="alert">
+                    <h3 class="alert alert-warning text-center">{{ 'game.message.lose'|trans }}</h3>
+                </div>
+            {% endif %}
+
+            {% if not game.isover %}
+            <p class="attempts">
+                {{ 'game.attempts'|trans({'%attempts%' : remaining_attempts}) }}
+            </p>
+            {% endif %}
+
+            <ul class="word-letters">
+                {% for letter in word_letters %}
+                <li class="not-guessed">{{ letter }}</li>
+                {% endfor %}
+            </ul>
+
+            <p class="attempts">
+                <a href="{{ path('game_reset') }}">{{ 'game.reset'|trans }}</a>
+            </p>
+
+            <br class="clearfix" />
+
+            {% if not game.isover %}
+            <h2>{{ 'game.try_letter'|trans }}</h2>
+
+            <ul>
+                {% for letter in letters %}
+                <li class="letter btn">
+                    <a href="{{ path('game_letter', {'letter' :letter}) }}">{{ letter|upper }}</a>
+                </li>
+                {% endfor %}
+            </ul>
+
+            <h2>{{ 'game.try_word'|trans }}</h2>
+
+            <form action="{{ path('game_try_word') }}" method="post" class="form-inline">
+                <div class="form-group">
+                    <input name="word" class="form-control mb-2" placeholder="{{ 'game.placeholder.guess'|trans }}"/>
+                </div>
+                <button>{{ 'game.button.guess'|trans }}</button>
+            </form>
+
+            {% endif %}
+        </div>
+        <div class="box">
+            {{ render(controller('AppBundle:Testimonials:index')) }}
+        </div>
+    {% endblock %}

+ 9 - 0
app/Resources/views/hello/index.html.twig

@@ -0,0 +1,9 @@
+<html>
+    <header>
+        <title>{{ title }}</title>
+    </header>
+    <body>
+        <h1>Hello world!</h1>
+        <h2>{{ name }}</h2>
+    </body>
+</html>

+ 5 - 0
app/Resources/views/hello/index.xml.twig

@@ -0,0 +1,5 @@
+<xml>
+    <title>{{ title }}</title>
+    <h1>Hello world!</h1>
+    <h2>{{ name }}</h2>
+</xml>

+ 10 - 0
app/Resources/views/index.html.twig

@@ -0,0 +1,10 @@
+{% extends 'base.html.twig' %}
+
+{% block content %}
+    <div id="homepage" class="box">
+        {{ 'app.banner.homepage'|trans }}
+    </div>
+    <div class="box">
+        {{ render(controller('AppBundle:Testimonials:index')) }}
+    </div>
+{% endblock %}

+ 6 - 0
app/Resources/views/player_list.html.twig

@@ -0,0 +1,6 @@
+<h3>{{ 'game.title.last_players'|trans }}</h3>
+<ul>
+    {% for player in players %}
+    <li>{{ player.fullname }}</li>
+    {% endfor %}
+</ul>

+ 27 - 0
app/Resources/views/player_login.html.twig

@@ -0,0 +1,27 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ parent() }} - {{ 'nav.login'|trans }}{% endblock %}
+
+{% block content %}
+    <div class="box">
+        <h2>{{ 'nav.login'|trans }}</h2>
+
+        {%  if error %}
+            <div class="error">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
+        {% endif %}
+
+        <form id="login" action="{{ path("player_signin") }}" method="POST">
+            <div class="form-group">
+                <label>{{ 'forms.player.username'|trans }}</label>
+                <input type="text" name="_username" value="{{ last_username }}" class="form-control" />
+            </div>
+            <div class="form-group">
+                <label>{{ 'forms.player.password'|trans }}</label>
+                <input type="password" name="_password" class="form-control" />
+            </div>
+            <div class="form-group">
+                <button type="submit" name="submit">{{ 'forms.player.login'|trans }}</button>
+            </div>
+        </form>
+    </div>
+{% endblock %}

+ 47 - 0
app/Resources/views/player_register.html.twig

@@ -0,0 +1,47 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ parent() }} - {{ 'nav.register'|trans }}{% endblock %}
+
+{% block content %}
+    <div class="box">
+        <h2>{{ 'nav.register'|trans }}</h2>
+
+
+        {{ form_start(register_form, {'attr': {'novalidate':''}}) }}
+
+        {{ form_errors(register_form) }}
+
+        <div class="form-group">
+            {{ form_label(register_form.username) }}
+            {{ form_errors(register_form.username) }}
+            {{ form_widget(register_form.username, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(register_form.fullname) }}
+            {{ form_errors(register_form.fullname) }}
+            {{ form_widget(register_form.fullname, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(register_form.email) }}
+            {{ form_errors(register_form.email) }}
+            {{ form_widget(register_form.email, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(register_form.dateOfBirth) }}
+            {{ form_errors(register_form.dateOfBirth) }}
+            {{ form_widget(register_form.dateOfBirth, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        <div class="form-group">
+            {{ form_label(register_form.password) }}
+            {{ form_errors(register_form.password) }}
+            {{ form_widget(register_form.password, {'attr':{'class': 'form-control'}}) }}
+        </div>
+
+        {{ form_end(register_form) }}
+
+    </div>
+{% endblock %}

+ 12 - 0
app/Resources/views/register_success.html.twig

@@ -0,0 +1,12 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ parent() }} - {{ 'nav.register'|trans }}{% endblock %}
+
+{% block content %}
+<div class="box">
+
+    <h2>{{ 'player.register.success'|trans }}</h2>
+
+    <p>{{ 'player.register.message'|trans }}</p>
+</div>
+{% endblock %}

+ 8 - 0
app/Resources/views/testimonials.html.twig

@@ -0,0 +1,8 @@
+<div id="testimonials">
+    <h2>{{ 'testimonials.title'|trans }}</h2>
+    <ul>
+        <li><strong>John Doe</strong> "I love this game, so addictive!"</li>
+        <li><strong>Martin Durand</strong> "Best web application ever"</li>
+        <li><strong>Paula Smith</strong> "Awesomeness!"</li>
+    </ul>
+</div>

+ 68 - 0
app/config/config.yml

@@ -0,0 +1,68 @@
+imports:
+    - { resource: parameters.yml }
+    - { resource: security.yml }
+    - { resource: services.yml }
+
+# Put parameters here that don't need to change on each machine where the app is deployed
+# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
+parameters:
+    locale: en
+    hangman_dictionary_path: ['%kernel.project_dir%/app/Resources/data/words.txt']
+
+framework:
+    #esi: ~
+    translator: { fallbacks: ['%locale%'] }
+    secret: '%secret%'
+    router:
+        resource: '%kernel.project_dir%/app/config/routing.yml'
+        strict_requirements: ~
+    form: ~
+    csrf_protection: ~
+    validation: { enable_annotations: true }
+    #serializer: { enable_annotations: true }
+    default_locale: '%locale%'
+    trusted_hosts: ~
+    session:
+        # https://symfony.com/doc/current/reference/configuration/framework.html#handler-id
+        handler_id: session.handler.native_file
+        save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
+    fragments: ~
+    http_method_override: true
+    assets: ~
+    php_errors:
+        log: true
+
+# Twig Configuration
+twig:
+    debug: '%kernel.debug%'
+    strict_variables: '%kernel.debug%'
+
+# Doctrine Configuration
+doctrine:
+    dbal:
+        driver: pdo_mysql
+        host: '%database_host%'
+        port: '%database_port%'
+        dbname: '%database_name%'
+        user: '%database_user%'
+        password: '%database_password%'
+        charset: UTF8
+        # if using pdo_sqlite as your database driver:
+        #   1. add the path in parameters.yml
+        #     e.g. database_path: '%kernel.project_dir%/var/data/data.sqlite'
+        #   2. Uncomment database_path in parameters.yml.dist
+        #   3. Uncomment next line:
+        #path: '%database_path%'
+
+    orm:
+        auto_generate_proxy_classes: '%kernel.debug%'
+        naming_strategy: doctrine.orm.naming_strategy.underscore
+        auto_mapping: true
+
+# Swiftmailer Configuration
+swiftmailer:
+    transport: '%mailer_transport%'
+    host: '%mailer_host%'
+    username: '%mailer_user%'
+    password: '%mailer_password%'
+    spool: { type: memory }

+ 42 - 0
app/config/config_dev.yml

@@ -0,0 +1,42 @@
+imports:
+    - { resource: config.yml }
+
+framework:
+    router:
+        resource: '%kernel.project_dir%/app/config/routing_dev.yml'
+        strict_requirements: true
+    profiler: { only_exceptions: false }
+    esi: ~
+
+web_profiler:
+    toolbar: true
+    intercept_redirects: false
+
+monolog:
+    handlers:
+        main:
+            type: stream
+            path: '%kernel.logs_dir%/%kernel.environment%.log'
+            level: debug
+            channels: ['!event']
+        console:
+            type: console
+            process_psr_3_messages: false
+            channels: ['!event', '!doctrine', '!console']
+        # To follow logs in real time, execute the following command:
+        # `bin/console server:log -vv`
+        server_log:
+            type: server_log
+            process_psr_3_messages: false
+            host: 127.0.0.1:9911
+        # uncomment to get logging in your browser
+        # you may have to allow bigger header sizes in your Web server configuration
+        #firephp:
+        #    type: firephp
+        #    level: info
+        #chromephp:
+        #    type: chromephp
+        #    level: info
+
+swiftmailer:
+    delivery_addresses: ['something@localhost.com']

+ 22 - 0
app/config/config_prod.yml

@@ -0,0 +1,22 @@
+imports:
+    - { resource: config.yml }
+
+#doctrine:
+#    orm:
+#        metadata_cache_driver: apc
+#        result_cache_driver: apc
+#        query_cache_driver: apc
+
+monolog:
+    handlers:
+        main:
+            type: fingers_crossed
+            action_level: error
+            handler: nested
+        nested:
+            type: stream
+            path: '%kernel.logs_dir%/%kernel.environment%.log'
+            level: debug
+        console:
+            type: console
+            process_psr_3_messages: false

+ 19 - 0
app/config/config_test.yml

@@ -0,0 +1,19 @@
+imports:
+    - { resource: config_dev.yml }
+
+framework:
+    test: ~
+    session:
+        storage_id: session.storage.mock_file
+    profiler:
+        collect: false
+
+web_profiler:
+    toolbar: false
+    intercept_redirects: false
+
+swiftmailer:
+    disable_delivery: true
+
+parameters:
+    hangman_path: ['%kernel.project_dir%/app/Resources/data/test.txt']

+ 19 - 0
app/config/parameters.yml.dist

@@ -0,0 +1,19 @@
+# This file is a "template" of what your parameters.yml file should look like
+# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production.
+# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
+parameters:
+    database_host: 127.0.0.1
+    database_port: ~
+    database_name: symfony
+    database_user: root
+    database_password: ~
+    # You should uncomment this if you want to use pdo_sqlite
+    #database_path: '%kernel.project_dir%/var/data/data.sqlite'
+
+    mailer_transport: smtp
+    mailer_host: 127.0.0.1
+    mailer_user: ~
+    mailer_password: ~
+
+    # A secret key that's used to generate certain security-related tokens
+    secret: ThisTokenIsNotSoSecretChangeIt

+ 3 - 0
app/config/routing.yml

@@ -0,0 +1,3 @@
+app:
+    resource: '@AppBundle/Controller/'
+    type: annotation

+ 14 - 0
app/config/routing_dev.yml

@@ -0,0 +1,14 @@
+_wdt:
+    resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
+    prefix: /_wdt
+
+_profiler:
+    resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
+    prefix: /_profiler
+
+_errors:
+    resource: '@TwigBundle/Resources/config/routing/errors.xml'
+    prefix: /_error
+
+_main:
+    resource: routing.yml

+ 41 - 0
app/config/security.yml

@@ -0,0 +1,41 @@
+# To get started with security, check out the documentation:
+# https://symfony.com/doc/current/security.html
+security:
+    encoders:
+        AppBundle\Entity\Player: bcrypt
+
+    # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
+    providers:
+        users:
+          entity:
+            class: AppBundle\Entity\Player
+            property: username
+        in_memory:
+            memory: ~
+
+    firewalls:
+        # disables authentication for assets and the profiler, adapt it according to your needs
+        dev:
+            pattern: ^/(_(profiler|wdt)|css|images|js)/
+            security: false
+
+        main:
+          pattern: "^/"
+          provider: users
+          anonymous: ~
+          form_login:
+            check_path: player_signin
+            login_path: player_signin
+            default_target_path: homepage_locale
+          logout:
+            path: player_logout
+            target: homepage_locale
+
+#            anonymous: ~
+            # activate different ways to authenticate
+
+            # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
+            #http_basic: ~
+
+            # https://symfony.com/doc/current/security/form_login_setup.html
+            #form_login: ~

+ 47 - 0
app/config/services.yml

@@ -0,0 +1,47 @@
+# Learn more about services, parameters and containers at
+# https://symfony.com/doc/current/service_container.html
+parameters:
+    #parameter_name: value
+
+services:
+    # default configuration for services in *this* file
+    _defaults:
+        # automatically injects dependencies in your services
+        autowire: true
+        # automatically registers your services as commands, event subscribers, etc.
+        autoconfigure: true
+        # this means you cannot fetch services directly from the container via $container->get()
+        # if you need to do this, you can override this setting on individual services
+        public: false
+
+    # makes classes in src/AppBundle available to be used as services
+    # this creates a service per class whose id is the fully-qualified class name
+    AppBundle\:
+        resource: '../../src/AppBundle/*'
+        # you can exclude directories or files
+        # but if a service is unused, it's removed anyway
+        exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
+
+    # controllers are imported separately to make sure they're public
+    # and have a tag that allows actions to type-hint services
+    AppBundle\Controller\:
+        resource: '../../src/AppBundle/Controller'
+        public: true
+        tags: ['controller.service_arguments']
+
+    # add more services, or override services that need manual wiring
+    # AppBundle\Services\ExampleService:
+    #     arguments:
+    #         $someArgument: 'some_value'
+
+#    app.runner:
+#      class: AppBundle\Game\Runner
+#    AppBundle\Game\Runner:
+#      public: true
+#      arguments: [ '@AppBundle\Game\Storage', '@AppBundle\Game\Wordlist' ]
+
+    AppBundle\Game\Wordlist:
+      arguments: [ '%hangman_dictionary_path%' ]
+      calls:
+        - method: addLoader
+          arguments: ['@AppBundle\Game\Loader\TextFileLoader']

+ 27 - 0
bin/console

@@ -0,0 +1,27 @@
+#!/usr/bin/env php
+<?php
+
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Component\Console\Input\ArgvInput;
+use Symfony\Component\Debug\Debug;
+
+// if you don't want to setup permissions the proper way, just uncomment the following PHP line
+// read https://symfony.com/doc/current/setup.html#checking-symfony-application-configuration-and-setup
+// for more information
+//umask(0000);
+
+set_time_limit(0);
+
+require __DIR__.'/../vendor/autoload.php';
+
+$input = new ArgvInput();
+$env = $input->getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev', true);
+$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption('--no-debug', true) && $env !== 'prod';
+
+if ($debug) {
+    Debug::enable();
+}
+
+$kernel = new AppKernel($env, $debug);
+$application = new Application($kernel);
+$application->run($input);

+ 146 - 0
bin/symfony_requirements

@@ -0,0 +1,146 @@
+#!/usr/bin/env php
+<?php
+
+require_once dirname(__FILE__).'/../var/SymfonyRequirements.php';
+
+$lineSize = 70;
+$symfonyRequirements = new SymfonyRequirements();
+$iniPath = $symfonyRequirements->getPhpIniConfigPath();
+
+echo_title('Symfony Requirements Checker');
+
+echo '> PHP is using the following php.ini file:'.PHP_EOL;
+if ($iniPath) {
+    echo_style('green', '  '.$iniPath);
+} else {
+    echo_style('yellow', '  WARNING: No configuration file (php.ini) used by PHP!');
+}
+
+echo PHP_EOL.PHP_EOL;
+
+echo '> Checking Symfony requirements:'.PHP_EOL.'  ';
+
+$messages = array();
+foreach ($symfonyRequirements->getRequirements() as $req) {
+    if ($helpText = get_error_message($req, $lineSize)) {
+        echo_style('red', 'E');
+        $messages['error'][] = $helpText;
+    } else {
+        echo_style('green', '.');
+    }
+}
+
+$checkPassed = empty($messages['error']);
+
+foreach ($symfonyRequirements->getRecommendations() as $req) {
+    if ($helpText = get_error_message($req, $lineSize)) {
+        echo_style('yellow', 'W');
+        $messages['warning'][] = $helpText;
+    } else {
+        echo_style('green', '.');
+    }
+}
+
+if ($checkPassed) {
+    echo_block('success', 'OK', 'Your system is ready to run Symfony projects');
+} else {
+    echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects');
+
+    echo_title('Fix the following mandatory requirements', 'red');
+
+    foreach ($messages['error'] as $helpText) {
+        echo ' * '.$helpText.PHP_EOL;
+    }
+}
+
+if (!empty($messages['warning'])) {
+    echo_title('Optional recommendations to improve your setup', 'yellow');
+
+    foreach ($messages['warning'] as $helpText) {
+        echo ' * '.$helpText.PHP_EOL;
+    }
+}
+
+echo PHP_EOL;
+echo_style('title', 'Note');
+echo '  The command console could use a different php.ini file'.PHP_EOL;
+echo_style('title', '~~~~');
+echo '  than the one used with your web server. To be on the'.PHP_EOL;
+echo '      safe side, please check the requirements from your web'.PHP_EOL;
+echo '      server using the ';
+echo_style('yellow', 'web/config.php');
+echo ' script.'.PHP_EOL;
+echo PHP_EOL;
+
+exit($checkPassed ? 0 : 1);
+
+function get_error_message(Requirement $requirement, $lineSize)
+{
+    if ($requirement->isFulfilled()) {
+        return;
+    }
+
+    $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.'   ').PHP_EOL;
+    $errorMessage .= '   > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.'   > ').PHP_EOL;
+
+    return $errorMessage;
+}
+
+function echo_title($title, $style = null)
+{
+    $style = $style ?: 'title';
+
+    echo PHP_EOL;
+    echo_style($style, $title.PHP_EOL);
+    echo_style($style, str_repeat('~', strlen($title)).PHP_EOL);
+    echo PHP_EOL;
+}
+
+function echo_style($style, $message)
+{
+    // ANSI color codes
+    $styles = array(
+        'reset' => "\033[0m",
+        'red' => "\033[31m",
+        'green' => "\033[32m",
+        'yellow' => "\033[33m",
+        'error' => "\033[37;41m",
+        'success' => "\033[37;42m",
+        'title' => "\033[34m",
+    );
+    $supports = has_color_support();
+
+    echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : '');
+}
+
+function echo_block($style, $title, $message)
+{
+    $message = ' '.trim($message).' ';
+    $width = strlen($message);
+
+    echo PHP_EOL.PHP_EOL;
+
+    echo_style($style, str_repeat(' ', $width));
+    echo PHP_EOL;
+    echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT));
+    echo PHP_EOL;
+    echo_style($style, $message);
+    echo PHP_EOL;
+    echo_style($style, str_repeat(' ', $width));
+    echo PHP_EOL;
+}
+
+function has_color_support()
+{
+    static $support;
+
+    if (null === $support) {
+        if (DIRECTORY_SEPARATOR == '\\') {
+            $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
+        } else {
+            $support = function_exists('posix_isatty') && @posix_isatty(STDOUT);
+        }
+    }
+
+    return $support;
+}

+ 71 - 0
composer.json

@@ -0,0 +1,71 @@
+{
+    "name": "charles/hangman",
+    "description": "Hangman project",
+    "license": "proprietary",
+    "type": "project",
+    "autoload": {
+        "psr-4": {
+            "AppBundle\\": "src/AppBundle"
+        },
+        "classmap": [
+            "app/AppKernel.php",
+            "app/AppCache.php"
+        ]
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Tests\\": "tests/"
+        },
+        "files": [
+            "vendor/symfony/symfony/src/Symfony/Component/VarDumper/Resources/functions/dump.php"
+        ]
+    },
+    "require": {
+        "php": ">=5.5.9",
+        "doctrine/doctrine-bundle": "^1.6",
+        "doctrine/orm": "^2.5",
+        "incenteev/composer-parameter-handler": "^2.0",
+        "sensio/distribution-bundle": "^5.0.19",
+        "sensio/framework-extra-bundle": "^5.0.0",
+        "symfony/monolog-bundle": "^3.1.0",
+        "symfony/phpunit-bridge": "^4.0",
+        "symfony/polyfill-apcu": "^1.0",
+        "symfony/swiftmailer-bundle": "^2.6.4",
+        "symfony/symfony": "3.4.*",
+        "twig/twig": "^1.0||^2.0"
+    },
+    "require-dev": {
+        "sensio/generator-bundle": "^3.0"
+    },
+    "scripts": {
+        "symfony-scripts": [
+            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
+            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
+            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
+            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
+            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
+            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
+        ],
+        "post-install-cmd": [
+            "@symfony-scripts"
+        ],
+        "post-update-cmd": [
+            "@symfony-scripts"
+        ]
+    },
+    "config": {
+        "sort-packages": true
+    },
+    "extra": {
+        "symfony-app-dir": "app",
+        "symfony-bin-dir": "bin",
+        "symfony-var-dir": "var",
+        "symfony-web-dir": "web",
+        "symfony-tests-dir": "tests",
+        "symfony-assets-install": "relative",
+        "incenteev-parameters": {
+            "file": "app/config/parameters.yml"
+        },
+        "branch-alias": null
+    }
+}

文件差异内容过多而无法显示
+ 2391 - 0
composer.lock


+ 31 - 0
phpunit.xml.dist

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
+         backupGlobals="false"
+         colors="true"
+         bootstrap="vendor/autoload.php"
+>
+    <php>
+        <ini name="error_reporting" value="-1" />
+        <server name="KERNEL_CLASS" value="AppKernel" />
+    </php>
+
+    <testsuites>
+        <testsuite name="Project Test Suite">
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist>
+            <directory>src</directory>
+            <exclude>
+                <directory>src/*Bundle/Resources</directory>
+                <directory>src/*/*Bundle/Resources</directory>
+                <directory>src/*/Bundle/*Bundle/Resources</directory>
+            </exclude>
+        </whitelist>
+    </filter>
+</phpunit>

+ 7 - 0
src/.htaccess

@@ -0,0 +1,7 @@
+<IfModule mod_authz_core.c>
+    Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+    Order deny,allow
+    Deny from all
+</IfModule>

+ 9 - 0
src/AppBundle/AppBundle.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace AppBundle;
+
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+class AppBundle extends Bundle
+{
+}

+ 57 - 0
src/AppBundle/Controller/ContactUsController.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 24/04/2018
+ * Time: 13:39
+ */
+
+namespace AppBundle\Controller;
+
+
+use AppBundle\Entity\Contact;
+use AppBundle\Form\ContactFormType;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+class ContactUsController extends Controller
+{
+  /**
+   * @return \Symfony\Component\HttpFoundation\Response
+   * @Route("{_locale}/contact_us", name="contact_us_locale")
+   */
+  public function formAction(Request $request) {
+    $contact_us = new Contact();
+
+    $form = $this->createForm(ContactFormType::class, $contact_us)
+      ->handleRequest($request);
+
+    if ($form->isSubmitted() && $form->isValid()) {
+
+      // Send an email.
+      $email = \Swift_Message::newInstance()
+        ->setTo('carlos.carrascal@gmail.com')
+        ->setFrom($contact_us->getSender())
+        ->setSubject($contact_us->getSubject())
+        ->setBody($contact_us->getMessage());
+
+      $this->get('mailer')->send($email);
+
+      // Redirect to home page.
+      return $this->redirectToRoute('contact_us_success', ['_locale' => $request->getLocale()]);
+    }
+
+    return $this->render('contact_us.html.twig', array(
+      'contact_us_form' => $form->createView(),
+    ));
+  }
+
+  /**
+   * @return \Symfony\Component\HttpFoundation\Response
+   * @Route("{_locale}/contact_us_success", name="contact_us_success_locale")
+   */
+  public function formSubmitSuccess() {
+    return $this->render('contact_us_success.html.twig');
+  }
+}

+ 95 - 0
src/AppBundle/Controller/DefaultController.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace AppBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
+
+
+/**
+ * Class DefaultController
+ * @package AppBundle\Controller
+ *
+ * To get the assets into the public folder: bin/console assets:install --symlink
+ */
+class DefaultController extends Controller
+{
+    /**
+     * @Route("/", name="homepage")
+     * @Route("{_locale}/", name="homepage_locale")
+     */
+    public function indexAction(Request $request)
+    {
+        // replace this example code with whatever you need
+        $response = $this->render('index.html.twig');
+
+        $response->setCache(array(
+          's_maxage' => '10000',
+          )
+        );
+
+        return $response;
+    }
+
+    /**
+     * @Route("/hello/world", name="hello world!")
+     */
+    public function helloWorld(Request $request) {
+      return new Response('Hello World!');
+    }
+
+    /**
+     * @Route("/hello/{name}", name="hello")
+     */
+    public function hello(Request $request) {
+      return new Response('Hello ' . $request->attributes->get('name') . '!');
+    }
+
+    /**
+     * @Route("/hello2/{name}", defaults={"name": "default"}, name="hello2")
+     */
+    public function hello2($name) {
+//      return new Response('Hello ' . $name . '!');
+      return $this->render('hello/index.html.twig.twig', [
+        'name' => $name,
+        'title' => 'this is the page title',
+      ]);
+    }
+
+
+  /**
+   * @param $month
+   * @param $day
+   * @return Response
+   *
+   * @Route(
+   *   "/birthday/{month}/{day}",
+   *   requirements={
+   *      "month": "\d+",
+   *      "day": "\d+"},
+   *   name="birthday"
+   * )
+   */
+    public function birthdateAction($month, $day) {
+      $currentYear = date("Y", time());
+
+      $result_current = date("l", mktime(0, 0, 0, $month, $day, $currentYear));
+      $result_last = date("l", mktime(0, 0, 0, $month, $day, $currentYear-1));
+
+      $results = array();
+      for ($year = $currentYear; $year <= $currentYear + 5; $year++) {
+        $results[$year] = date("l", mktime(0, 0, 0, $month, $day, $year));
+      }
+      //dump($result_last);
+
+      return $this->render('birthday/index.html.twig.twig', [
+        'result_current' => $result_current,
+        'result_last' => $result_last,
+        'results' => $results,
+      ]);
+    }
+
+}

+ 134 - 0
src/AppBundle/Controller/GameController.php

@@ -0,0 +1,134 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 23/04/2018
+ * Time: 16:10
+ */
+
+namespace AppBundle\Controller;
+
+use AppBundle\Game\Loader\TextFileLoader;
+use AppBundle\Game\Runner;
+use AppBundle\Game\Game;
+use AppBundle\Game\Storage;
+use AppBundle\Game\WordList;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
+
+class GameController extends Controller
+{
+  /**
+   * @var Runner
+   */
+  private $runner;
+
+
+  function __construct(Runner $runner) {
+    $this->runner = $runner;
+  }
+
+  private function getRunner() {
+    return $this->runner;
+  }
+
+  private function getGame() {
+    $game = $this->getRunner()->loadGame();
+    return $game;
+  }
+
+  private function getWordLetters(Game $game) {
+    // Hide the letters that the user doesn't know yet.
+    $word_letters = $game->getWordLetters();
+    foreach ($word_letters as $key => $letter) {
+      if (!$game->isLetterFound($letter)) {
+        $word_letters[$key] = '?';
+      }
+    }
+    return $word_letters;
+  }
+
+  private function getRemainingLetters(Game $game) {
+    $letters = range('a', 'z');
+    $triedLetters = $game->getTriedLetters();
+
+    foreach ($letters as $key => $letter) {
+      if (in_array($letter, $triedLetters)) {
+        unset($letters[$key]);
+      }
+    }
+    return $letters;
+  }
+
+  /**
+   * @Route("/game", name="game")
+   * @Route("{_locale}/game", name="game_locale")
+   * @Cache(smaxage="15")
+   */
+  public function indexAction(Request $request)
+  {
+    // This check can be done with an annotation too.
+    $this->denyAccessUnlessGranted('ROLE_USER');
+
+    $game = $this->getGame();
+
+    $status = '';
+    if ($game->isWon()) {
+      $status = 'YOU WON!';
+    } else if ($game->isHanged()) {
+      $status = 'YOU ARE A LOOSER! AND YOU HAVE NO FRIENDS';
+    }
+
+    return $this->render('game.html.twig', [
+      'remaining_attempts' => $game->getRemainingAttempts(),
+      'word_letters' => $this->getWordLetters($game),
+      'status' => $status,
+      'game' => $game,
+      'letters' => $this->getRemainingLetters($game),
+    ]);
+  }
+
+  /**
+   * @param Request $request
+   * @Route("{_locale}/game/letter/{letter}", name="game_letter")
+   * @Security("has_role('ROLE_USER')")
+   */
+  public function tryLetterAction(Request $request, $letter) {
+
+    $runner = $this->getRunner();
+    $runner->playLetter($letter);
+
+    return $this->redirectToRoute('game_locale', ['_locale' => $request->getLocale()]);
+  }
+
+  /**
+   * @param Request $request
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   * @Route("{_locale}/game/reset", name="game_reset")
+   * @Security("has_role('ROLE_USER')")
+   */
+  public function resetGameAction(Request $request) {
+
+    $runner = $this->getRunner();
+    $runner->resetGame();
+
+    return $this->redirectToRoute('game_locale', ['_locale' => $request->getLocale()]);
+  }
+
+  /**
+   * @param Request $request
+   * @Route("{_locale}/game/tryword", name="game_try_word")
+   * @Security("has_role('ROLE_USER')")
+   */
+  public function tryWordAction(Request $request) {
+
+    $word = $request->get('word');
+    $runner = $this->getRunner();
+    $runner->playWord($word);
+
+    return $this->redirectToRoute('game_locale', ['_locale' => $request->getLocale()]);
+  }
+}

+ 97 - 0
src/AppBundle/Controller/PlayerController.php

@@ -0,0 +1,97 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 25/04/2018
+ * Time: 16:49
+ */
+
+namespace AppBundle\Controller;
+
+
+use AppBundle\AppBundle;
+use AppBundle\Entity\Player;
+use AppBundle\Form\PlayerFormType;
+use AppBundle\Services\PlayerService;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+
+class PlayerController extends Controller
+{
+
+  private $playerService;
+
+  function __construct(PlayerService $playerService) {
+    $this->playerService = $playerService;
+  }
+
+  /**
+   * @return \Symfony\Component\HttpFoundation\Response
+   * @Route("{_locale}/register", name="player_register")
+   */
+  public function formAction(Request $request) {
+    $player = new Player();
+
+    $form = $this->createForm(PlayerFormType::class, $player)
+      ->handleRequest($request);
+
+    if ($form->isSubmitted() && $form->isValid()) {
+
+      $this->playerService->registerPlayer($player);
+
+      // Redirect to register success page.
+      return $this->redirectToRoute('register_success', ['_locale' => $request->getLocale()]);
+    }
+
+    return $this->render('player_register.html.twig', array(
+      'register_form' => $form->createView(),
+    ));
+  }
+
+  /**
+   * @return \Symfony\Component\HttpFoundation\Response
+   * @Route("{_locale}/register_success", name="register_success")
+   */
+  public function formSubmitSuccess() {
+    return $this->render('register_success.html.twig');
+  }
+
+  /**
+   * @Route("{_locale}/player_list", name="player_list")
+   */
+  public function listAction() {
+
+    $doctrine =$this->getDoctrine();
+    $manager = $doctrine->getManager();
+    $repo = $manager->getRepository(Player::class);
+
+    $players = $repo->findAll();
+
+    return $this->render('player_list.html.twig', array(
+      'players' => $players,
+    ));
+
+  }
+
+  /**
+   * @Route("{_locale}/login", name="player_signin")
+   */
+  public function loginAction() {
+
+    $helper = $this->get("security.authentication_utils");
+
+    return $this->render('player_login.html.twig', array(
+      'last_username' => $helper->getLastUsername(),
+      'error' => $helper->getLastAuthenticationError(),
+    ));
+  }
+
+  /**
+   * @Route("{_locale}/logout", name="player_logout")
+   */
+  public function logoutAction() {
+    // This is handled by Symfony, will never get called.
+  }
+}

+ 24 - 0
src/AppBundle/Controller/TestimonialsController.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 23/04/2018
+ * Time: 16:46
+ */
+
+namespace AppBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+
+class TestimonialsController extends Controller
+{
+  /**
+   * This will render the testimonials block.
+   */
+  public function indexAction(Request $request)
+  {
+    return $this->render('testimonials.html.twig');
+  }
+
+}

+ 84 - 0
src/AppBundle/Entity/Contact.php

@@ -0,0 +1,84 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 24/04/2018
+ * Time: 13:33
+ */
+
+namespace AppBundle\Entity;
+
+use Symfony\Component\Validator\Constraints as Assert;
+
+class Contact
+{
+  /**
+   * @var $sender
+   * @Assert\NotBlank(message="form.contact_us.notblank")
+   * @Assert\Email
+   */
+  private $sender;
+
+  /**
+   * @var $subject
+   * @Assert\NotBlank(message="form.contact_us.notblank")
+   * @Assert\Length(min=10,max=50)
+   */
+  private $subject;
+
+  /**
+   * @var $message
+   * @Assert\NotBlank(message="form.contact_us.notblank")
+   */
+  private $message;
+
+  /**
+   * @return mixed
+   */
+  public function getSender()
+  {
+    return $this->sender;
+  }
+
+  /**
+   * @param mixed $sender
+   */
+  public function setSender($sender)
+  {
+    $this->sender = $sender;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getSubject()
+  {
+    return $this->subject;
+  }
+
+  /**
+   * @param mixed $subject
+   */
+  public function setSubject($subject)
+  {
+    $this->subject = $subject;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getMessage()
+  {
+    return $this->message;
+  }
+
+  /**
+   * @param mixed $message
+   */
+  public function setMessage($message)
+  {
+    $this->message = $message;
+  }
+
+
+}

+ 128 - 0
src/AppBundle/Entity/Player.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace AppBundle\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Security\Core\User\UserInterface;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * @ORM\Entity
+ * @ORM\Table(name="players")
+ *
+ * This entity will be stored in database.
+ * You can use console to create database:
+ *  bin/console doctrine:database:create
+ *
+ * And the schema will create the table.
+ *  bin/console doctrine:schema:create
+ *
+ * Also see database configuration in parameters.yml
+ */
+class Player implements UserInterface
+{
+    /**
+     * @ORM\Column(type="integer")
+     * @ORM\Id
+     * @ORM\GeneratedValue(strategy="AUTO")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column
+     * @Assert\NotBlank(message="form.contact_us.notblank")
+     */
+    private $username = '';
+
+    /**
+     * @ORM\Column
+     * @Assert\NotBlank(message="form.contact_us.notblank")
+     */
+    private $fullname = '';
+
+    /**
+     * @ORM\Column
+     * @Assert\Email
+     */
+    private $email = '';
+
+    /**
+     * @ORM\Column(type="date")
+     * @Assert\NotBlank(message="form.contact_us.notblank")
+     */
+    private $dateOfBirth;
+
+    /**
+     * @ORM\Column
+     * @Assert\NotBlank(message="form.contact_us.notblank")
+     */
+    private $password = '';
+
+    public function getUsername(): string
+    {
+        return $this->username;
+    }
+
+    public function setUsername($username)
+    {
+        $this->username = $username;
+    }
+
+    public function getFullname(): string
+    {
+        return $this->fullname;
+    }
+
+    public function setFullname($fullname)
+    {
+        $this->fullname = $fullname;
+    }
+
+    public function getEmail(): string
+    {
+        return $this->email;
+    }
+
+    public function setEmail($email)
+    {
+        $this->email = $email;
+    }
+
+    public function getDateOfBirth(): ?\DateTimeImmutable
+    {
+        if ($this->dateOfBirth instanceof \DateTime) {
+            $this->dateOfBirth = \DateTimeImmutable::createFromMutable($this->dateOfBirth);
+        }
+
+        return $this->dateOfBirth;
+    }
+
+    public function setDateOfBirth(\DateTimeInterface $dateOfBirth)
+    {
+        $this->dateOfBirth = $dateOfBirth;
+    }
+
+    public function getPassword(): string
+    {
+        return $this->password;
+    }
+
+    public function setPassword($password)
+    {
+        $this->password = $password;
+    }
+
+    public function getRoles(): array
+    {
+        return ['ROLE_USER'];
+    }
+
+    public function getSalt(): void
+    {
+    }
+
+    public function eraseCredentials(): void
+    {
+    }
+
+}

+ 29 - 0
src/AppBundle/Form/ContactFormType.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 24/04/2018
+ * Time: 13:36
+ */
+
+namespace AppBundle\Form;
+
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class ContactFormType extends AbstractType
+{
+
+  public function buildForm(FormBuilderInterface $builder, array $options) {
+    $builder
+      ->add('sender', EmailType::class, ['label' => 'form.contact_us.sender'])
+      ->add('subject', TextType::class, ['label' => 'form.contact_us.subject'])
+      ->add('message', TextareaType::class, ['label' => 'form.contact_us.message'])
+      ->add('submit', SubmitType::class, ['label' => 'form.contact_us.submit']);
+  }
+}

+ 32 - 0
src/AppBundle/Form/PlayerFormType.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 25/04/2018
+ * Time: 16:43
+ */
+
+namespace AppBundle\Form;
+
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\DateType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\PasswordType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class PlayerFormType extends AbstractType
+{
+  public function buildForm(FormBuilderInterface $builder, array $options) {
+    $builder
+      ->add('username', TextType::class, ['label' => 'form.player.username'])
+      ->add('email', EmailType::class, ['label' => 'form.player.email'])
+      ->add('fullname', TextType::class, ['label' => 'form.player.fullname'])
+      ->add('dateOfBirth', DateType::class, ['label' => 'form.player.dateofbirth'])
+      ->add('password', PasswordType::class, ['label' => 'form.player.password'])
+      ->add('submit', SubmitType::class, ['label' => 'form.player.submit']);
+  }
+
+}

+ 11 - 0
src/AppBundle/Game/Events.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace AppBundle\Game;
+
+class Events
+{
+  const GAME_START = 'game.start';
+  const GAME_OVER = 'game.over';
+  const GAME_WON = 'game.won';
+  const GAME_FAILED = 'game.failed';
+}

+ 7 - 0
src/AppBundle/Game/Exception/Exception.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace AppBundle\Game\Exception;
+
+class Exception extends \DomainException
+{
+}

+ 7 - 0
src/AppBundle/Game/Exception/LoadingException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace AppBundle\Game\Exception;
+
+class LoadingException extends Exception
+{
+}

+ 9 - 0
src/AppBundle/Game/Exception/LogicException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace AppBundle\Game\Exception;
+
+//use AppBundle\Game\Exception\Exception;
+
+class LogicException extends Exception
+{
+}

+ 7 - 0
src/AppBundle/Game/Exception/RuntimeException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace AppBundle\Game\Exception;
+
+class RuntimeException extends Exception
+{
+}

+ 128 - 0
src/AppBundle/Game/Game.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace AppBundle\Game;
+
+use AppBundle\Game\Exception\Exception;
+use function strtolower;
+
+class Game
+{
+    const MAX_ATTEMPTS = 11;
+
+    private $word;
+    private $attempts;
+    private $triedLetters;
+    private $foundLetters;
+
+    private function __construct(string $word, int $attempts = 0, array $triedLetters = [], array $foundLetters = [])
+    {
+        $this->word = strtolower($word);
+        $this->attempts = $attempts;
+        $this->triedLetters = $triedLetters;
+        $this->foundLetters = $foundLetters;
+    }
+
+    public function getRemainingAttempts(): int
+    {
+        return static::MAX_ATTEMPTS - $this->attempts;
+    }
+
+    public function isLetterFound(string $letter): bool
+    {
+        return in_array(strtolower($letter), $this->foundLetters, true);
+    }
+
+    public function isHanged(): bool
+    {
+        return static::MAX_ATTEMPTS === $this->attempts;
+    }
+
+    public function isOver(): bool
+    {
+        return $this->isWon() || $this->isHanged();
+    }
+
+    public function isWon(): bool
+    {
+        $diff = array_diff($this->getWordLetters(), $this->foundLetters);
+
+        return 0 === count($diff);
+    }
+
+    public function getWord(): string
+    {
+        return $this->word;
+    }
+
+    public function getWordLetters(): array
+    {
+        return str_split($this->word);
+    }
+
+    public function getAttempts(): int
+    {
+        return $this->attempts;
+    }
+
+    public function getTriedLetters(): array
+    {
+        return $this->triedLetters;
+    }
+
+    public function getFoundLetters(): array
+    {
+        return $this->foundLetters;
+    }
+
+    public function reset(): void
+    {
+        $this->attempts = 0;
+        $this->triedLetters = [];
+        $this->foundLetters = [];
+    }
+
+    public function tryWord(string $word): bool
+    {
+        if (strtolower($word) === $this->word) {
+            $this->foundLetters = array_unique($this->getWordLetters());
+
+            return true;
+        }
+
+        $this->attempts = self::MAX_ATTEMPTS;
+
+        return false;
+    }
+
+    public function tryLetter(string $letter): bool
+    {
+        $letter = strtolower($letter);
+
+        if (0 === preg_match('/^[a-z]$/', $letter)) {
+            throw new Exception(sprintf('The letter "%s" is not a valid ASCII character matching [a-z].', $letter));
+        }
+
+        if (in_array($letter, $this->triedLetters, true)) {
+            ++$this->attempts;
+
+            return false;
+        }
+
+        if (false !== strpos($this->word, $letter)) {
+            $this->foundLetters[] = $letter;
+            $this->triedLetters[] = $letter;
+
+            return true;
+        }
+
+        $this->triedLetters[] = $letter;
+        ++$this->attempts;
+
+        return false;
+    }
+
+    public static function createByWord(string $word): self
+    {
+        return new static($word);
+    }
+}

+ 28 - 0
src/AppBundle/Game/GameEvent.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace AppBundle\Game;
+
+use AppBundle\Entity\Player;
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+class GameEvent extends BaseEvent
+{
+    private $player;
+    private $game;
+
+    public function __construct(Player $player, Game $game)
+    {
+        $this->player = $player;
+        $this->game = $game;
+    }
+
+    public function getPlayer(): Player
+    {
+        return $this->player;
+    }
+
+    public function getGame(): Game
+    {
+        return $this->game;
+    }
+}

+ 22 - 0
src/AppBundle/Game/Loader/LoaderInterface.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace AppBundle\Game\Loader;
+
+interface LoaderInterface
+{
+    /**
+     * Loads a words list data source.
+     *
+     * @param string $dictionary The absolute path to a dictionary file
+     *
+     * @return array The list of loaded words
+     */
+    public function load(string $dictionary): array;
+
+    /**
+     * Returns the supported type by loader.
+     *
+     * @return string
+     */
+    public function getType(): string;
+}

+ 19 - 0
src/AppBundle/Game/Loader/TextFileLoader.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace AppBundle\Game\Loader;
+
+class TextFileLoader implements LoaderInterface
+{
+    /**
+     * @inheritdoc
+     */
+    public function load(string $dictionary): array
+    {
+        return array_map('trim', file($dictionary));
+    }
+
+    public function getType(): string
+    {
+        return 'txt';
+    }
+}

+ 25 - 0
src/AppBundle/Game/Loader/XmlFileLoader.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace AppBundle\Game\Loader;
+
+class XmlFileLoader implements LoaderInterface
+{
+    /**
+     * @inheritdoc
+     */
+    public function load(string $dictionary): array
+    {
+        $words = [];
+        $xml = new \SimpleXmlElement(file_get_contents($dictionary));
+        foreach ($xml->word as $word) {
+            $words[] = (string) $word;
+        }
+
+        return $words;
+    }
+
+    public function getType(): string
+    {
+        return 'xml';
+    }
+}

+ 99 - 0
src/AppBundle/Game/Runner.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace AppBundle\Game;
+
+use AppBundle\Game\Exception\LogicException;
+
+class Runner
+{
+    private $storage;
+    private $wordList;
+
+    public function __construct(Storage $storage, WordList $wordList)
+    {
+        $this->storage = $storage;
+        $this->wordList = $wordList;
+    }
+
+    /**
+     * Loads the current game or creates a new one.
+     */
+    public function loadGame(): Game
+    {
+        if ($this->storage->hasGame()) {
+            return $this->storage->loadGame();
+        }
+
+        return $this->createGame();
+    }
+
+    /**
+     * Tests the given letter against the current game.
+     */
+    public function playLetter(string $letter): Game
+    {
+        $game = $this->storage->loadGame();
+
+        $game->tryLetter($letter);
+        $this->storage->save($game);
+
+        return $game;
+    }
+
+    /**
+     * Tests the given word against the current game.
+     */
+    public function playWord(string $word): Game
+    {
+        $game = $this->storage->loadGame();
+
+        $game->tryWord($word);
+        $this->storage->save($game);
+
+        return $game;
+    }
+
+    public function resetGame(): void
+    {
+        $this->storage->reset();
+    }
+
+    public function resetGameOnSuccess(): void
+    {
+        $game = $this->storage->loadGame();
+
+        if (!$game->isOver()) {
+            throw new LogicException('Current game is not yet over.');
+        }
+
+        if (!$game->isWon()) {
+            throw new LogicException('Current game must be won.');
+        }
+
+        $this->resetGame();
+    }
+
+    public function resetGameOnFailure(): void
+    {
+        $game = $this->storage->loadGame();
+
+        if (!$game->isOver()) {
+            throw new LogicException('Current game is not yet over.');
+        }
+
+        if (!$game->isHanged()) {
+            throw new LogicException('Current game must be lost.');
+        }
+
+        $this->resetGame();
+    }
+
+    private function createGame(): Game
+    {
+        $word = $this->wordList->getRandomWord();
+        $game = $this->storage->newGame($word);
+        $this->storage->save($game);
+
+        return $game;
+    }
+}

+ 62 - 0
src/AppBundle/Game/Storage.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace AppBundle\Game;
+
+use AppBundle\Game\Exception\LoadingException;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+class Storage
+{
+    private const STORAGE_KEY = 'hangman';
+
+    private $session;
+
+    public function __construct(SessionInterface $session)
+    {
+        $this->session = $session;
+    }
+
+    /**
+     * Resets the current game context.
+     */
+    public function reset(): void
+    {
+        $this->session->remove(self::STORAGE_KEY);
+    }
+
+    /**
+     * Creates a new Game instance.
+     */
+    public function newGame($word): Game
+    {
+        return Game::createByWord($word);
+    }
+
+    /**
+     * Checks whether a game has already been started.
+     */
+    public function hasGame(): bool
+    {
+        return $this->session->has(self::STORAGE_KEY);
+    }
+
+    /**
+     * Loads an existing game.
+     */
+    public function loadGame(): Game
+    {
+        if (!$this->hasGame()) {
+            throw new LoadingException('There is no game to load.');
+        }
+
+        return $this->session->get(self::STORAGE_KEY);
+    }
+
+    /**
+     * Saves the provided game.
+     */
+    public function save(Game $game): void
+    {
+        $this->session->set(self::STORAGE_KEY, $game);
+    }
+}

+ 78 - 0
src/AppBundle/Game/WordList.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace AppBundle\Game;
+
+use AppBundle\Game\Exception\RuntimeException;
+use AppBundle\Game\Loader\LoaderInterface;
+
+class WordList
+{
+    private $words = [];
+    private $loaders = [];
+    private $loaded = false;
+    private $dictionaries;
+
+    public function __construct(array $dictionaries)
+    {
+        $this->dictionaries = $dictionaries;
+    }
+
+    public function addLoader(LoaderInterface $loader): void
+    {
+        $this->loaders[strtolower($loader->getType())] = $loader;
+        $this->loaded = false;
+    }
+
+    /**
+     * Returns a word picked randomly from the loaded dictionaries.
+     */
+    public function getRandomWord(): string
+    {
+        $this->loadDictionaries();
+
+        return $this->words[array_rand($this->words)];
+    }
+
+    /**
+     * Adds a new word to the list.
+     */
+    public function addWord(string $word): void
+    {
+        if (!in_array($word, $this->words, true)) {
+            $this->words[] = $word;
+        }
+    }
+
+    private function loadDictionaries(): void
+    {
+        if ($this->loaded) {
+            return;
+        }
+
+        foreach ($this->dictionaries as $dictionary) {
+            $this->loadDictionary($dictionary);
+        }
+
+        $this->loaded = true;
+    }
+
+    private function findLoader(string $type): LoaderInterface
+    {
+        $key = strtolower($type);
+        if (!isset($this->loaders[$key])) {
+            throw new RuntimeException(sprintf('There is no loader able to load a %s dictionary.', $type));
+        }
+
+        return $this->loaders[$key];
+    }
+
+    private function loadDictionary(string $path): void
+    {
+        $loader = $this->findLoader(pathinfo($path, PATHINFO_EXTENSION));
+
+        $words = $loader->load($path);
+        foreach ($words as $word) {
+            $this->addWord($word);
+        }
+    }
+}

文件差异内容过多而无法显示
+ 5 - 0
src/AppBundle/Resources/public/app.css


文件差异内容过多而无法显示
+ 1 - 0
src/AppBundle/Resources/public/app.js


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-400.13c25e00.woff2


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-400.721fc276.woff


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-400italic.609847f1.woff2


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-400italic.66ae1aaa.woff


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-700.3c991ad0.woff


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-700.bce26fe5.woff2


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-700italic.97a3ede8.woff


二进制
src/AppBundle/Resources/public/fonts/arvo-latin-700italic.bf080110.woff2


二进制
src/AppBundle/Resources/public/fonts/caveat-brush-latin-400.3dff6a0b.woff2


二进制
src/AppBundle/Resources/public/fonts/caveat-brush-latin-400.c5954593.woff


+ 42 - 0
src/AppBundle/Services/PlayerService.php

@@ -0,0 +1,42 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 26/04/2018
+ * Time: 14:03
+ */
+
+namespace AppBundle\Services;
+
+
+use AppBundle\Entity\Player;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
+
+class PlayerService
+{
+  private $entityManager;
+  private $passwordEncoder;
+
+  function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $passwordEncoder) {
+    $this->entityManager = $entityManager;
+    $this->passwordEncoder = $passwordEncoder;
+  }
+
+  /**
+   * Register a player and encode the password using the config in security.yml for Player entity.
+   *
+   * @param Player $player
+   * @return bool
+   */
+  public function registerPlayer(Player $player) {
+    // Encrypt user password.
+    $player->setPassword($this->passwordEncoder->encodePassword($player, $player->getPassword()));
+
+    // Save the user to database.
+    $this->entityManager->persist($player);
+    $this->entityManager->flush();
+
+    return TRUE;
+  }
+}

+ 18 - 0
tests/AppBundle/Controller/DefaultControllerTest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Tests\AppBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class DefaultControllerTest // extends WebTestCase
+{
+    public function testIndex()
+    {
+        $client = static::createClient();
+
+        $crawler = $client->request('GET', '/');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+        $this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
+    }
+}

+ 31 - 0
tests/AppBundle/Game/GameTest.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 25/04/2018
+ * Time: 14:12
+ */
+
+namespace Tests\AppBundle\Game;
+
+
+use PHPUnit\Framework\TestCase;
+use AppBundle\Game\Game;
+
+class GameTest extends TestCase
+{
+  public function testDefaultAttempts() {
+    $game = Game::createByWord('zzzz');
+
+    $letters = range('a', 'y');
+    $attempts = 0;
+    while (!$game->isOver()) {
+      $game->tryLetter($letters[$attempts]);
+      $attempts++;
+    }
+
+    $this->assertSame($attempts, 11);
+
+  }
+
+}

+ 48 - 0
tests/AppBundle/Game/HangmanTest.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 25/04/2018
+ * Time: 16:06
+ */
+
+namespace Tests\AppBundle\Game;
+
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class HangmanTest extends WebTestCase
+{
+  public function testWinHangman() {
+    $client = static::createClient();
+    $crawler = $client->request('GET', 'en/game');
+
+    $response = $client->getResponse();
+    $this->assertTrue($response->isSuccessful());
+
+    $client->followRedirects(true);
+    $form = $crawler->selectButton('Let me guess...')->form();
+    $crawler = $client->submit($form, ['word' => 'php']);
+
+    $items = $crawler->filter('html:contains("YOU WON!")');
+
+    $this->assertCount(1, $items);
+  }
+
+  public function testLoseHangman() {
+    $client = static::createClient();
+    $crawler = $client->request('GET', 'en/game');
+
+    $response = $client->getResponse();
+    $this->assertTrue($response->isSuccessful());
+
+    $client->followRedirects(true);
+    $form = $crawler->selectButton('Let me guess...')->form();
+    $crawler = $client->submit($form, ['word' => 'imposer']);
+
+    $items = $crawler->filter('html:contains("YOU ARE A LOOSER!")');
+
+    $this->assertCount(1, $items);
+  }
+
+}

+ 68 - 0
tests/AppBundle/Game/WordListTest.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: charles
+ * Date: 25/04/2018
+ * Time: 15:24
+ */
+
+namespace Tests\AppBundle\Game;
+
+
+use AppBundle\Game\Loader\LoaderInterface;
+use AppBundle\Game\WordList;
+use PHPUnit\Framework\TestCase;
+
+class WordListTest extends TestCase
+{
+
+  public function testGettingWord() {
+
+    $stub = $this->provideLoaderInterface(array('word'));
+
+    $wordList = new WordList(['filename.abc']);
+    $wordList->addLoader($stub);
+
+    $word = $wordList->getRandomWord();
+
+    $this->assertSame($word, 'word');
+  }
+
+  public function testAddWord() {
+    $stub = $this->provideLoaderInterface();
+    $wordList = new WordList(['filename.abc']);
+    $wordList->addLoader($stub);
+    $wordList->addWord('word');
+
+    $word = $wordList->getRandomWord();
+    $this->assertSame($word, 'word');
+
+  }
+
+  /**
+   * @expectedException \AppBundle\Game\Exception\RuntimeException
+   */
+  public function testWrongDictionaryType() {
+    $stub = $this->provideLoaderInterface(array('word'));
+
+    $wordList = new WordList(['filename.xxx']);
+    $wordList->addLoader($stub);
+
+    $word = $wordList->getRandomWord();
+
+  }
+
+  private function provideLoaderInterface($wordlist = array()) {
+    $stub = $this->createMock(LoaderInterface::class);
+
+    $stub
+    ->method('getType')
+    ->willReturn('abc');
+
+    $stub
+    ->method('load')
+    ->willReturn($wordlist);
+
+    return $stub;
+  }
+}

+ 817 - 0
var/SymfonyRequirements.php

@@ -0,0 +1,817 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/*
+ * Users of PHP 5.2 should be able to run the requirements checks.
+ * This is why the file and all classes must be compatible with PHP 5.2+
+ * (e.g. not using namespaces and closures).
+ *
+ * ************** CAUTION **************
+ *
+ * DO NOT EDIT THIS FILE as it will be overridden by Composer as part of
+ * the installation/update process. The original file resides in the
+ * SensioDistributionBundle.
+ *
+ * ************** CAUTION **************
+ */
+
+/**
+ * Represents a single PHP requirement, e.g. an installed extension.
+ * It can be a mandatory requirement or an optional recommendation.
+ * There is a special subclass, named PhpIniRequirement, to check a php.ini configuration.
+ *
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class Requirement
+{
+    private $fulfilled;
+    private $testMessage;
+    private $helpText;
+    private $helpHtml;
+    private $optional;
+
+    /**
+     * Constructor that initializes the requirement.
+     *
+     * @param bool        $fulfilled   Whether the requirement is fulfilled
+     * @param string      $testMessage The message for testing the requirement
+     * @param string      $helpHtml    The help text formatted in HTML for resolving the problem
+     * @param string|null $helpText    The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     * @param bool        $optional    Whether this is only an optional recommendation not a mandatory requirement
+     */
+    public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false)
+    {
+        $this->fulfilled = (bool) $fulfilled;
+        $this->testMessage = (string) $testMessage;
+        $this->helpHtml = (string) $helpHtml;
+        $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText;
+        $this->optional = (bool) $optional;
+    }
+
+    /**
+     * Returns whether the requirement is fulfilled.
+     *
+     * @return bool true if fulfilled, otherwise false
+     */
+    public function isFulfilled()
+    {
+        return $this->fulfilled;
+    }
+
+    /**
+     * Returns the message for testing the requirement.
+     *
+     * @return string The test message
+     */
+    public function getTestMessage()
+    {
+        return $this->testMessage;
+    }
+
+    /**
+     * Returns the help text for resolving the problem.
+     *
+     * @return string The help text
+     */
+    public function getHelpText()
+    {
+        return $this->helpText;
+    }
+
+    /**
+     * Returns the help text formatted in HTML.
+     *
+     * @return string The HTML help
+     */
+    public function getHelpHtml()
+    {
+        return $this->helpHtml;
+    }
+
+    /**
+     * Returns whether this is only an optional recommendation and not a mandatory requirement.
+     *
+     * @return bool true if optional, false if mandatory
+     */
+    public function isOptional()
+    {
+        return $this->optional;
+    }
+}
+
+/**
+ * Represents a PHP requirement in form of a php.ini configuration.
+ *
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class PhpIniRequirement extends Requirement
+{
+    /**
+     * Constructor that initializes the requirement.
+     *
+     * @param string        $cfgName           The configuration name used for ini_get()
+     * @param bool|callback $evaluation        Either a boolean indicating whether the configuration should evaluate to true or false,
+     *                                         or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
+     * @param bool          $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
+     *                                         This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
+     *                                         Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
+     * @param string|null   $testMessage       The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
+     * @param string|null   $helpHtml          The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
+     * @param string|null   $helpText          The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     * @param bool          $optional          Whether this is only an optional recommendation not a mandatory requirement
+     */
+    public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false)
+    {
+        $cfgValue = ini_get($cfgName);
+
+        if (is_callable($evaluation)) {
+            if (null === $testMessage || null === $helpHtml) {
+                throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.');
+            }
+
+            $fulfilled = call_user_func($evaluation, $cfgValue);
+        } else {
+            if (null === $testMessage) {
+                $testMessage = sprintf('%s %s be %s in php.ini',
+                    $cfgName,
+                    $optional ? 'should' : 'must',
+                    $evaluation ? 'enabled' : 'disabled'
+                );
+            }
+
+            if (null === $helpHtml) {
+                $helpHtml = sprintf('Set <strong>%s</strong> to <strong>%s</strong> in php.ini<a href="#phpini">*</a>.',
+                    $cfgName,
+                    $evaluation ? 'on' : 'off'
+                );
+            }
+
+            $fulfilled = $evaluation == $cfgValue;
+        }
+
+        parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional);
+    }
+}
+
+/**
+ * A RequirementCollection represents a set of Requirement instances.
+ *
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class RequirementCollection implements IteratorAggregate
+{
+    /**
+     * @var Requirement[]
+     */
+    private $requirements = array();
+
+    /**
+     * Gets the current RequirementCollection as an Iterator.
+     *
+     * @return Traversable A Traversable interface
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->requirements);
+    }
+
+    /**
+     * Adds a Requirement.
+     *
+     * @param Requirement $requirement A Requirement instance
+     */
+    public function add(Requirement $requirement)
+    {
+        $this->requirements[] = $requirement;
+    }
+
+    /**
+     * Adds a mandatory requirement.
+     *
+     * @param bool        $fulfilled   Whether the requirement is fulfilled
+     * @param string      $testMessage The message for testing the requirement
+     * @param string      $helpHtml    The help text formatted in HTML for resolving the problem
+     * @param string|null $helpText    The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     */
+    public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null)
+    {
+        $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false));
+    }
+
+    /**
+     * Adds an optional recommendation.
+     *
+     * @param bool        $fulfilled   Whether the recommendation is fulfilled
+     * @param string      $testMessage The message for testing the recommendation
+     * @param string      $helpHtml    The help text formatted in HTML for resolving the problem
+     * @param string|null $helpText    The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     */
+    public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null)
+    {
+        $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true));
+    }
+
+    /**
+     * Adds a mandatory requirement in form of a php.ini configuration.
+     *
+     * @param string        $cfgName           The configuration name used for ini_get()
+     * @param bool|callback $evaluation        Either a boolean indicating whether the configuration should evaluate to true or false,
+     *                                         or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
+     * @param bool          $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
+     *                                         This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
+     *                                         Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
+     * @param string        $testMessage       The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
+     * @param string        $helpHtml          The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
+     * @param string|null   $helpText          The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     */
+    public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null)
+    {
+        $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false));
+    }
+
+    /**
+     * Adds an optional recommendation in form of a php.ini configuration.
+     *
+     * @param string        $cfgName           The configuration name used for ini_get()
+     * @param bool|callback $evaluation        Either a boolean indicating whether the configuration should evaluate to true or false,
+     *                                         or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
+     * @param bool          $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
+     *                                         This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
+     *                                         Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
+     * @param string        $testMessage       The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
+     * @param string        $helpHtml          The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
+     * @param string|null   $helpText          The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
+     */
+    public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null)
+    {
+        $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true));
+    }
+
+    /**
+     * Adds a requirement collection to the current set of requirements.
+     *
+     * @param RequirementCollection $collection A RequirementCollection instance
+     */
+    public function addCollection(RequirementCollection $collection)
+    {
+        $this->requirements = array_merge($this->requirements, $collection->all());
+    }
+
+    /**
+     * Returns both requirements and recommendations.
+     *
+     * @return Requirement[]
+     */
+    public function all()
+    {
+        return $this->requirements;
+    }
+
+    /**
+     * Returns all mandatory requirements.
+     *
+     * @return Requirement[]
+     */
+    public function getRequirements()
+    {
+        $array = array();
+        foreach ($this->requirements as $req) {
+            if (!$req->isOptional()) {
+                $array[] = $req;
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * Returns the mandatory requirements that were not met.
+     *
+     * @return Requirement[]
+     */
+    public function getFailedRequirements()
+    {
+        $array = array();
+        foreach ($this->requirements as $req) {
+            if (!$req->isFulfilled() && !$req->isOptional()) {
+                $array[] = $req;
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * Returns all optional recommendations.
+     *
+     * @return Requirement[]
+     */
+    public function getRecommendations()
+    {
+        $array = array();
+        foreach ($this->requirements as $req) {
+            if ($req->isOptional()) {
+                $array[] = $req;
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * Returns the recommendations that were not met.
+     *
+     * @return Requirement[]
+     */
+    public function getFailedRecommendations()
+    {
+        $array = array();
+        foreach ($this->requirements as $req) {
+            if (!$req->isFulfilled() && $req->isOptional()) {
+                $array[] = $req;
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * Returns whether a php.ini configuration is not correct.
+     *
+     * @return bool php.ini configuration problem?
+     */
+    public function hasPhpIniConfigIssue()
+    {
+        foreach ($this->requirements as $req) {
+            if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the PHP configuration file (php.ini) path.
+     *
+     * @return string|false php.ini file path
+     */
+    public function getPhpIniConfigPath()
+    {
+        return get_cfg_var('cfg_file_path');
+    }
+}
+
+/**
+ * This class specifies all requirements and optional recommendations that
+ * are necessary to run the Symfony Standard Edition.
+ *
+ * @author Tobias Schultze <http://tobion.de>
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class SymfonyRequirements extends RequirementCollection
+{
+    const LEGACY_REQUIRED_PHP_VERSION = '5.3.3';
+    const REQUIRED_PHP_VERSION = '5.5.9';
+
+    /**
+     * Constructor that initializes the requirements.
+     */
+    public function __construct()
+    {
+        /* mandatory requirements follow */
+
+        $installedPhpVersion = phpversion();
+        $requiredPhpVersion = $this->getPhpRequiredVersion();
+
+        $this->addRecommendation(
+            $requiredPhpVersion,
+            'Vendors should be installed in order to check all requirements.',
+            'Run the <code>composer install</code> command.',
+            'Run the "composer install" command.'
+        );
+
+        if (false !== $requiredPhpVersion) {
+            $this->addRequirement(
+                version_compare($installedPhpVersion, $requiredPhpVersion, '>='),
+                sprintf('PHP version must be at least %s (%s installed)', $requiredPhpVersion, $installedPhpVersion),
+                sprintf('You are running PHP version "<strong>%s</strong>", but Symfony needs at least PHP "<strong>%s</strong>" to run.
+                Before using Symfony, upgrade your PHP installation, preferably to the latest version.',
+                    $installedPhpVersion, $requiredPhpVersion),
+                sprintf('Install PHP %s or newer (installed version is %s)', $requiredPhpVersion, $installedPhpVersion)
+            );
+        }
+
+        $this->addRequirement(
+            version_compare($installedPhpVersion, '5.3.16', '!='),
+            'PHP version must not be 5.3.16 as Symfony won\'t work properly with it',
+            'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)'
+        );
+
+        $this->addRequirement(
+            is_dir(__DIR__.'/../vendor/composer'),
+            'Vendor libraries must be installed',
+            'Vendor libraries are missing. Install composer following instructions from <a href="http://getcomposer.org/">http://getcomposer.org/</a>. '.
+                'Then run "<strong>php composer.phar install</strong>" to install them.'
+        );
+
+        $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache';
+
+        $this->addRequirement(
+            is_writable($cacheDir),
+            'app/cache/ or var/cache/ directory must be writable',
+            'Change the permissions of either "<strong>app/cache/</strong>" or  "<strong>var/cache/</strong>" directory so that the web server can write into it.'
+        );
+
+        $logsDir = is_dir(__DIR__.'/../var/logs') ? __DIR__.'/../var/logs' : __DIR__.'/logs';
+
+        $this->addRequirement(
+            is_writable($logsDir),
+            'app/logs/ or var/logs/ directory must be writable',
+            'Change the permissions of either "<strong>app/logs/</strong>" or  "<strong>var/logs/</strong>" directory so that the web server can write into it.'
+        );
+
+        if (version_compare($installedPhpVersion, '7.0.0', '<')) {
+            $this->addPhpIniRequirement(
+                'date.timezone', true, false,
+                'date.timezone setting must be set',
+                'Set the "<strong>date.timezone</strong>" setting in php.ini<a href="#phpini">*</a> (like Europe/Paris).'
+            );
+        }
+
+        if (false !== $requiredPhpVersion && version_compare($installedPhpVersion, $requiredPhpVersion, '>=')) {
+            $timezones = array();
+            foreach (DateTimeZone::listAbbreviations() as $abbreviations) {
+                foreach ($abbreviations as $abbreviation) {
+                    $timezones[$abbreviation['timezone_id']] = true;
+                }
+            }
+
+            $this->addRequirement(
+                isset($timezones[@date_default_timezone_get()]),
+                sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()),
+                'Your default timezone is not supported by PHP. Check for typos in your <strong>php.ini</strong> file and have a look at the list of deprecated timezones at <a href="http://php.net/manual/en/timezones.others.php">http://php.net/manual/en/timezones.others.php</a>.'
+            );
+        }
+
+        $this->addRequirement(
+            function_exists('iconv'),
+            'iconv() must be available',
+            'Install and enable the <strong>iconv</strong> extension.'
+        );
+
+        $this->addRequirement(
+            function_exists('json_encode'),
+            'json_encode() must be available',
+            'Install and enable the <strong>JSON</strong> extension.'
+        );
+
+        $this->addRequirement(
+            function_exists('session_start'),
+            'session_start() must be available',
+            'Install and enable the <strong>session</strong> extension.'
+        );
+
+        $this->addRequirement(
+            function_exists('ctype_alpha'),
+            'ctype_alpha() must be available',
+            'Install and enable the <strong>ctype</strong> extension.'
+        );
+
+        $this->addRequirement(
+            function_exists('token_get_all'),
+            'token_get_all() must be available',
+            'Install and enable the <strong>Tokenizer</strong> extension.'
+        );
+
+        $this->addRequirement(
+            function_exists('simplexml_import_dom'),
+            'simplexml_import_dom() must be available',
+            'Install and enable the <strong>SimpleXML</strong> extension.'
+        );
+
+        if (function_exists('apc_store') && ini_get('apc.enabled')) {
+            if (version_compare($installedPhpVersion, '5.4.0', '>=')) {
+                $this->addRequirement(
+                    version_compare(phpversion('apc'), '3.1.13', '>='),
+                    'APC version must be at least 3.1.13 when using PHP 5.4',
+                    'Upgrade your <strong>APC</strong> extension (3.1.13+).'
+                );
+            } else {
+                $this->addRequirement(
+                    version_compare(phpversion('apc'), '3.0.17', '>='),
+                    'APC version must be at least 3.0.17',
+                    'Upgrade your <strong>APC</strong> extension (3.0.17+).'
+                );
+            }
+        }
+
+        $this->addPhpIniRequirement('detect_unicode', false);
+
+        if (extension_loaded('suhosin')) {
+            $this->addPhpIniRequirement(
+                'suhosin.executor.include.whitelist',
+                create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'),
+                false,
+                'suhosin.executor.include.whitelist must be configured correctly in php.ini',
+                'Add "<strong>phar</strong>" to <strong>suhosin.executor.include.whitelist</strong> in php.ini<a href="#phpini">*</a>.'
+            );
+        }
+
+        if (extension_loaded('xdebug')) {
+            $this->addPhpIniRequirement(
+                'xdebug.show_exception_trace', false, true
+            );
+
+            $this->addPhpIniRequirement(
+                'xdebug.scream', false, true
+            );
+
+            $this->addPhpIniRecommendation(
+                'xdebug.max_nesting_level',
+                create_function('$cfgValue', 'return $cfgValue > 100;'),
+                true,
+                'xdebug.max_nesting_level should be above 100 in php.ini',
+                'Set "<strong>xdebug.max_nesting_level</strong>" to e.g. "<strong>250</strong>" in php.ini<a href="#phpini">*</a> to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.'
+            );
+        }
+
+        $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null;
+
+        $this->addRequirement(
+            null !== $pcreVersion,
+            'PCRE extension must be available',
+            'Install the <strong>PCRE</strong> extension (version 8.0+).'
+        );
+
+        if (extension_loaded('mbstring')) {
+            $this->addPhpIniRequirement(
+                'mbstring.func_overload',
+                create_function('$cfgValue', 'return (int) $cfgValue === 0;'),
+                true,
+                'string functions should not be overloaded',
+                'Set "<strong>mbstring.func_overload</strong>" to <strong>0</strong> in php.ini<a href="#phpini">*</a> to disable function overloading by the mbstring extension.'
+            );
+        }
+
+        /* optional recommendations follow */
+
+        if (file_exists(__DIR__.'/../vendor/composer')) {
+            require_once __DIR__.'/../vendor/autoload.php';
+
+            try {
+                $r = new ReflectionClass('Sensio\Bundle\DistributionBundle\SensioDistributionBundle');
+
+                $contents = file_get_contents(dirname($r->getFileName()).'/Resources/skeleton/app/SymfonyRequirements.php');
+            } catch (ReflectionException $e) {
+                $contents = '';
+            }
+            $this->addRecommendation(
+                file_get_contents(__FILE__) === $contents,
+                'Requirements file should be up-to-date',
+                'Your requirements file is outdated. Run composer install and re-check your configuration.'
+            );
+        }
+
+        $this->addRecommendation(
+            version_compare($installedPhpVersion, '5.3.4', '>='),
+            'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions',
+            'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.'
+        );
+
+        $this->addRecommendation(
+            version_compare($installedPhpVersion, '5.3.8', '>='),
+            'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156',
+            'Install PHP 5.3.8 or newer if your project uses annotations.'
+        );
+
+        $this->addRecommendation(
+            version_compare($installedPhpVersion, '5.4.0', '!='),
+            'You should not use PHP 5.4.0 due to the PHP bug #61453',
+            'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.'
+        );
+
+        $this->addRecommendation(
+            version_compare($installedPhpVersion, '5.4.11', '>='),
+            'When using the logout handler from the Symfony Security Component, you should have at least PHP 5.4.11 due to PHP bug #63379 (as a workaround, you can also set invalidate_session to false in the security logout handler configuration)',
+            'Install PHP 5.4.11 or newer if your project uses the logout handler from the Symfony Security Component.'
+        );
+
+        $this->addRecommendation(
+            (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<'))
+            ||
+            version_compare($installedPhpVersion, '5.4.8', '>='),
+            'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909',
+            'Install PHP 5.3.18+ or PHP 5.4.8+ if you want nice error messages for all fatal errors in the development environment.'
+        );
+
+        if (null !== $pcreVersion) {
+            $this->addRecommendation(
+                $pcreVersion >= 8.0,
+                sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion),
+                '<strong>PCRE 8.0+</strong> is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Symfony probably works anyway but it is recommended to upgrade your PCRE extension.'
+            );
+        }
+
+        $this->addRecommendation(
+            class_exists('DomDocument'),
+            'PHP-DOM and PHP-XML modules should be installed',
+            'Install and enable the <strong>PHP-DOM</strong> and the <strong>PHP-XML</strong> modules.'
+        );
+
+        $this->addRecommendation(
+            function_exists('mb_strlen'),
+            'mb_strlen() should be available',
+            'Install and enable the <strong>mbstring</strong> extension.'
+        );
+
+        $this->addRecommendation(
+            function_exists('utf8_decode'),
+            'utf8_decode() should be available',
+            'Install and enable the <strong>XML</strong> extension.'
+        );
+
+        $this->addRecommendation(
+            function_exists('filter_var'),
+            'filter_var() should be available',
+            'Install and enable the <strong>filter</strong> extension.'
+        );
+
+        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            $this->addRecommendation(
+                function_exists('posix_isatty'),
+                'posix_isatty() should be available',
+                'Install and enable the <strong>php_posix</strong> extension (used to colorize the CLI output).'
+            );
+        }
+
+        $this->addRecommendation(
+            extension_loaded('intl'),
+            'intl extension should be available',
+            'Install and enable the <strong>intl</strong> extension (used for validators).'
+        );
+
+        if (extension_loaded('intl')) {
+            // in some WAMP server installations, new Collator() returns null
+            $this->addRecommendation(
+                null !== new Collator('fr_FR'),
+                'intl extension should be correctly configured',
+                'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.'
+            );
+
+            // check for compatible ICU versions (only done when you have the intl extension)
+            if (defined('INTL_ICU_VERSION')) {
+                $version = INTL_ICU_VERSION;
+            } else {
+                $reflector = new ReflectionExtension('intl');
+
+                ob_start();
+                $reflector->info();
+                $output = strip_tags(ob_get_clean());
+
+                preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches);
+                $version = $matches[1];
+            }
+
+            $this->addRecommendation(
+                version_compare($version, '4.0', '>='),
+                'intl ICU version should be at least 4+',
+                'Upgrade your <strong>intl</strong> extension with a newer ICU version (4+).'
+            );
+
+            if (class_exists('Symfony\Component\Intl\Intl')) {
+                $this->addRecommendation(
+                    \Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion(),
+                    sprintf('intl ICU version installed on your system is outdated (%s) and does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()),
+                    'To get the latest internationalization data upgrade the ICU system package and the intl PHP extension.'
+                );
+                if (\Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion()) {
+                    $this->addRecommendation(
+                        \Symfony\Component\Intl\Intl::getIcuDataVersion() === \Symfony\Component\Intl\Intl::getIcuVersion(),
+                        sprintf('intl ICU version installed on your system (%s) does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()),
+                        'To avoid internationalization data inconsistencies upgrade the symfony/intl component.'
+                    );
+                }
+            }
+
+            $this->addPhpIniRecommendation(
+                'intl.error_level',
+                create_function('$cfgValue', 'return (int) $cfgValue === 0;'),
+                true,
+                'intl.error_level should be 0 in php.ini',
+                'Set "<strong>intl.error_level</strong>" to "<strong>0</strong>" in php.ini<a href="#phpini">*</a> to inhibit the messages when an error occurs in ICU functions.'
+            );
+        }
+
+        $accelerator =
+            (extension_loaded('eaccelerator') && ini_get('eaccelerator.enable'))
+            ||
+            (extension_loaded('apc') && ini_get('apc.enabled'))
+            ||
+            (extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable'))
+            ||
+            (extension_loaded('Zend OPcache') && ini_get('opcache.enable'))
+            ||
+            (extension_loaded('xcache') && ini_get('xcache.cacher'))
+            ||
+            (extension_loaded('wincache') && ini_get('wincache.ocenabled'))
+        ;
+
+        $this->addRecommendation(
+            $accelerator,
+            'a PHP accelerator should be installed',
+            'Install and/or enable a <strong>PHP accelerator</strong> (highly recommended).'
+        );
+
+        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+            $this->addRecommendation(
+                $this->getRealpathCacheSize() >= 5 * 1024 * 1024,
+                'realpath_cache_size should be at least 5M in php.ini',
+                'Setting "<strong>realpath_cache_size</strong>" to e.g. "<strong>5242880</strong>" or "<strong>5M</strong>" in php.ini<a href="#phpini">*</a> may improve performance on Windows significantly in some cases.'
+            );
+        }
+
+        $this->addPhpIniRecommendation('short_open_tag', false);
+
+        $this->addPhpIniRecommendation('magic_quotes_gpc', false, true);
+
+        $this->addPhpIniRecommendation('register_globals', false, true);
+
+        $this->addPhpIniRecommendation('session.auto_start', false);
+
+        $this->addRecommendation(
+            class_exists('PDO'),
+            'PDO should be installed',
+            'Install <strong>PDO</strong> (mandatory for Doctrine).'
+        );
+
+        if (class_exists('PDO')) {
+            $drivers = PDO::getAvailableDrivers();
+            $this->addRecommendation(
+                count($drivers) > 0,
+                sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'),
+                'Install <strong>PDO drivers</strong> (mandatory for Doctrine).'
+            );
+        }
+    }
+
+    /**
+     * Loads realpath_cache_size from php.ini and converts it to int.
+     *
+     * (e.g. 16k is converted to 16384 int)
+     *
+     * @return int
+     */
+    protected function getRealpathCacheSize()
+    {
+        $size = ini_get('realpath_cache_size');
+        $size = trim($size);
+        $unit = '';
+        if (!ctype_digit($size)) {
+            $unit = strtolower(substr($size, -1, 1));
+            $size = (int) substr($size, 0, -1);
+        }
+        switch ($unit) {
+            case 'g':
+                return $size * 1024 * 1024 * 1024;
+            case 'm':
+                return $size * 1024 * 1024;
+            case 'k':
+                return $size * 1024;
+            default:
+                return (int) $size;
+        }
+    }
+
+    /**
+     * Defines PHP required version from Symfony version.
+     *
+     * @return string|false The PHP required version or false if it could not be guessed
+     */
+    protected function getPhpRequiredVersion()
+    {
+        if (!file_exists($path = __DIR__.'/../composer.lock')) {
+            return false;
+        }
+
+        $composerLock = json_decode(file_get_contents($path), true);
+        foreach ($composerLock['packages'] as $package) {
+            $name = $package['name'];
+            if ('symfony/symfony' !== $name && 'symfony/http-kernel' !== $name) {
+                continue;
+            }
+
+            return (int) $package['version'][1] > 2 ? self::REQUIRED_PHP_VERSION : self::LEGACY_REQUIRED_PHP_VERSION;
+        }
+
+        return false;
+    }
+}

+ 0 - 0
var/cache/.gitkeep


+ 0 - 0
var/logs/.gitkeep


+ 0 - 0
var/sessions/.gitkeep


+ 68 - 0
web/.htaccess

@@ -0,0 +1,68 @@
+# Use the front controller as index file. It serves as a fallback solution when
+# every other rewrite/redirect fails (e.g. in an aliased environment without
+# mod_rewrite). Additionally, this reduces the matching process for the
+# start page (path "/") because otherwise Apache will apply the rewriting rules
+# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
+DirectoryIndex app.php
+
+# By default, Apache does not evaluate symbolic links if you did not enable this
+# feature in your server configuration. Uncomment the following line if you
+# install assets as symlinks or if you experience problems related to symlinks
+# when compiling LESS/Sass/CoffeScript assets.
+# Options FollowSymlinks
+
+# Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve
+# to the front controller "/app.php" but be rewritten to "/app.php/app".
+<IfModule mod_negotiation.c>
+    Options -MultiViews
+</IfModule>
+
+<IfModule mod_rewrite.c>
+    RewriteEngine On
+
+    # Determine the RewriteBase automatically and set it as environment variable.
+    # If you are using Apache aliases to do mass virtual hosting or installed the
+    # project in a subdirectory, the base path will be prepended to allow proper
+    # resolution of the app.php file and to redirect to the correct URI. It will
+    # work in environments without path prefix as well, providing a safe, one-size
+    # fits all solution. But as you do not need it in this case, you can comment
+    # the following 2 lines to eliminate the overhead.
+    RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
+    RewriteRule ^(.*) - [E=BASE:%1]
+
+    # Sets the HTTP_AUTHORIZATION header removed by Apache
+    RewriteCond %{HTTP:Authorization} .
+    RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+
+    # Redirect to URI without front controller to prevent duplicate content
+    # (with and without `/app.php`). Only do this redirect on the initial
+    # rewrite by Apache and not on subsequent cycles. Otherwise we would get an
+    # endless redirect loop (request -> rewrite to front controller ->
+    # redirect -> request -> ...).
+    # So in case you get a "too many redirects" error or you always get redirected
+    # to the start page because your Apache does not expose the REDIRECT_STATUS
+    # environment variable, you have 2 choices:
+    # - disable this feature by commenting the following 2 lines or
+    # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
+    #   following RewriteCond (best solution)
+    RewriteCond %{ENV:REDIRECT_STATUS} ^$
+    RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
+
+    # If the requested filename exists, simply serve it.
+    # We only want to let Apache serve files and not directories.
+    RewriteCond %{REQUEST_FILENAME} -f
+    RewriteRule ^ - [L]
+
+    # Rewrite all other queries to the front controller.
+    RewriteRule ^ %{ENV:BASE}/app.php [L]
+</IfModule>
+
+<IfModule !mod_rewrite.c>
+    <IfModule mod_alias.c>
+        # When mod_rewrite is not available, we instruct a temporary redirect of
+        # the start page to the front controller explicitly so that the website
+        # and the generated links can still be used.
+        RedirectMatch 302 ^/$ /app.php/
+        # RedirectTemp cannot be used instead
+    </IfModule>
+</IfModule>

+ 21 - 0
web/app.php

@@ -0,0 +1,21 @@
+<?php
+
+use Symfony\Component\HttpFoundation\Request;
+
+require __DIR__.'/../vendor/autoload.php';
+if (PHP_VERSION_ID < 70000) {
+    include_once __DIR__.'/../var/bootstrap.php.cache';
+}
+
+$kernel = new AppKernel('prod', false);
+if (PHP_VERSION_ID < 70000) {
+    $kernel->loadClassCache();
+}
+//$kernel = new AppCache($kernel);
+
+// When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
+//Request::enableHttpMethodParameterOverride();
+$request = Request::createFromGlobals();
+$response = $kernel->handle($request);
+$response->send();
+$kernel->terminate($request, $response);

+ 35 - 0
web/app_dev.php

@@ -0,0 +1,35 @@
+<?php
+
+use Symfony\Component\Debug\Debug;
+use Symfony\Component\HttpFoundation\Request;
+
+// If you don't want to setup permissions the proper way, just uncomment the following PHP line
+// read https://symfony.com/doc/current/setup.html#checking-symfony-application-configuration-and-setup
+// for more information
+//umask(0000);
+
+// This check prevents access to debug front controllers that are deployed by accident to production servers.
+// Feel free to remove this, extend it, or make something more sophisticated.
+if (isset($_SERVER['HTTP_CLIENT_IP'])
+    || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
+    || !(in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'], true) || PHP_SAPI === 'cli-server')
+) {
+    header('HTTP/1.0 403 Forbidden');
+    exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
+}
+
+require __DIR__.'/../vendor/autoload.php';
+Debug::enable();
+
+$kernel = new AppKernel('dev', true);
+
+// Use Symfony App Cache
+$kernel = new AppCache($kernel);
+
+if (PHP_VERSION_ID < 70000) {
+    $kernel->loadClassCache();
+}
+$request = Request::createFromGlobals();
+$response = $kernel->handle($request);
+$response->send();
+$kernel->terminate($request, $response);

二进制
web/apple-touch-icon.png


文件差异内容过多而无法显示
+ 422 - 0
web/config.php


二进制
web/favicon.ico


+ 5 - 0
web/robots.txt

@@ -0,0 +1,5 @@
+# www.robotstxt.org/
+# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
+
+User-agent: *
+Disallow: