1<?php
2/*
3 * Copyright 2022 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth\Cache;
19
20use Psr\Cache\CacheItemInterface;
21
22/**
23 * A cache item.
24 */
25final class TypedItem implements CacheItemInterface
26{
27    /**
28     * @var mixed
29     */
30    private mixed $value;
31
32    /**
33     * @var \DateTimeInterface|null
34     */
35    private ?\DateTimeInterface $expiration;
36
37    /**
38     * @var bool
39     */
40    private bool $isHit = false;
41
42    /**
43     * @param string $key
44     */
45    public function __construct(
46        private string $key
47    ) {
48        $this->key = $key;
49        $this->expiration = null;
50    }
51
52    /**
53     * {@inheritdoc}
54     */
55    public function getKey(): string
56    {
57        return $this->key;
58    }
59
60    /**
61     * {@inheritdoc}
62     */
63    public function get(): mixed
64    {
65        return $this->isHit() ? $this->value : null;
66    }
67
68    /**
69     * {@inheritdoc}
70     */
71    public function isHit(): bool
72    {
73        if (!$this->isHit) {
74            return false;
75        }
76
77        if ($this->expiration === null) {
78            return true;
79        }
80
81        return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp();
82    }
83
84    /**
85     * {@inheritdoc}
86     */
87    public function set(mixed $value): static
88    {
89        $this->isHit = true;
90        $this->value = $value;
91
92        return $this;
93    }
94
95    /**
96     * {@inheritdoc}
97     */
98    public function expiresAt($expiration): static
99    {
100        if ($this->isValidExpiration($expiration)) {
101            $this->expiration = $expiration;
102
103            return $this;
104        }
105
106        $error = sprintf(
107            'Argument 1 passed to %s::expiresAt() must implement interface DateTimeInterface, %s given',
108            get_class($this),
109            gettype($expiration)
110        );
111
112        throw new \TypeError($error);
113    }
114
115    /**
116     * {@inheritdoc}
117     */
118    public function expiresAfter($time): static
119    {
120        if (is_int($time)) {
121            $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S"));
122        } elseif ($time instanceof \DateInterval) {
123            $this->expiration = $this->currentTime()->add($time);
124        } elseif ($time === null) {
125            $this->expiration = $time;
126        } else {
127            $message = 'Argument 1 passed to %s::expiresAfter() must be an ' .
128                       'instance of DateInterval or of the type integer, %s given';
129            $error = sprintf($message, get_class($this), gettype($time));
130
131            throw new \TypeError($error);
132        }
133
134        return $this;
135    }
136
137    /**
138     * Determines if an expiration is valid based on the rules defined by PSR6.
139     *
140     * @param mixed $expiration
141     * @return bool
142     */
143    private function isValidExpiration($expiration)
144    {
145        if ($expiration === null) {
146            return true;
147        }
148
149        // We test for two types here due to the fact the DateTimeInterface
150        // was not introduced until PHP 5.5. Checking for the DateTime type as
151        // well allows us to support 5.4.
152        if ($expiration instanceof \DateTimeInterface) {
153            return true;
154        }
155
156        return false;
157    }
158
159    /**
160     * @return \DateTime
161     */
162    protected function currentTime()
163    {
164        return new \DateTime('now', new \DateTimeZone('UTC'));
165    }
166}
167