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 {"": "", ...} and answers {"results": {"": ""}}. * 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]); } }