RelationFactory::parseRelationUrl()   B
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
nc 7
nop 3
dl 0
loc 28
rs 8.439
c 1
b 0
f 0
ccs 13
cts 13
cp 1
crap 5
1
<?php
2
3
/**
4
 * apparat-object
5
 *
6
 * @category    Apparat
7
 * @package     Apparat\Object
8
 * @subpackage  Apparat\Object\<Layer>
9
 * @author      Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright   Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license     http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Apparat\Object\Domain\Factory;
38
39
use Apparat\Kernel\Ports\Kernel;
40
use Apparat\Object\Domain\Model\Relation\ContributedByRelation;
41
use Apparat\Object\Domain\Model\Relation\ContributesRelation;
42
use Apparat\Object\Domain\Model\Relation\EmbeddedByRelation;
43
use Apparat\Object\Domain\Model\Relation\EmbedsRelation;
44
use Apparat\Object\Domain\Model\Relation\InvalidArgumentException;
45
use Apparat\Object\Domain\Model\Relation\LikedByRelation;
46
use Apparat\Object\Domain\Model\Relation\LikesRelation;
47
use Apparat\Object\Domain\Model\Relation\OutOfBoundsException;
48
use Apparat\Object\Domain\Model\Relation\Proxy\ContributedByProxyRelation;
49
use Apparat\Object\Domain\Model\Relation\Proxy\ContributesProxyRelation;
50
use Apparat\Object\Domain\Model\Relation\Proxy\EmbeddedByProxyRelation;
51
use Apparat\Object\Domain\Model\Relation\Proxy\EmbedsProxyRelation;
52
use Apparat\Object\Domain\Model\Relation\Proxy\LikedByProxyRelation;
53
use Apparat\Object\Domain\Model\Relation\Proxy\LikesProxyRelation;
54
use Apparat\Object\Domain\Model\Relation\Proxy\ReferredByProxyRelation;
55
use Apparat\Object\Domain\Model\Relation\Proxy\RefersToProxyRelation;
56
use Apparat\Object\Domain\Model\Relation\Proxy\RepliedByProxyRelation;
57
use Apparat\Object\Domain\Model\Relation\Proxy\RepliesToProxyRelation;
58
use Apparat\Object\Domain\Model\Relation\Proxy\RepostedByProxyRelation;
59
use Apparat\Object\Domain\Model\Relation\Proxy\RepostsProxyRelation;
60
use Apparat\Object\Domain\Model\Relation\Proxy\SyndicatedFromProxyRelation;
61
use Apparat\Object\Domain\Model\Relation\Proxy\SyndicatedToProxyRelation;
62
use Apparat\Object\Domain\Model\Relation\ReferredByRelation;
63
use Apparat\Object\Domain\Model\Relation\RefersToRelation;
64
use Apparat\Object\Domain\Model\Relation\RelationInterface;
65
use Apparat\Object\Domain\Model\Relation\RepliedByRelation;
66
use Apparat\Object\Domain\Model\Relation\RepliesToRelation;
67
use Apparat\Object\Domain\Model\Relation\RepostedByRelation;
68
use Apparat\Object\Domain\Model\Relation\RepostsRelation;
69
use Apparat\Object\Domain\Model\Relation\SyndicatedFromRelation;
70
use Apparat\Object\Domain\Model\Relation\SyndicatedToRelation;
71
use Apparat\Object\Domain\Model\Uri\ApparatUrl;
72
use Apparat\Object\Domain\Model\Uri\Url;
73
use Apparat\Object\Domain\Repository\RepositoryInterface;
74
use Apparat\Object\Infrastructure\Utilities\Validator;
75
76
/**
77
 * Relation factory
78
 *
79
 * @package Apparat\Object
80
 * @subpackage Apparat\Object\Domain
81
 */
82
class RelationFactory
83
{
84
    /**
85
     * URL component key
86
     *
87
     * @string
88
     */
89
    const PARSE_URL = 'url';
90
    /**
91
     * Label component key
92
     *
93
     * @string
94
     */
95
    const PARSE_LABEL = 'label';
96
    /**
97
     * Email component key
98
     *
99
     * @string
100
     */
101
    const PARSE_EMAIL = 'email';
102
    /**
103
     * Component relation coupling
104
     *
105
     * @string
106
     */
107
    const PARSE_COUPLING = 'coupling';
108
    /**
109
     * Relation types
110
     *
111
     * @var array
112
     */
113
    public static $relationTypes = [
114
        ContributesRelation::TYPE => ContributesRelation::class,
115
        ContributedByRelation::TYPE => ContributedByRelation::class,
116
        EmbedsRelation::TYPE => EmbedsRelation::class,
117
        EmbeddedByRelation::TYPE => EmbeddedByRelation::class,
118
        LikesRelation::TYPE => LikesRelation::class,
119
        LikedByRelation::TYPE => LikedByRelation::class,
120
        RefersToRelation::TYPE => RefersToRelation::class,
121
        ReferredByRelation::TYPE => ReferredByRelation::class,
122
        RepliesToRelation::TYPE => RepliesToRelation::class,
123
        RepliedByRelation::TYPE => RepliedByRelation::class,
124
        RepostsRelation::TYPE => RepostsRelation::class,
125
        RepostedByRelation::TYPE => RepostedByRelation::class,
126
        SyndicatedToRelation::TYPE => SyndicatedToRelation::class,
127
        SyndicatedFromRelation::TYPE => SyndicatedFromRelation::class,
128
    ];
129
    /**
130
     * Proxy relation types
131
     *
132
     * @var array
133
     */
134
    public static $proxyRelationTypes = [
135
        ContributesRelation::TYPE => ContributesProxyRelation::class,
136
        ContributedByRelation::TYPE => ContributedByProxyRelation::class,
137
        EmbedsRelation::TYPE => EmbedsProxyRelation::class,
138
        EmbeddedByRelation::TYPE => EmbeddedByProxyRelation::class,
139
        LikesRelation::TYPE => LikesProxyRelation::class,
140
        LikedByRelation::TYPE => LikedByProxyRelation::class,
141
        RefersToRelation::TYPE => RefersToProxyRelation::class,
142
        ReferredByRelation::TYPE => ReferredByProxyRelation::class,
143
        RepliesToRelation::TYPE => RepliesToProxyRelation::class,
144
        RepliedByRelation::TYPE => RepliedByProxyRelation::class,
145
        RepostsRelation::TYPE => RepostsProxyRelation::class,
146
        RepostedByRelation::TYPE => RepostedByProxyRelation::class,
147
        SyndicatedToRelation::TYPE => SyndicatedToProxyRelation::class,
148
        SyndicatedFromRelation::TYPE => SyndicatedFromProxyRelation::class,
149
    ];
150
151
    /**
152
     * Parse a relation serialization and instantiate the relation
153
     *
154
     * @param string $relationType Relation type
155
     * @param string $relation Relation serialization
156
     * @param RepositoryInterface $contextRepository Context repository
157
     * @return RelationInterface Relation object
158
     */
159 41
    public static function createFromString($relationType, $relation, RepositoryInterface $contextRepository)
160
    {
161
        // Validate the relation type
162 41
        self::validateRelationType($relationType);
163
164
        // Parse the relation string
165 41
        $relationComponents = self::parseRelationString($relation, $contextRepository);
166 38
        $isProxyRelation = $relationComponents[self::PARSE_URL] instanceof ApparatUrl;
167
168
        // Create the relation instance
169 38
        return Kernel::create(
170 38
            $isProxyRelation ? self::$proxyRelationTypes[$relationType] : self::$relationTypes[$relationType],
171
            array_values($relationComponents)
172
        );
173
    }
174
175
    /**
176
     * Validate a relation type
177
     *
178
     * @param string $relationType Relation type
179
     * @throws InvalidArgumentException If the relation type is invalid
180
     */
181 45
    public static function validateRelationType($relationType)
182
    {
183
        // If the relation type is invalid
184 45
        if (empty($relationType) || empty(self::$relationTypes[$relationType])) {
185 1
            throw new OutOfBoundsException(
186 1
                sprintf('Invalid object relation type "%s"', $relationType),
187 1
                OutOfBoundsException::INVALID_OBJECT_RELATION_TYPE
188
            );
189
        }
190 45
    }
191
192
    /**
193
     * Parse a relation serialization and instantiate the relation object
194
     *
195
     * @param string $relation Relation serialization
196
     * @param RepositoryInterface $contextRepository Context repository
197
     * @return array Parsed relation components
198
     * @throws InvalidArgumentException If the email component has already been registered
199
     * @throws InvalidArgumentException If the URL component has already been registered
200
     */
201 41
    protected static function parseRelationString($relation, RepositoryInterface $contextRepository)
202
    {
203
        $parsed = [
204 41
            self::PARSE_URL => null,
205 41
            self::PARSE_LABEL => null,
206 41
            self::PARSE_EMAIL => null,
207 41
            self::PARSE_COUPLING => RelationInterface::LOOSE_COUPLING,
208
        ];
209
210
        // Split the relation string and parse the components
211 41
        foreach (preg_split('%\s+%', $relation) as $relationComponent) {
212
            // If it's an email component
213 41
            if (!strncmp('<', $relationComponent, 1)) {
214
                // If the email component has already been registered
215 34
                if (!empty($parsed[self::PARSE_EMAIL])) {
216 1
                    throw new InvalidArgumentException(
217 1
                        sprintf('Repeated relation email component "%s" not allowed', self::PARSE_EMAIL),
218 1
                        InvalidArgumentException::REPEATED_RELATION_COMPONENT_NOT_ALLOWED
219
                    );
220
                }
221
222 34
                $parsed[self::PARSE_EMAIL] = self::parseRelationEmail($relationComponent);
223 33
                continue;
224
            }
225
226
            // Next: Try to parse it as URL
227
            try {
228 39
                $parsed[self::PARSE_COUPLING] = intval($parsed[self::PARSE_COUPLING]);
229 39
                $url = self::parseRelationUrl(
230
                    $relationComponent,
231 39
                    $parsed[self::PARSE_COUPLING],
232
                    $contextRepository
233
                );
234
235
                // If the URL component has already been registered
236 33
                if (!empty($parsed[self::PARSE_URL])) {
237 1
                    throw new InvalidArgumentException(
238 1
                        sprintf('Repeated relation url component "%s" not allowed', self::PARSE_URL),
239 1
                        InvalidArgumentException::REPEATED_RELATION_COMPONENT_NOT_ALLOWED
240
                    );
241
                }
242
243 33
                $parsed[self::PARSE_URL] = $url;
244
245
                // Else: Process as label component
246 39
            } catch (\Exception $e) {
247
                // If it's a repeated URL component
248 39
                if (($e instanceof InvalidArgumentException)
249 39
                    && ($e->getCode() == InvalidArgumentException::REPEATED_RELATION_COMPONENT_NOT_ALLOWED)
250
                ) {
251 1
                    throw $e;
252
                }
253
254 39
                $parsed[self::PARSE_LABEL] = trim($parsed[self::PARSE_LABEL].' '.$relationComponent);
255
            }
256
        }
257
258 38
        return $parsed;
259
    }
260
261
    /**
262
     * Parse and validate a relation email address component
263
     *
264
     * @param string $relationEmail Email address
265
     * @return string Valid email address
266
     * @throws InvalidArgumentException If the email address is invalid
267
     */
268 34
    protected static function parseRelationEmail($relationEmail)
269
    {
270
        // If it's a valid email address
271 34
        if (preg_match('%^\<(.+)\>$%', $relationEmail, $emailAddress) && Validator::isEmail($emailAddress[1])) {
272 33
            return $emailAddress[1];
273
        }
274
275 1
        throw new InvalidArgumentException(
276 1
            sprintf('Invalid relation email address "%s"', $relationEmail),
277 1
            InvalidArgumentException::INVALID_RELATION_EMAIL_ADDRESS
278
        );
279
    }
280
281
    /**
282
     * Parse and instantiate a relation URL
283
     *
284
     * @param string $url URL
285
     * @param int $coupling Strong coupling
286
     * @param RepositoryInterface $contextRepository Context repository
287
     * @return Url URL
288
     * @throws InvalidArgumentException If the relation URL is invalid
289
     */
290 39
    protected static function parseRelationUrl($url, &$coupling, RepositoryInterface $contextRepository)
291
    {
292 39
        if (strlen($url)) {
293
            // If the URL requires tight coupling
294 39
            if (!strncmp('!', $url, 1)) {
295 31
                $coupling = RelationInterface::TIGHT_COUPLING;
296 31
                $url = substr($url, 1);
297
            }
298
299
            // Try to instantiate as an apparat URL
300
            try {
301 39
                return Kernel::create(ApparatUrl::class, [$url, true, $contextRepository]);
302
303
                // If there's an apparat URL problem: Try to instantiate as a regular URL
304 39
            } catch (\Apparat\Object\Domain\Model\Uri\InvalidArgumentException $e) {
305
                /** @var Url $urlInstance */
306 39
                $urlInstance = Kernel::create(Url::class, [$url]);
307 39
                if ($urlInstance->isAbsolute()) {
308 33
                    return $urlInstance;
309
                }
310
            }
311
        }
312
313 38
        throw new InvalidArgumentException(
314 38
            sprintf('Invalid relation URL "%s"', $url),
315 38
            InvalidArgumentException::INVALID_RELATION_URL
316
        );
317
    }
318
}
319