Skip to content

6. Data Scopes

A web application serves many users at once, and each user makes numerous requests. Data can therefore be shared at several levels, known as its scope:

  • application scope: the data is shared by all users throughout the application’s lifetime (the list of doctors, for example);
  • request scope: the data exists only for the duration of a request (a view’s model, the logged-in user as identified for that request);
  • session scope: the data is specific to a user and must persist from one request to the next (the selected language, a shopping cart);
  • client scope: data stored in the browser itself, in a cookie.

In [PHP], these scopes behave in a particular way because the application is rebuilt with every request (Example 01): nothing survives in memory from one request to the next. Any data that needs to last longer than a single request is therefore stored outside of [PHP]: in the cache, a database, files, or on the client’s side. This is the so-called “shared-nothing” architecture: each request is processed in isolation by a process that shares nothing with those processing other requests. This approach has significant advantages: an error affects only a single request, no data leaks from one user to another, and multiple identical servers can be run behind a load balancer, provided they share the same cache and database.

In [Laravel], each scope has its own container:

Scope
Container
Lifetime
application
the cache ([Cache] front end, example 20), or the database
the selected duration, or unlimited
query
the container’s objects (a singleton exists for only one query), the query’s attributes (example 12)
processing a query
session
the session ([$request->session()], [session()]), stored by the server and retrieved using a cookie
Default 2-hour inactivity period ([SESSION_LIFETIME])
client
a cookie ([withCookie], example 22), encrypted by default
the cookie’s specified duration

The session for [Laravel] is a server-maintained session, like those for [Java] ([JSESSIONID]) or the classic [PHP] ([PHPSESSID]): its data is stored on the server (one file per session, with the configuration from our examples), and the browser only holds the identifier in an encrypted cookie (Example 20). This differs from [Flask], whose entire session is stored in a signed cookie on the client side.

6.1. Example 23: Scopes

23-scopes/
├── app/
│   ├── Http/
│   │   └── Controllers/
│   │       └── ScopesController.php
│   └── Services/
│       ├── CounterService.php
│       └── RequestInfo.php
├── resources/
│   └── views/
│       └── scopes/
│           └── scopes.blade.php
└── ...

6.1.1. The objects: app/Services/CounterService.php and RequestInfo.php

<?php
// app/Services/CounterService.php: APPLICATION scope, a counter shared by
// all the requests of all the users, stored in the cache (see example 20)

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class CounterService
{
    // one more number under the [$key] key; the "file" cache does not increase a
    // number atomically: a lock protects the reading and the writing
    public static function next(string $key): int
    {
        return Cache::lock("$key-lock", 10)->block(5, function () use ($key) {
            $value = Cache::get($key, 0) + 1;
            Cache::forever($key, $value);
            return $value;
        });
    }

    public function increment(): int
    {
        return self::next('visits');
    }
}
  • Lines 13–20: an application scope counter: a number stored in the cache under a key and incremented by 1 on each call. The cache provides a method [Cache::increment], but with the “file” store used in our examples, it is not atomic: it reads the value, increments it, and then writes it, and two concurrent requests could read the same value—resulting in a lost visit. A lock (example 20) therefore protects both the read and the write operations. (With a store such as [Redis] or a database, [Cache::increment] is atomic, and the lock is unnecessary.) The function returns the value of the lock, which is the same as the value returned by the function it protects;
  • lines 22–25: the visit counter.
<?php
// app/Services/RequestInfo.php: REQUEST scope, the information of a request

namespace App\Services;

class RequestInfo
{
    public readonly int $number;

    public readonly string $start;

    public function __construct()
    {
        // one number per OBJECT created, taken from a counter shared by all the requests
        $this->number = CounterService::next('instances');
        // the time, with its milliseconds (v)
        $this->start = now()->format('H:i:s.v');
    }
}
  • lines 8–10: properties declared as [readonly] one by one (rather than the entire class): they can only be assigned once, by the constructor;
  • lines 12–18: request-scope information: a number, derived from a counter shared by all requests (the counter from the previous class, under a different key), and the time of the request. The function [now()] returns the current time as a [Carbon] date; the format [v] specifies the milliseconds.

6.1.2. Configuration: bootstrap/app.php

<?php
// bootstrap/app.php: the application of example 23

use App\Services\CounterService;
use App\Services\RequestInfo;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

(require __DIR__.'/../../vendor/autoload.php')->addPsr4('App\\', __DIR__.'/../app/');

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        // the route that serves Bootstrap (see example 15)
        then: fn () => require __DIR__.'/../../common/bootstrap.php')
    ->withMiddleware(function (Middleware $middleware): void {
        // the [myVisits] cookie is not encrypted (the others are)
        $middleware->encryptCookies(except: ['myVisits']);
    })
    ->withExceptions()
    // a single object of each class during the handling of a request
    ->withSingletons([CounterService::class])
    ->withScopedSingletons([RequestInfo::class])
    ->create();
  • Line 18: The cookie [myVisits] is not encrypted; all others are (Example 22);
  • Lines 22–23: Both services are registered with the container. [withSingletons] (example 06): a single object, created on the first request. [withScopedSingletons]: a single object per request. In a standard [PHP], the two are equivalent, since each request rebuilds the application and its container. The difference becomes apparent with [Laravel Octane], an application server that keeps the application in memory from one request to the next to improve performance: a singleton is shared by all requests, while a “scoped” singleton is recreated for each one. Declaring the desired scope ensures the code is correct in both cases.

6.1.3. The controller: app/Http/Controllers/ScopesController.php

<?php
// app/Http/Controllers/ScopesController.php: the four scopes of data

namespace App\Http\Controllers;

use App\Services\CounterService;
use App\Services\RequestInfo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class ScopesController
{
    // the two objects are provided by the container (see bootstrap/app.php)
    public function display(Request $request, CounterService $counter, RequestInfo $info): Response
    {
        // APPLICATION: the shared counter
        $total = $counter->increment();
        // SESSION: data specific to THIS browser, kept by the SERVER
        // (a file of storage/framework/sessions), found again thanks to the session cookie
        $sessionVisits = $request->session()->increment('visits');
        // CLIENT: data kept by the BROWSER in cookies,
        // sent back to the server with each request; one in clear text, the other encrypted
        $myVisits = (int) $request->cookie('myVisits') + 1;
        $myEncryptedVisits = (int) $request->cookie('myEncryptedVisits') + 1;
        return response()->view('scopes.scopes', [
            'total' => $total, 'instance' => $info->number,
            'sessionVisits' => $sessionVisits, 'myVisits' => $myVisits,
            'myEncryptedVisits' => $myEncryptedVisits,
        ])->withCookie(cookie('myVisits', $myVisits))
            ->withCookie(cookie('myEncryptedVisits', $myEncryptedVisits));
    }
}
  • line 14: the action receives the request and the two services, provided by the container;
  • line 17: the application scope;
  • line 20: the session scope. [$request->session()] returns the user’s session; its method [increment] increments the value of a key by 1 (or sets it to 0 if it does not exist), and returns the new value. A value is read using [get('visits')] and written using [put('visits', 3)]; the function [session('visits')] is a shortcut for reading. [Laravel] saves the session at the end of the request (the [StartSession] middleware from the [web] group);
  • lines 23–24: the client scope, with two cookies. Their values are strings provided by the browser and may therefore be missing or invalid: the [(int)] conversion returns 0 for [null] or for text that is not a number;
  • Lines 25–30: To set a cookie, you need the response object: [response()->view(...)] constructs it from the view (Example 13), and then [withCookie] adds the cookies to it. Without an expiration time, a cookie lasts until the browser is closed (this is a “session” cookie, as defined by browsers).

6.1.4. The view: resources/views/scopes/scopes.blade.php

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Scopes</title>
  <link rel="stylesheet" href="{{ route('bootstrap', 'css/bootstrap.min.css') }}">
</head>
<body>
  <div class="container py-3">
    <h1 class="h3">The scopes of data</h1>
    {{-- @inject: an object requested from the container, from the view; during the SAME
         request, it is the same object as the one of the action --}}
    @inject('info', 'App\Services\RequestInfo')
    <ul>
      <li>application: {{ $total }} visit(s) to the site, all browsers combined</li>
      <li>request: object no. {{ $instance }} (no. {{ $info->number }} in the view),
        request received at {{ $info->start }}</li>
      <li>session: {{ $sessionVisits }} visit(s) from this browser ({{ session('visits') }} in the view)</li>
      <li>client: {{ $myVisits }} visit(s) from this browser (clear-text cookie),
        {{ $myEncryptedVisits }} (encrypted cookie)</li>
    </ul>
  </div>
</body>
</html>
  • Line 13: The [@inject] directive requests an object from the container, from within the view, and stores it in a variable (here, [$info]). During the same request, this is the same [RequestInfo] object as the one from the action: the view displays its number, which must be the one received from the action;
  • Line 18: The session is also accessible in the views.

6.1.5. Execution

After a few visits in a browser:

Image

Let’s track the requests with [curl], which acts as a first browser by storing its cookies in the file [a.txt]:

curl -i -c a.txt -b a.txt http://localhost:8000/
HTTP/1.1 200 OK
Set-Cookie: myVisits=1; path=/; httponly; samesite=lax
Set-Cookie: myEncryptedVisits=eyJpdiI6InFOdXY5QmhoSlNCTWx4Q0VmN1lDSUE9PSIsInZhbHVlIjoiNm1GckpjQ3JK...; path=/; httponly; samesite=lax
Set-Cookie: XSRF-TOKEN=eyJpdiI6Im5IcHgvN2RRWiswNEZkMFRrUHBkOEE9PSIsInZhbHVlIjoiMTk1eElxemZz...; expires=...; Max-Age=7200; path=/; samesite=lax
Set-Cookie: example23_session=eyJpdiI6IjVEbjh1V2toeTdCdUFhSDdWWkV3ZHc9PSIsInZhbHVlIjoiK1J5SVNi...; expires=...; Max-Age=7200; path=/; httponly; samesite=lax
...
      <li>application: 1 visit(s) to the site, all browsers combined</li>
      <li>request: object no. 1 (no. 1 in the view),
        request received at 16:19:05.722</li>
      <li>session: 1 visit(s) from this browser (1 in the view)</li>
      <li>client: 1 visit(s) from this browser (clear-text cookie),
        1 (encrypted cookie)</li>
...

The response sets four cookies. The first, [myVisits], contains the value in plain text. The second, [myEncryptedVisits], contains the same value, encrypted: the visible part is a Base64-encoded string (JSON), which contains the encryption initialization vector ([iv]), the encrypted value ([value]), and an authentication code ([mac]), which ensures that the value has not been altered. The last two are for the session and anti-CSRF protection (example 20). A second visit, with the same cookies:

curl -i -c a.txt -b a.txt http://localhost:8000/
...
Set-Cookie: myVisits=2; path=/; httponly; samesite=lax
...
      <li>application: 2 visit(s) to the site, all browsers combined</li>
      <li>request: object no. 2 (no. 2 in the view),
        request received at 16:19:05.922</li>
      <li>session: 2 visit(s) from this browser (2 in the view)</li>
      <li>client: 2 visit(s) from this browser (clear-text cookie),
        2 (encrypted cookie)</li>
...

A second “browser,” with its own cookie file, which is initially empty:

curl -c b.txt -b b.txt http://localhost:8000/
1
2
3
4
5
6
      <li>application: 3 visit(s) to the site, all browsers combined</li>
      <li>request: object no. 3 (no. 3 in the view),
        request received at 16:19:06.027</li>
      <li>session: 1 visit(s) from this browser (1 in the view)</li>
      <li>client: 1 visit(s) from this browser (clear-text cookie),
        1 (encrypted cookie)</li>

The application counter continued running; the session and cookies for the second browser start over at 1. Each request had its own [RequestInfo] object, and in each one, the action and view were the same. Finally, a malicious user forges their own cookies: 1,000 visits in [myVisits], 999 in [myEncryptedVisits], and a session ID of their own invention:

curl -b "myVisits=1000;myEncryptedVisits=999;example23_session=abc" http://localhost:8000/
1
2
3
4
5
      <li>application: 4 visit(s) to the site, all browsers combined</li>
      ...
      <li>session: 1 visit(s) from this browser (1 in the view)</li>
      <li>client: 1001 visit(s) from this browser (clear-text cookie),
        1 (encrypted cookie)</li>

The unencrypted cookie was accepted as is: 1,001 visits. The cookie [myEncryptedVisits], however, could not be decrypted: [Laravel] ignored it, as if it were absent. The same applies to the session cookie: the user cannot choose another user’s session and is assigned a new one. This is the key difference between a regular cookie and an encrypted cookie. Let’s remember the rule: data coming from the client is trustworthy only if it is protected by the server (encrypted, as here, or signed, like the token in the chapter on authentication); data that has nothing to hide, and whose value is verified by the server (the language in Example 22), can remain in plaintext.