1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer;
6
7use JMS\Serializer\Exception\LogicException;
8use JMS\Serializer\Exception\RuntimeException;
9use JMS\Serializer\Exclusion\DepthExclusionStrategy;
10use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
11use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
12use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
13use JMS\Serializer\Exclusion\VersionExclusionStrategy;
14use JMS\Serializer\Metadata\ClassMetadata;
15use JMS\Serializer\Metadata\PropertyMetadata;
16use Metadata\MetadataFactory;
17use Metadata\MetadataFactoryInterface;
18
19abstract class Context
20{
21    /**
22     * @var array
23     */
24    private $attributes = [];
25
26    /**
27     * @var string
28     */
29    private $format;
30
31    /**
32     * @var VisitorInterface
33     */
34    private $visitor;
35
36    /**
37     * @var GraphNavigatorInterface
38     */
39    private $navigator;
40
41    /**
42     * @var MetadataFactory
43     */
44    private $metadataFactory;
45
46    /** @var DisjunctExclusionStrategy */
47    private $exclusionStrategy;
48
49    /**
50     * @var bool
51     */
52    private $initialized = false;
53
54    /** @var \SplStack */
55    private $metadataStack;
56
57    public function __construct()
58    {
59    }
60
61    public function initialize(string $format, VisitorInterface $visitor, GraphNavigatorInterface $navigator, MetadataFactoryInterface $factory): void
62    {
63        if ($this->initialized) {
64            throw new LogicException('This context was already initialized, and cannot be re-used.');
65        }
66
67        $this->format = $format;
68        $this->visitor = $visitor;
69        $this->navigator = $navigator;
70        $this->metadataFactory = $factory;
71        $this->metadataStack = new \SplStack();
72
73        if (isset($this->attributes['groups'])) {
74            $this->addExclusionStrategy(new GroupsExclusionStrategy($this->attributes['groups']));
75        }
76
77        if (isset($this->attributes['version'])) {
78            $this->addExclusionStrategy(new VersionExclusionStrategy($this->attributes['version']));
79        }
80
81        if (!empty($this->attributes['max_depth_checks'])) {
82            $this->addExclusionStrategy(new DepthExclusionStrategy());
83        }
84
85        $this->initialized = true;
86    }
87
88    public function getMetadataFactory(): MetadataFactoryInterface
89    {
90        return $this->metadataFactory;
91    }
92
93    public function getVisitor(): VisitorInterface
94    {
95        return $this->visitor;
96    }
97
98    public function getNavigator(): GraphNavigatorInterface
99    {
100        return $this->navigator;
101    }
102
103    public function getExclusionStrategy(): ?ExclusionStrategyInterface
104    {
105        return $this->exclusionStrategy;
106    }
107
108    /**
109     * @return mixed
110     */
111    public function getAttribute(string $key)
112    {
113        return $this->attributes[$key];
114    }
115
116    public function hasAttribute(string $key): bool
117    {
118        return isset($this->attributes[$key]);
119    }
120
121    /**
122     * @param mixed $value
123     */
124    public function setAttribute(string $key, $value): self
125    {
126        $this->assertMutable();
127        $this->attributes[$key] = $value;
128
129        return $this;
130    }
131
132    private function assertMutable(): void
133    {
134        if (!$this->initialized) {
135            return;
136        }
137
138        throw new LogicException('This context was already initialized and is immutable; you cannot modify it anymore.');
139    }
140
141    public function addExclusionStrategy(ExclusionStrategyInterface $strategy): self
142    {
143        $this->assertMutable();
144
145        if (null === $this->exclusionStrategy) {
146            $this->exclusionStrategy = $strategy;
147            return $this;
148        }
149
150        if ($this->exclusionStrategy instanceof DisjunctExclusionStrategy) {
151            $this->exclusionStrategy->addStrategy($strategy);
152            return $this;
153        }
154
155        $this->exclusionStrategy = new DisjunctExclusionStrategy([
156            $this->exclusionStrategy,
157            $strategy,
158        ]);
159
160        return $this;
161    }
162
163    public function setVersion(string $version): self
164    {
165        $this->attributes['version'] = $version;
166
167        return $this;
168    }
169
170    /**
171     * @param array|string $groups
172     */
173    public function setGroups($groups): self
174    {
175        if (empty($groups)) {
176            throw new LogicException('The groups must not be empty.');
177        }
178
179        $this->attributes['groups'] = (array) $groups;
180
181        return $this;
182    }
183
184    public function enableMaxDepthChecks(): self
185    {
186        $this->attributes['max_depth_checks'] = true;
187
188        return $this;
189    }
190
191    public function getFormat(): string
192    {
193        return $this->format;
194    }
195
196    public function pushClassMetadata(ClassMetadata $metadata): void
197    {
198        $this->metadataStack->push($metadata);
199    }
200
201    public function pushPropertyMetadata(PropertyMetadata $metadata): void
202    {
203        $this->metadataStack->push($metadata);
204    }
205
206    public function popPropertyMetadata(): void
207    {
208        $metadata = $this->metadataStack->pop();
209
210        if (!$metadata instanceof PropertyMetadata) {
211            throw new RuntimeException('Context metadataStack not working well');
212        }
213    }
214
215    public function popClassMetadata(): void
216    {
217        $metadata = $this->metadataStack->pop();
218
219        if (!$metadata instanceof ClassMetadata) {
220            throw new RuntimeException('Context metadataStack not working well');
221        }
222    }
223
224    public function getMetadataStack(): \SplStack
225    {
226        return $this->metadataStack;
227    }
228
229    /**
230     * @return array
231     */
232    public function getCurrentPath(): array
233    {
234        if (!$this->metadataStack) {
235            return [];
236        }
237
238        $paths = [];
239        foreach ($this->metadataStack as $metadata) {
240            if ($metadata instanceof PropertyMetadata) {
241                array_unshift($paths, $metadata->name);
242            }
243        }
244
245        return $paths;
246    }
247
248    abstract public function getDepth(): int;
249
250    abstract public function getDirection(): int;
251}
252