1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache;
13
14use Psr\Log\LoggerInterface;
15use Symfony\Component\Cache\Exception\InvalidArgumentException;
16use Symfony\Component\Cache\Exception\LogicException;
17use Symfony\Contracts\Cache\ItemInterface;
18
19/**
20 * @author Nicolas Grekas <p@tchwork.com>
21 */
22final class CacheItem implements ItemInterface
23{
24    private const METADATA_EXPIRY_OFFSET = 1527506807;
25
26    protected $key;
27    protected $value;
28    protected $isHit = false;
29    protected $expiry;
30    protected $metadata = [];
31    protected $newMetadata = [];
32    protected $innerItem;
33    protected $poolHash;
34    protected $isTaggable = false;
35
36    /**
37     * {@inheritdoc}
38     */
39    public function getKey(): string
40    {
41        return $this->key;
42    }
43
44    /**
45     * {@inheritdoc}
46     *
47     * @return mixed
48     */
49    public function get()
50    {
51        return $this->value;
52    }
53
54    /**
55     * {@inheritdoc}
56     */
57    public function isHit(): bool
58    {
59        return $this->isHit;
60    }
61
62    /**
63     * {@inheritdoc}
64     *
65     * @return $this
66     */
67    public function set($value): self
68    {
69        $this->value = $value;
70
71        return $this;
72    }
73
74    /**
75     * {@inheritdoc}
76     *
77     * @return $this
78     */
79    public function expiresAt($expiration): self
80    {
81        if (null === $expiration) {
82            $this->expiry = null;
83        } elseif ($expiration instanceof \DateTimeInterface) {
84            $this->expiry = (float) $expiration->format('U.u');
85        } else {
86            throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given.', \is_object($expiration) ? \get_class($expiration) : \gettype($expiration)));
87        }
88
89        return $this;
90    }
91
92    /**
93     * {@inheritdoc}
94     *
95     * @return $this
96     */
97    public function expiresAfter($time): self
98    {
99        if (null === $time) {
100            $this->expiry = null;
101        } elseif ($time instanceof \DateInterval) {
102            $this->expiry = microtime(true) + \DateTime::createFromFormat('U', 0)->add($time)->format('U.u');
103        } elseif (\is_int($time)) {
104            $this->expiry = $time + microtime(true);
105        } else {
106            throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', \is_object($time) ? \get_class($time) : \gettype($time)));
107        }
108
109        return $this;
110    }
111
112    /**
113     * {@inheritdoc}
114     */
115    public function tag($tags): ItemInterface
116    {
117        if (!$this->isTaggable) {
118            throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
119        }
120        if (!is_iterable($tags)) {
121            $tags = [$tags];
122        }
123        foreach ($tags as $tag) {
124            if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) {
125                throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag)));
126            }
127            $tag = (string) $tag;
128            if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
129                continue;
130            }
131            if ('' === $tag) {
132                throw new InvalidArgumentException('Cache tag length must be greater than zero.');
133            }
134            if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
135                throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
136            }
137            $this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
138        }
139
140        return $this;
141    }
142
143    /**
144     * {@inheritdoc}
145     */
146    public function getMetadata(): array
147    {
148        return $this->metadata;
149    }
150
151    /**
152     * Returns the list of tags bound to the value coming from the pool storage if any.
153     *
154     * @deprecated since Symfony 4.2, use the "getMetadata()" method instead.
155     */
156    public function getPreviousTags(): array
157    {
158        @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "getMetadata()" method instead.', __METHOD__), \E_USER_DEPRECATED);
159
160        return $this->metadata[self::METADATA_TAGS] ?? [];
161    }
162
163    /**
164     * Validates a cache key according to PSR-6.
165     *
166     * @param mixed $key The key to validate
167     *
168     * @throws InvalidArgumentException When $key is not valid
169     */
170    public static function validateKey($key): string
171    {
172        if (!\is_string($key)) {
173            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
174        }
175        if ('' === $key) {
176            throw new InvalidArgumentException('Cache key length must be greater than zero.');
177        }
178        if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
179            throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
180        }
181
182        return $key;
183    }
184
185    /**
186     * Internal logging helper.
187     *
188     * @internal
189     */
190    public static function log(?LoggerInterface $logger, string $message, array $context = [])
191    {
192        if ($logger) {
193            $logger->warning($message, $context);
194        } else {
195            $replace = [];
196            foreach ($context as $k => $v) {
197                if (is_scalar($v)) {
198                    $replace['{'.$k.'}'] = $v;
199                }
200            }
201            @trigger_error(strtr($message, $replace), \E_USER_WARNING);
202        }
203    }
204}
205