diff --git a/session_example/session_example.info.yml b/session_example/session_example.info.yml index 780019c..63bbd62 100644 --- a/session_example/session_example.info.yml +++ b/session_example/session_example.info.yml @@ -4,4 +4,4 @@ description: 'An example of how to use session in Drupal 8.' package: Example modules core: 8.x dependencies: - - examples + - examples:examples diff --git a/session_example/session_example.links.task.yml b/session_example/session_example.links.task.yml index c2c0067..7dac047 100644 --- a/session_example/session_example.links.task.yml +++ b/session_example/session_example.links.task.yml @@ -1,3 +1,4 @@ +# The Edit and View tabs for the session example pages. session_example.form: route_name: session_example.form base_route: session_example.form diff --git a/session_example/src/Controller/SessionExampleController.php b/session_example/src/Controller/SessionExampleController.php index da6e23e..d1f698c 100644 --- a/session_example/src/Controller/SessionExampleController.php +++ b/session_example/src/Controller/SessionExampleController.php @@ -7,6 +7,8 @@ use Symfony\Component\HttpFoundation\Request; /** * Controller for a page to display the session information. + * + * @ingroup session_example */ class SessionExampleController extends ControllerBase { @@ -29,13 +31,20 @@ class SessionExampleController extends ControllerBase { // Make a table of the session information. $row = []; foreach (['name', 'email', 'quest', 'color'] as $item) { - $key = 'session_example.' . $item; + $key = "session_example.$item"; // Get the session value, with a default of 'No name' etc. for each type // of information we have. $row[0][$item] = $session->get($key, $this->t('No @type', ['@type' => $item])); } return [ + // Since this page will be cached, we have to manage the caching. We'll + // use a cache tag and manage it within the session helper. We use the + // session ID to guarantee a unique tag per session. The submission form + // will manage invalidating this tag. + '#cache' => [ + 'tags' => ['session_example:' . $session->getId()], + ], 'description' => [ '#type' => 'item', '#title' => $this->t('Saved Session Keys'), diff --git a/session_example/src/Form/SessionExampleForm.php b/session_example/src/Form/SessionExampleForm.php index b0e54cc..bf70160 100644 --- a/session_example/src/Form/SessionExampleForm.php +++ b/session_example/src/Form/SessionExampleForm.php @@ -6,10 +6,19 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Link; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Form to allow the user to store information in their session. + * + * In this object we'll inject the session service. In the submission form we + * got the session from a Request object supplied by the routing system. Either + * of these work, because they're the same object. But we use injection here + * because the buildForm() method does not have an easy way to derive the + * Request object or the session. + * + * @ingroup session_example */ class SessionExampleForm extends FormBase { @@ -23,15 +32,22 @@ class SessionExampleForm extends FormBase { */ protected $session; + /** + * The cache tag invalidator service. + * + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface + */ + protected $cache_tag_invalidator; + /** * Constructs a new SessionExampleForm object. * - * @param \Symfony\Component\HttpFoundation\RequestStack - * The request stack object. + * @param \Symfony\Component\HttpFoundation\Session\SessionInterface + * The session object. */ - public function __construct(RequestStack $request) { - // We grab the session object from the current request. - $this->session = $request->getCurrentRequest()->getSession(); + public function __construct(SessionInterface $session, CacheTagsInvalidatorInterface $invalidator) { + $this->session = $session; + $this->cache_tag_invalidator = $invalidator; } /** @@ -39,7 +55,8 @@ class SessionExampleForm extends FormBase { */ public static function create(ContainerInterface $container) { return new static( - $container->get('request_stack') + $container->get('session'), + $container->get('cache_tags.invalidator') ); } @@ -102,6 +119,28 @@ class SessionExampleForm extends FormBase { return $form; } + /** + * Store a form value in the session. + * + * Form values are always a string. This means an empty string is a valid + * value for when a user wants to remove a value from the session. We have to + * handle this special case for the session object. + * + * @param string $key + * The key. + * @param string $value + * The value. + */ + protected function setSessionValue($key, $value) { + if (empty($value)) { + // If the value is an empty string, remove the key from the session. + $this->session->remove($key); + } + else { + $this->session->set($key, $value); + } + } + /** * {@inheritdoc} */ @@ -115,24 +154,12 @@ class SessionExampleForm extends FormBase { $this->setSessionValue('session_example.color', $form_state->getValue('color')); // Tell the user what happened here, and that they can look at another page // to see the result. - $link = Link::createFromRoute('Check here.', 'session_example.view'); - $this->messenger()->addMessage($this->t('The session has been saved successfully. @link', ['@link' => $link->toString()])); - } - - /** - * Store a new value in the session. - * - * The form value is always a string. This means an empty string is a valid - * value for when a user wants to remove a value from the session. We have to - * handle this special case for the session object. - */ - protected function setSessionValue($key, $value) { - if (empty($value)) { - $this->session->remove($key); - } - else { - $this->session->set($key, $value); - } + $this->messenger()->addMessage($this->t('The session has been saved successfully. @link', [ + '@link' => Link::createFromRoute('Check here.', 'session_example.view')->toString() + ])); + // Since we might have changed the session information, we will invalidate + // the cache tag for this session. + $this->invalidateCacheTag(); } /** @@ -149,6 +176,19 @@ class SessionExampleForm extends FormBase { $this->session->remove($item); } $this->messenger()->addMessage($this->t('Session is cleared.')); + // Since we might have changed the session information, we will invalidate + // the cache tag for this session. + $this->invalidateCacheTag(); + } + + /** + * Invalidate the cache tag for this session. + * + * The form will use this method to invalidate the cache tag when the user + * updates their information in the submit handlers. + */ + protected function invalidateCacheTag() { + $this->cache_tag_invalidator->invalidateTags(['session_example:' . $this->session->getId()]); } } diff --git a/session_example/tests/src/Functional/SessionExampleTest.php b/session_example/tests/src/Functional/SessionExampleTest.php index 8d15358..48fed49 100644 --- a/session_example/tests/src/Functional/SessionExampleTest.php +++ b/session_example/tests/src/Functional/SessionExampleTest.php @@ -32,7 +32,7 @@ class SessionExampleTest extends BrowserTestBase { * Test all the routes, and ensure that forms can be submitted. */ public function testSessionExampleLinks() { - $assertion = $this->assertSession(); + $assert = $this->assertSession(); // Routes with menu links, and their form buttons. $routes_with_menu_links = [ @@ -42,7 +42,7 @@ class SessionExampleTest extends BrowserTestBase { // Ensure the links appear in the tools menu sidebar. $this->drupalGet(''); foreach (array_keys($routes_with_menu_links) as $route) { - $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + $assert->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); } // All our routes with their form buttons. @@ -55,41 +55,42 @@ class SessionExampleTest extends BrowserTestBase { foreach ($routes as $route => $buttons) { $url = Url::fromRoute($route); $this->drupalGet($url); - $assertion->statusCodeEquals(200); + $assert->statusCodeEquals(200); foreach ($buttons as $button) { $this->drupalPostForm($url, [], $button); - $assertion->statusCodeEquals(200); + $assert->statusCodeEquals(200); } } } /** - * Basic tests for Session Example. + * Functional tests for the session example. */ public function testSessionExample() { + $assert = $this->assertSession(); // Get the form and verify that it has placeholders. $this->drupalGet(Url::fromRoute('session_example.form')); - $this->assertRaw('placeholder="Your name."', 'Name placeholder contains Your name'); - $this->assertRaw('placeholder="Your email address."', 'Email placeholder contains Your email address.'); - $this->assertRaw('placeholder="What is your quest?"', 'Name placeholder contains What is your quest?'); + $assert->responseContains('placeholder="Your name."', 'Name placeholder contains Your name'); + $assert->responseContains('placeholder="Your email address."', 'Email placeholder contains Your email address.'); + $assert->responseContains('placeholder="What is your quest?"', 'Quest placeholder contains What is your quest?'); // Get the report and verify that it doesn't show any session information. $this->clickLink('View'); - $this->assertText('No name'); - $this->assertText('No email'); - $this->assertText('No quest'); - $this->assertText('No color'); + $assert->pageTextContains('No name'); + $assert->pageTextContains('No email'); + $assert->pageTextContains('No quest'); + $assert->pageTextContains('No color'); // Save an empty session submission. $this->drupalPostForm(Url::fromRoute('session_example.form'), [], 'Save'); - $this->assertText('The session has been saved successfully.'); + $assert->pageTextContains('The session has been saved successfully.'); // Make sure an empty session submission still has no reported information. $this->clickLink('Check here'); - $this->assertText('No name'); - $this->assertText('No email'); - $this->assertText('No quest'); - $this->assertText('No color'); + $assert->pageTextContains('No name'); + $assert->pageTextContains('No email'); + $assert->pageTextContains('No quest'); + $assert->pageTextContains('No color'); // Submit some session information. $form_data = [ @@ -101,19 +102,50 @@ class SessionExampleTest extends BrowserTestBase { // Check that the report shows our information. $this->clickLink('Check here'); - $this->assertText('Sir Lancelot'); - $this->assertText('To seek the Grail'); - $this->assertText('blue'); + foreach ($form_data as $value) { + $assert->pageTextContains($value); + } // Clear the session. $this->drupalPostForm(Url::fromRoute('session_example.form'), [], 'Clear Session'); - $this->assertText('Session is cleared.'); + $assert->pageTextContains('Session is cleared.'); // Verify that the session information doesn't show Sir Lancelot (or anyone // else). $this->clickLink('View'); - $this->assertText('No name'); - $this->assertText('No email'); + $assert->pageTextContains('No name'); + $assert->pageTextContains('No email'); + $assert->pageTextContains('No quest'); + $assert->pageTextContains('No color'); + } + + public function testUserIsolation() { + $assert = $this->assertSession(); + // Our setUp() method has already logged in a user, so let's add some data. + $form_data = [ + 'name' => 'Sir Lancelot', + 'quest' => 'To seek the Grail', + 'color' => 'blue', + ]; + $this->drupalPostForm(Url::fromRoute('session_example.form'), $form_data, 'Save'); + + // Check that the report shows our information. + $this->clickLink('Check here'); + foreach ($form_data as $value) { + $assert->pageTextContains($value); + } + + // Let's log in a new user and make sure they can't see the other user's + // data. + $this->drupalLogin( + $this->createUser(['access content']) + ); + $this->drupalGet(Url::fromRoute('session_example.view')); + $assert->statusCodeEquals(200); + $assert->pageTextContains('No name'); + $assert->pageTextContains('No email'); + $assert->pageTextContains('No quest'); + $assert->pageTextContains('No color'); } }