Completed
Push — feature-EZP-25696 ( 52d929...5f47d3 )
by André
23:51
created

Location::loadLocationByRemoteId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the Role controller class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 *
9
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\REST\Server\Controller;
12
13
use eZ\Publish\Core\REST\Common\Message;
14
use eZ\Publish\Core\REST\Common\Exceptions;
15
use eZ\Publish\Core\REST\Server\Values;
16
use eZ\Publish\Core\REST\Server\Controller as RestController;
17
use eZ\Publish\API\Repository\LocationService;
18
use eZ\Publish\API\Repository\ContentService;
19
use eZ\Publish\API\Repository\TrashService;
20
use eZ\Publish\API\Repository\Exceptions\InvalidArgumentException;
21
use eZ\Publish\Core\REST\Server\Exceptions\BadRequestException;
22
use eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException;
23
use Symfony\Component\HttpFoundation\Request;
24
25
/**
26
 * Location controller.
27
 */
28
class Location extends RestController
29
{
30
    /**
31
     * Location service.
32
     *
33
     * @var \eZ\Publish\API\Repository\LocationService
34
     */
35
    protected $locationService;
36
37
    /**
38
     * Content service.
39
     *
40
     * @var \eZ\Publish\API\Repository\ContentService
41
     */
42
    protected $contentService;
43
44
    /**
45
     * Trash service.
46
     *
47
     * @var \eZ\Publish\API\Repository\TrashService
48
     */
49
    protected $trashService;
50
51
    /**
52
     * Construct controller.
53
     *
54
     * @param \eZ\Publish\API\Repository\LocationService $locationService
55
     * @param \eZ\Publish\API\Repository\ContentService $contentService
56
     * @param \eZ\Publish\API\Repository\TrashService $trashService
57
     */
58
    public function __construct(LocationService $locationService, ContentService $contentService, TrashService $trashService)
59
    {
60
        $this->locationService = $locationService;
61
        $this->contentService = $contentService;
62
        $this->trashService = $trashService;
63
    }
64
65
    /**
66
     * Loads the location for a given ID (x)or remote ID.
67
     *
68
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException
69
     *
70
     * @return \eZ\Publish\Core\REST\Server\Values\TemporaryRedirect
71
     */
72
    public function redirectLocation(Request $request)
73
    {
74
        if (!$request->query->has('id') && !$request->query->has('remoteId')) {
75
            throw new BadRequestException("At least one of 'id' or 'remoteId' parameters is required.");
76
        }
77
78
        if ($request->query->has('id')) {
79
            $location = $this->locationService->loadLocation($request->query->get('id'));
80
        } else {
81
            $location = $this->locationService->loadLocationByRemoteId($request->query->get('remoteId'));
82
        }
83
84
        return new Values\TemporaryRedirect(
85
            $this->router->generate(
86
                'ezpublish_rest_loadLocation',
87
                array(
88
                    'locationPath' => trim($location->pathString, '/'),
89
                )
90
            )
91
        );
92
    }
93
94
    /**
95
     * Creates a new location for object with id $contentId.
96
     *
97
     * @param mixed $contentId
98
     *
99
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\ForbiddenException
100
     *
101
     * @return \eZ\Publish\Core\REST\Server\Values\CreatedLocation
102
     */
103
    public function createLocation($contentId, Request $request)
104
    {
105
        $locationCreateStruct = $this->inputDispatcher->parse(
106
            new Message(
107
                array('Content-Type' => $request->headers->get('Content-Type')),
108
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
109
            )
110
        );
111
112
        $contentInfo = $this->contentService->loadContentInfo($contentId);
113
114
        try {
115
            $createdLocation = $this->locationService->createLocation($contentInfo, $locationCreateStruct);
0 ignored issues
show
Compatibility introduced by
$locationCreateStruct of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not a sub-type of object<eZ\Publish\API\Re...t\LocationCreateStruct>. It seems like you assume a child class of the class eZ\Publish\API\Repository\Values\ValueObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
116
        } catch (InvalidArgumentException $e) {
117
            throw new ForbiddenException($e->getMessage());
118
        }
119
120
        return new Values\CreatedLocation(array('restLocation' => new Values\RestLocation($createdLocation, 0)));
121
    }
122
123
    /**
124
     * Loads a location.
125
     *
126
     * @param string $locationPath
127
     *
128
     * @return \eZ\Publish\Core\REST\Server\Values\RestLocation
129
     */
130
    public function loadLocation($locationPath)
131
    {
132
        $location = $this->locationService->loadLocation(
133
            $this->extractLocationIdFromPath($locationPath)
134
        );
135
136
        if (trim($location->pathString, '/') != $locationPath) {
137
            throw new Exceptions\NotFoundException(
138
                "Could not find location with path string $locationPath"
139
            );
140
        }
141
142
        return new Values\CachedValue(
143
            new Values\RestLocation(
144
                $location,
145
                $this->locationService->getLocationChildCount($location)
146
            ),
147
            array('locationId' => $location->id)
148
        );
149
    }
150
151
    /**
152
     * Deletes a location.
153
     *
154
     * @param string $locationPath
155
     *
156
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
157
     */
158
    public function deleteSubtree($locationPath)
159
    {
160
        $location = $this->locationService->loadLocation(
161
            $this->extractLocationIdFromPath($locationPath)
162
        );
163
        $this->locationService->deleteLocation($location);
164
165
        return new Values\NoContent();
166
    }
167
168
    /**
169
     * Copies a subtree to a new destination.
170
     *
171
     * @param string $locationPath
172
     *
173
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated
174
     */
175
    public function copySubtree($locationPath, Request $request)
176
    {
177
        $location = $this->locationService->loadLocation(
178
            $this->extractLocationIdFromPath($locationPath)
179
        );
180
181
        $destinationLocation = $this->locationService->loadLocation(
182
            $this->extractLocationIdFromPath(
183
                $this->requestParser->parseHref(
184
                    $request->headers->get('Destination'),
185
                    'locationPath'
186
                )
187
            )
188
        );
189
190
        $newLocation = $this->locationService->copySubtree($location, $destinationLocation);
191
192
        return new Values\ResourceCreated(
193
            $this->router->generate(
194
                'ezpublish_rest_loadLocation',
195
                array(
196
                    'locationPath' => trim($newLocation->pathString, '/'),
197
                )
198
            )
199
        );
200
    }
201
202
    /**
203
     * Moves a subtree to a new location.
204
     *
205
     * @param string $locationPath
206
     *
207
     * @throws \eZ\Publish\Core\REST\Server\Exceptions\BadRequestException if the Destination header cannot be parsed as location or trash
208
     *
209
     * @return \eZ\Publish\Core\REST\Server\Values\ResourceCreated | \eZ\Publish\Core\REST\Server\Values\NoContent
210
     */
211
    public function moveSubtree($locationPath, Request $request)
212
    {
213
        $locationToMove = $this->locationService->loadLocation(
214
            $this->extractLocationIdFromPath($locationPath)
215
        );
216
217
        $destinationLocationId = null;
218
        $destinationHref = $request->headers->get('Destination');
219
        try {
220
            // First check to see if the destination is for moving within another subtree
221
            $destinationLocationId = $this->extractLocationIdFromPath(
222
                $this->requestParser->parseHref($destinationHref, 'locationPath')
0 ignored issues
show
Bug introduced by
It seems like $destinationHref defined by $request->headers->get('Destination') on line 218 can also be of type array; however, eZ\Publish\Core\REST\Com...uestParser::parseHref() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
223
            );
224
225
            // We're moving the subtree
226
            $destinationLocation = $this->locationService->loadLocation($destinationLocationId);
227
            $this->locationService->moveSubtree($locationToMove, $destinationLocation);
228
229
            // Reload the location to get the new position is subtree
230
            $locationToMove = $this->locationService->loadLocation($locationToMove->id);
231
232
            return new Values\ResourceCreated(
233
                $this->router->generate(
234
                    'ezpublish_rest_loadLocation',
235
                    array(
236
                        'locationPath' => trim($locationToMove->pathString, '/'),
237
                    )
238
                )
239
            );
240
        } catch (Exceptions\InvalidArgumentException $e) {
241
            // If parsing of destination fails, let's try to see if destination is trash
242
            try {
243
                $route = $this->requestParser->parse($destinationHref);
0 ignored issues
show
Bug introduced by
It seems like $destinationHref defined by $request->headers->get('Destination') on line 218 can also be of type array; however, eZ\Publish\Core\REST\Common\RequestParser::parse() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
244
                if (!isset($route['_route']) || $route['_route'] !== 'ezpublish_rest_loadTrashItems') {
245
                    throw new Exceptions\InvalidArgumentException('');
246
                }
247
                // Trash the subtree
248
                $trashItem = $this->trashService->trash($locationToMove);
249
250
                if (isset($trashItem)) {
251
                    return new Values\ResourceCreated(
252
                        $this->router->generate(
253
                            'ezpublish_rest_loadTrashItem',
254
                            array('trashItemId' => $trashItem->id)
0 ignored issues
show
Documentation introduced by
The property $id is declared protected in eZ\Publish\API\Repository\Values\Content\Location. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
255
                        )
256
                    );
257
                } else {
258
                    // Only a location has been trashed and not the object
259
                    return new Values\NoContent();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \eZ\Publish\C...ver\Values\NoContent(); (eZ\Publish\Core\REST\Server\Values\NoContent) is incompatible with the return type documented by eZ\Publish\Core\REST\Ser...r\Location::moveSubtree of type eZ\Publish\Core\REST\Server\Values\ResourceCreated.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
260
                }
261
            } catch (Exceptions\InvalidArgumentException $e) {
262
                // If that fails, the Destination header is not formatted right
263
                // so just throw the BadRequestException
264
                throw new BadRequestException("{$destinationHref} is not an acceptable destination");
265
            }
266
        }
267
    }
268
269
    /**
270
     * Swaps a location with another one.
271
     *
272
     * @param string $locationPath
273
     *
274
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
275
     */
276
    public function swapLocation($locationPath, Request $request)
277
    {
278
        $locationId = $this->extractLocationIdFromPath($locationPath);
279
        $location = $this->locationService->loadLocation($locationId);
280
281
        $destinationLocation = $this->locationService->loadLocation(
282
            $this->extractLocationIdFromPath(
283
                $this->requestParser->parseHref(
284
                    $request->headers->get('Destination'),
285
                    'locationPath'
286
                )
287
            )
288
        );
289
290
        $this->locationService->swapLocation($location, $destinationLocation);
291
292
        return new Values\NoContent();
293
    }
294
295
    /**
296
     * Loads a location by remote ID.
297
     *
298
     * @todo remove, or use in loadLocation with filter
299
     *
300
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
301
     */
302
    public function loadLocationByRemoteId(Request $request)
303
    {
304
        return new Values\LocationList(
305
            array(
306
                new Values\RestLocation(
307
                    $location = $this->locationService->loadLocationByRemoteId(
308
                        $request->query->get('remoteId')
309
                    ),
310
                    $this->locationService->getLocationChildCount($location)
311
                ),
312
            ),
313
            $request->getPathInfo()
314
        );
315
    }
316
317
    /**
318
     * Loads all locations for content object.
319
     *
320
     * @param mixed $contentId
321
     *
322
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
323
     */
324
    public function loadLocationsForContent($contentId, Request $request)
325
    {
326
        $restLocations = array();
327
        $contentInfo = $this->contentService->loadContentInfo($contentId);
328
        foreach ($this->locationService->loadLocations($contentInfo) as $location) {
329
            $restLocations[] = new Values\RestLocation(
330
                $location,
331
                // @todo Remove, and make optional in VO. Not needed for a location list.
332
                $this->locationService->getLocationChildCount($location)
333
            );
334
        }
335
336
        return new Values\CachedValue(
337
            new Values\LocationList($restLocations, $request->getPathInfo()),
338
            array('locationId' => $contentInfo->mainLocationId)
339
        );
340
    }
341
342
    /**
343
     * Loads child locations of a location.
344
     *
345
     * @param string $locationPath
346
     *
347
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
348
     */
349
    public function loadLocationChildren($locationPath, Request $request)
350
    {
351
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
352
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : 10;
353
354
        $restLocations = array();
355
        $locationId = $this->extractLocationIdFromPath($locationPath);
356
        $children = $this->locationService->loadLocationChildren(
357
            $this->locationService->loadLocation($locationId),
358
            $offset >= 0 ? $offset : 0,
359
            $limit >= 0 ? $limit : 25
360
        )->locations;
361
        foreach ($children as $location) {
362
            $restLocations[] = new Values\RestLocation(
363
                $location,
364
                $this->locationService->getLocationChildCount($location)
365
            );
366
        }
367
368
        return new Values\CachedValue(
369
            new Values\LocationList($restLocations, $request->getPathInfo()),
370
            array('locationId' => $locationId)
371
        );
372
    }
373
374
    /**
375
     * Extracts and returns an item id from a path, e.g. /1/2/58 => 58.
376
     *
377
     * @param string $path
378
     *
379
     * @return mixed
380
     */
381
    private function extractLocationIdFromPath($path)
382
    {
383
        $pathParts = explode('/', $path);
384
385
        return array_pop($pathParts);
386
    }
387
388
    /**
389
     * Updates a location.
390
     *
391
     * @param string $locationPath
392
     *
393
     * @return \eZ\Publish\Core\REST\Server\Values\RestLocation
394
     */
395
    public function updateLocation($locationPath, Request $request)
396
    {
397
        $locationUpdate = $this->inputDispatcher->parse(
398
            new Message(
399
                array('Content-Type' => $request->headers->get('Content-Type')),
400
                $request->getContent()
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, eZ\Publish\Core\REST\Common\Message::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
401
            )
402
        );
403
404
        $location = $this->locationService->loadLocation($this->extractLocationIdFromPath($locationPath));
405
406
        // First handle hiding/unhiding so that updating location afterwards
407
        // will return updated location with hidden/visible status correctly updated
408
        // Exact check for true/false is needed as null signals that no hiding/unhiding
409
        // is to be performed
410
        if ($locationUpdate->hidden === true) {
0 ignored issues
show
Documentation introduced by
The property hidden does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
411
            $this->locationService->hideLocation($location);
412
        } elseif ($locationUpdate->hidden === false) {
0 ignored issues
show
Documentation introduced by
The property hidden does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
413
            $this->locationService->unhideLocation($location);
414
        }
415
416
        return new Values\RestLocation(
417
            $location = $this->locationService->updateLocation($location, $locationUpdate->locationUpdateStruct),
0 ignored issues
show
Documentation introduced by
The property locationUpdateStruct does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
418
            $this->locationService->getLocationChildCount($location)
419
        );
420
    }
421
}
422