Completed
Push — ezp-26116-trashing-multiple-lo... ( 238209 )
by
unknown
28:30
created

Location   D

Complexity

Total Complexity 31

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 395
rs 4.9
c 1
b 1
f 0
wmc 31
lcom 1
cbo 26

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A redirectLocation() 0 21 4
A createLocation() 0 19 2
A loadLocation() 0 20 2
A deleteSubtree() 0 9 1
B copySubtree() 0 26 1
B moveSubtree() 0 58 6
A swapLocation() 0 18 1
A loadLocationByRemoteId() 0 14 1
A loadLocationsForContent() 0 17 2
B loadLocationChildren() 0 24 6
A extractLocationIdFromPath() 0 6 1
B updateLocation() 0 26 3
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
262
            } catch (Exceptions\InvalidArgumentException $e) {
263
                // If that fails, the Destination header is not formatted right
264
                // so just throw the BadRequestException
265
                throw new BadRequestException("{$destinationHref} is not an acceptable destination");
266
            }
267
        }
268
    }
269
270
    /**
271
     * Swaps a location with another one.
272
     *
273
     * @param string $locationPath
274
     *
275
     * @return \eZ\Publish\Core\REST\Server\Values\NoContent
276
     */
277
    public function swapLocation($locationPath, Request $request)
278
    {
279
        $locationId = $this->extractLocationIdFromPath($locationPath);
280
        $location = $this->locationService->loadLocation($locationId);
281
282
        $destinationLocation = $this->locationService->loadLocation(
283
            $this->extractLocationIdFromPath(
284
                $this->requestParser->parseHref(
285
                    $request->headers->get('Destination'),
286
                    'locationPath'
287
                )
288
            )
289
        );
290
291
        $this->locationService->swapLocation($location, $destinationLocation);
292
293
        return new Values\NoContent();
294
    }
295
296
    /**
297
     * Loads a location by remote ID.
298
     *
299
     * @todo remove, or use in loadLocation with filter
300
     *
301
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
302
     */
303
    public function loadLocationByRemoteId(Request $request)
304
    {
305
        return new Values\LocationList(
306
            array(
307
                new Values\RestLocation(
308
                    $location = $this->locationService->loadLocationByRemoteId(
309
                        $request->query->get('remoteId')
310
                    ),
311
                    $this->locationService->getLocationChildCount($location)
312
                ),
313
            ),
314
            $request->getPathInfo()
315
        );
316
    }
317
318
    /**
319
     * Loads all locations for content object.
320
     *
321
     * @param mixed $contentId
322
     *
323
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
324
     */
325
    public function loadLocationsForContent($contentId, Request $request)
326
    {
327
        $restLocations = array();
328
        $contentInfo = $this->contentService->loadContentInfo($contentId);
329
        foreach ($this->locationService->loadLocations($contentInfo) as $location) {
330
            $restLocations[] = new Values\RestLocation(
331
                $location,
332
                // @todo Remove, and make optional in VO. Not needed for a location list.
333
                $this->locationService->getLocationChildCount($location)
334
            );
335
        }
336
337
        return new Values\CachedValue(
338
            new Values\LocationList($restLocations, $request->getPathInfo()),
339
            array('locationId' => $contentInfo->mainLocationId)
340
        );
341
    }
342
343
    /**
344
     * Loads child locations of a location.
345
     *
346
     * @param string $locationPath
347
     *
348
     * @return \eZ\Publish\Core\REST\Server\Values\LocationList
349
     */
350
    public function loadLocationChildren($locationPath, Request $request)
351
    {
352
        $offset = $request->query->has('offset') ? (int)$request->query->get('offset') : 0;
353
        $limit = $request->query->has('limit') ? (int)$request->query->get('limit') : 10;
354
355
        $restLocations = array();
356
        $locationId = $this->extractLocationIdFromPath($locationPath);
357
        $children = $this->locationService->loadLocationChildren(
358
            $this->locationService->loadLocation($locationId),
359
            $offset >= 0 ? $offset : 0,
360
            $limit >= 0 ? $limit : 25
361
        )->locations;
362
        foreach ($children as $location) {
363
            $restLocations[] = new Values\RestLocation(
364
                $location,
365
                $this->locationService->getLocationChildCount($location)
366
            );
367
        }
368
369
        return new Values\CachedValue(
370
            new Values\LocationList($restLocations, $request->getPathInfo()),
371
            array('locationId' => $locationId)
372
        );
373
    }
374
375
    /**
376
     * Extracts and returns an item id from a path, e.g. /1/2/58 => 58.
377
     *
378
     * @param string $path
379
     *
380
     * @return mixed
381
     */
382
    private function extractLocationIdFromPath($path)
383
    {
384
        $pathParts = explode('/', $path);
385
386
        return array_pop($pathParts);
387
    }
388
389
    /**
390
     * Updates a location.
391
     *
392
     * @param string $locationPath
393
     *
394
     * @return \eZ\Publish\Core\REST\Server\Values\RestLocation
395
     */
396
    public function updateLocation($locationPath, Request $request)
397
    {
398
        $locationUpdate = $this->inputDispatcher->parse(
399
            new Message(
400
                array('Content-Type' => $request->headers->get('Content-Type')),
401
                $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...
402
            )
403
        );
404
405
        $location = $this->locationService->loadLocation($this->extractLocationIdFromPath($locationPath));
406
407
        // First handle hiding/unhiding so that updating location afterwards
408
        // will return updated location with hidden/visible status correctly updated
409
        // Exact check for true/false is needed as null signals that no hiding/unhiding
410
        // is to be performed
411
        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...
412
            $this->locationService->hideLocation($location);
413
        } 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...
414
            $this->locationService->unhideLocation($location);
415
        }
416
417
        return new Values\RestLocation(
418
            $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...
419
            $this->locationService->getLocationChildCount($location)
420
        );
421
    }
422
}
423