1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all copies or substantial portions of the software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook\GraphNodes;
25
26/**
27 * Class GraphNode
28 *
29 * @package Facebook
30 */
31class GraphNode extends Collection
32{
33    /**
34     * @var array Maps object key names to Graph object types.
35     */
36    protected static $graphObjectMap = [];
37
38    /**
39     * Init this Graph object.
40     *
41     * @param array $data
42     */
43    public function __construct(array $data = [])
44    {
45        parent::__construct($this->castItems($data));
46    }
47
48    /**
49     * Iterates over an array and detects the types each node
50     * should be cast to and returns all the items as an array.
51     *
52     * @TODO Add auto-casting to AccessToken entities.
53     *
54     * @param array $data The array to iterate over.
55     *
56     * @return array
57     */
58    public function castItems(array $data)
59    {
60        $items = [];
61
62        foreach ($data as $k => $v) {
63            if ($this->shouldCastAsDateTime($k)
64                && (is_numeric($v)
65                    || $this->isIso8601DateString($v))
66            ) {
67                $items[$k] = $this->castToDateTime($v);
68            } elseif ($k === 'birthday') {
69                $items[$k] = $this->castToBirthday($v);
70            } else {
71                $items[$k] = $v;
72            }
73        }
74
75        return $items;
76    }
77
78    /**
79     * Uncasts any auto-casted datatypes.
80     * Basically the reverse of castItems().
81     *
82     * @return array
83     */
84    public function uncastItems()
85    {
86        $items = $this->asArray();
87
88        return array_map(function ($v) {
89            if ($v instanceof \DateTime) {
90                return $v->format(\DateTime::ISO8601);
91            }
92
93            return $v;
94        }, $items);
95    }
96
97    /**
98     * Get the collection of items as JSON.
99     *
100     * @param int $options
101     *
102     * @return string
103     */
104    public function asJson($options = 0)
105    {
106        return json_encode($this->uncastItems(), $options);
107    }
108
109    /**
110     * Detects an ISO 8601 formatted string.
111     *
112     * @param string $string
113     *
114     * @return boolean
115     *
116     * @see https://developers.facebook.com/docs/graph-api/using-graph-api/#readmodifiers
117     * @see http://www.cl.cam.ac.uk/~mgk25/iso-time.html
118     * @see http://en.wikipedia.org/wiki/ISO_8601
119     */
120    public function isIso8601DateString($string)
121    {
122        // This insane regex was yoinked from here:
123        // http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
124        // ...and I'm all like:
125        // http://thecodinglove.com/post/95378251969/when-code-works-and-i-dont-know-why
126        $crazyInsaneRegexThatSomehowDetectsIso8601 = '/^([\+-]?\d{4}(?!\d{2}\b))'
127            . '((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?'
128            . '|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d'
129            . '|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])'
130            . '((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d'
131            . '([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/';
132
133        return preg_match($crazyInsaneRegexThatSomehowDetectsIso8601, $string) === 1;
134    }
135
136    /**
137     * Determines if a value from Graph should be cast to DateTime.
138     *
139     * @param string $key
140     *
141     * @return boolean
142     */
143    public function shouldCastAsDateTime($key)
144    {
145        return in_array($key, [
146            'created_time',
147            'updated_time',
148            'start_time',
149            'end_time',
150            'backdated_time',
151            'issued_at',
152            'expires_at',
153            'publish_time'
154        ], true);
155    }
156
157    /**
158     * Casts a date value from Graph to DateTime.
159     *
160     * @param int|string $value
161     *
162     * @return \DateTime
163     */
164    public function castToDateTime($value)
165    {
166        if (is_int($value)) {
167            $dt = new \DateTime();
168            $dt->setTimestamp($value);
169        } else {
170            $dt = new \DateTime($value);
171        }
172
173        return $dt;
174    }
175
176    /**
177     * Casts a birthday value from Graph to Birthday
178     *
179     * @param string $value
180     *
181     * @return Birthday
182     */
183    public function castToBirthday($value)
184    {
185        return new Birthday($value);
186    }
187
188    /**
189     * Getter for $graphObjectMap.
190     *
191     * @return array
192     */
193    public static function getObjectMap()
194    {
195        return static::$graphObjectMap;
196    }
197}
198