Jacek Kowalski
4 hours ago 6b9278a5d57091fce636dbb640b380cfb990db6c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
declare(strict_types=1);
 
namespace App\Controller;
 
use App\Repository\BookRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
 
class InventoryController extends AbstractController {
    public const int MAX_SCANS_PER_REQUEST = 500;
 
    public const string SAVED = 'saved';
    public const string RETURNED = 'returned';
    public const string UNKNOWN_BOOK = 'unknown_book';
    public const string INVALID = 'invalid';
 
    #[Route('/inventory/', name: 'inventory', methods: ['GET'])]
    public function index(): Response {
        return $this->render('inventory/index.html.twig');
    }
 
    /**
     * The service worker that caches the page. It is served by the application rather than from
     * public/ so that its scope covers /inventory/ and so that it knows the URLs to cache.
     */
    #[Route('/inventory/sw.js', name: 'inventory_service_worker', methods: ['GET'])]
    public function serviceWorker(): Response {
        $response = $this->render('inventory/sw.js.twig');
        $response->headers->set('Content-Type', 'text/javascript; charset=UTF-8');
        $response->headers->set('Cache-Control', 'no-cache');
 
        return $response;
    }
 
    #[Route('/inventory/books', name: 'inventory_books', methods: ['GET'])]
    public function books(BookRepository $books): JsonResponse {
        $response = $this->json(['books' => $books->findInventoryData()]);
        $response->headers->set('Cache-Control', 'no-store');
 
        return $response;
    }
 
    /**
     * Accepts {"<book ID>": "<location>", ...} and answers {"results": {"<book ID>": "<status>"}}.
     * Every listed book is marked as seen now.
     * A book found on the shelf while on loan is marked as returned.
     */
    #[Route('/inventory/sync', name: 'inventory_sync', methods: ['POST'])]
    public function sync(Request $request, BookRepository $books, EntityManagerInterface $entityManager): JsonResponse {
        if ($request->getContentTypeFormat() !== 'json') {
            return $this->json(['error' => 'Expected a JSON body.'], Response::HTTP_UNSUPPORTED_MEDIA_TYPE);
        }
 
        // Decoded as an object: as an array, {"0": "..."} would be indistinguishable from a list.
        try {
            $payload = json_decode($request->getContent(), false, 2, \JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            $payload = null;
        }
 
        $scans = $payload instanceof \stdClass ? get_object_vars($payload) : null;
        if ($scans === null || \count($scans) > self::MAX_SCANS_PER_REQUEST) {
            return $this->json(['error' => 'Expected an object mapping at most ' . self::MAX_SCANS_PER_REQUEST . ' book IDs to locations.'], Response::HTTP_BAD_REQUEST);
        }
 
        $valid = [];
        $results = [];
        foreach ($scans as $id => $location) {
            $location = \is_string($location) ? trim($location) : '';
 
            if (!\is_int($id) || $id < 0 || $location === '' || mb_strlen($location) > 255) {
                $results[$id] = self::INVALID;
            } else {
                $valid[$id] = $location;
                $results[$id] = self::UNKNOWN_BOOK;
            }
        }
 
        $now = new \DateTimeImmutable();
        foreach ($valid === [] ? [] : $books->findForInventorySync(array_keys($valid)) as $book) {
            $results[$book->getId()] = self::SAVED;
 
            if ($book->isBorrowed()) {
                $book->giveBack($now);
                $results[$book->getId()] = self::RETURNED;
            }
 
            $book->seenAt($valid[$book->getId()], $now);
        }
 
        $entityManager->flush();
 
        return $this->json(['results' => (object)$results]);
    }
}