1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer;
6
7use JMS\Serializer\ContextFactory\DefaultDeserializationContextFactory;
8use JMS\Serializer\ContextFactory\DefaultSerializationContextFactory;
9use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
10use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
11use JMS\Serializer\Exception\InvalidArgumentException;
12use JMS\Serializer\Exception\RuntimeException;
13use JMS\Serializer\Exception\UnsupportedFormatException;
14use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface;
15use JMS\Serializer\Type\Parser;
16use JMS\Serializer\Type\ParserInterface;
17use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory;
18use JMS\Serializer\Visitor\Factory\SerializationVisitorFactory;
19use Metadata\MetadataFactoryInterface;
20
21/**
22 * Serializer Implementation.
23 *
24 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
25 */
26final class Serializer implements SerializerInterface, ArrayTransformerInterface
27{
28    /**
29     * @var MetadataFactoryInterface
30     */
31    private $factory;
32
33    /**
34     * @var TypeParser
35     */
36    private $typeParser;
37
38    /**
39     * @var SerializationVisitorFactory[]
40     */
41    private $serializationVisitors = [];
42
43    /**
44     * @var DeserializationVisitorFactory[]
45     */
46    private $deserializationVisitors = [];
47
48    /**
49     * @var SerializationContextFactoryInterface
50     */
51    private $serializationContextFactory;
52
53    /**
54     * @var DeserializationContextFactoryInterface
55     */
56    private $deserializationContextFactory;
57
58    /**
59     * @var GraphNavigatorFactoryInterface[]
60     */
61    private $graphNavigators;
62
63    /**
64     * @param GraphNavigatorFactoryInterface[] $graphNavigators
65     * @param SerializationVisitorFactory[] $serializationVisitors
66     * @param DeserializationVisitorFactory[] $deserializationVisitors
67     */
68    public function __construct(
69        MetadataFactoryInterface $factory,
70        array $graphNavigators,
71        array $serializationVisitors,
72        array $deserializationVisitors,
73        ?SerializationContextFactoryInterface $serializationContextFactory = null,
74        ?DeserializationContextFactoryInterface $deserializationContextFactory = null,
75        ?ParserInterface $typeParser = null
76    ) {
77        $this->factory = $factory;
78        $this->graphNavigators = $graphNavigators;
79        $this->serializationVisitors = $serializationVisitors;
80        $this->deserializationVisitors = $deserializationVisitors;
81
82        $this->typeParser = $typeParser ?? new Parser();
83
84        $this->serializationContextFactory = $serializationContextFactory ?: new DefaultSerializationContextFactory();
85        $this->deserializationContextFactory = $deserializationContextFactory ?: new DefaultDeserializationContextFactory();
86    }
87
88    /**
89     * Parses a direction string to one of the direction constants.
90     */
91    public static function parseDirection(string $dirStr): int
92    {
93        switch (strtolower($dirStr)) {
94            case 'serialization':
95                return GraphNavigatorInterface::DIRECTION_SERIALIZATION;
96
97            case 'deserialization':
98                return GraphNavigatorInterface::DIRECTION_DESERIALIZATION;
99
100            default:
101                throw new InvalidArgumentException(sprintf('The direction "%s" does not exist.', $dirStr));
102        }
103    }
104
105    private function findInitialType(?string $type, SerializationContext $context): ?string
106    {
107        if (null !== $type) {
108            return $type;
109        } elseif ($context->hasAttribute('initial_type')) {
110            return $context->getAttribute('initial_type');
111        }
112        return null;
113    }
114
115    private function getNavigator(int $direction): GraphNavigatorInterface
116    {
117        if (!isset($this->graphNavigators[$direction])) {
118            throw new RuntimeException(
119                sprintf(
120                    'Can not find a graph navigator for the direction "%s".',
121                    GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction ? 'serialization' : 'deserialization'
122                )
123            );
124        }
125
126        return $this->graphNavigators[$direction]->getGraphNavigator();
127    }
128
129    private function getVisitor(int $direction, string $format): VisitorInterface
130    {
131        $factories = GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction
132            ? $this->serializationVisitors
133            : $this->deserializationVisitors;
134
135        if (!isset($factories[$format])) {
136            throw new UnsupportedFormatException(
137                sprintf(
138                    'The format "%s" is not supported for %s.',
139                    $format,
140                    GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction ? 'serialization' : 'deserialization'
141                )
142            );
143        }
144
145        return $factories[$format]->getVisitor();
146    }
147
148    /**
149     * {@InheritDoc}
150     */
151    public function serialize($data, string $format, ?SerializationContext $context = null, ?string $type = null): string
152    {
153        if (null === $context) {
154            $context = $this->serializationContextFactory->createSerializationContext();
155        }
156
157        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $format);
158        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_SERIALIZATION);
159
160        $type = $this->findInitialType($type, $context);
161
162        $result = $this->visit($navigator, $visitor, $context, $data, $format, $type);
163        return $visitor->getResult($result);
164    }
165
166    /**
167     * {@InheritDoc}
168     */
169    public function deserialize(string $data, string $type, string $format, ?DeserializationContext $context = null)
170    {
171        if (null === $context) {
172            $context = $this->deserializationContextFactory->createDeserializationContext();
173        }
174
175        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $format);
176        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_DESERIALIZATION);
177
178        $result = $this->visit($navigator, $visitor, $context, $data, $format, $type);
179
180        return $visitor->getResult($result);
181    }
182
183    /**
184     * {@InheritDoc}
185     */
186    public function toArray($data, ?SerializationContext $context = null, ?string $type = null): array
187    {
188        if (null === $context) {
189            $context = $this->serializationContextFactory->createSerializationContext();
190        }
191
192        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'json');
193        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_SERIALIZATION);
194
195        $type = $this->findInitialType($type, $context);
196        $result = $this->visit($navigator, $visitor, $context, $data, 'json', $type);
197        $result = $this->convertArrayObjects($result);
198
199        if (!\is_array($result)) {
200            throw new RuntimeException(sprintf(
201                'The input data of type "%s" did not convert to an array, but got a result of type "%s".',
202                \is_object($data) ? \get_class($data) : \gettype($data),
203                \is_object($result) ? \get_class($result) : \gettype($result)
204            ));
205        }
206
207        return $result;
208    }
209
210    /**
211     * {@InheritDoc}
212     */
213    public function fromArray(array $data, string $type, ?DeserializationContext $context = null)
214    {
215        if (null === $context) {
216            $context = $this->deserializationContextFactory->createDeserializationContext();
217        }
218
219        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, 'json');
220        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_DESERIALIZATION);
221
222        return $this->visit($navigator, $visitor, $context, $data, 'json', $type, false);
223    }
224
225    /**
226     * @param mixed $data
227     *
228     * @return mixed
229     */
230    private function visit(GraphNavigatorInterface $navigator, VisitorInterface $visitor, Context $context, $data, string $format, ?string $type = null, bool $prepare = true)
231    {
232        $context->initialize(
233            $format,
234            $visitor,
235            $navigator,
236            $this->factory
237        );
238
239        $visitor->setNavigator($navigator);
240        $navigator->initialize($visitor, $context);
241
242        if ($prepare) {
243            $data = $visitor->prepare($data);
244        }
245
246        if (null !== $type) {
247            $type = $this->typeParser->parse($type);
248        }
249        return $navigator->accept($data, $type);
250    }
251
252    /**
253     * @param mixed $data
254     *
255     * @return mixed
256     */
257    private function convertArrayObjects($data)
258    {
259        if ($data instanceof \ArrayObject || $data instanceof \stdClass) {
260            $data = (array) $data;
261        }
262        if (\is_array($data)) {
263            foreach ($data as $k => $v) {
264                $data[$k] = $this->convertArrayObjects($v);
265            }
266        }
267
268        return $data;
269    }
270}
271