Passed
Pull Request — master (#1)
by Tom
02:08
created

HasLinksTrait::buildLinks()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: tom
5
 * Date: 28/03/19
6
 * Time: 12:47
7
 */
8
9
namespace TomHart\Restful\Traits;
10
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Routing\Router;
13
use Illuminate\Support\Str;
14
use ReflectionException;
15
use ReflectionMethod;
16
use TomHart\Restful\Concerns\HasLinks;
17
18
trait HasLinksTrait
19
{
20
21
    /**
22
     * Add the links attribute to the model.
23
     */
24
    public function initializeHasLinksTrait(): void
25
    {
26
        $this->append('_links');
0 ignored issues
show
Bug introduced by
It seems like append() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
27
    }
28
29
    /**
30
     * Get the links for this model.
31
     * @return mixed[]
32
     * @throws ReflectionException
33
     */
34
    public function getLinksAttribute(): array
35
    {
36
        $links = $this->buildLinks();
37
        $relationships = $this->buildRelationshipLinks();
38
        if (!empty($relationships)) {
39
            $links['relationships'] = $relationships;
40
        }
41
42
        return $links;
43
    }
44
45
    /**
46
     * Returns the _links for the REST responses.
47
     *
48
     * @return mixed[]
49
     */
50
    public function buildLinks(): array
51
    {
52
        $routes = ['create', 'store', 'show', 'update', 'destroy'];
53
        $links = [];
54
55
        $router = app(Router::class);
56
57
        foreach ($routes as $routePart) {
58
            $link = $this->buildLink($this, $routePart, $router);
59
60
            if ($link) {
61
                $links[$routePart] = $link;
62
            }
63
        }
64
65
        return $links;
66
    }
67
68
69
    /**
70
     * Builds the links to create the relationship resources.
71
     *
72
     * @return mixed[]
73
     * @throws ReflectionException
74
     */
75
    public function buildRelationshipLinks(): array
76
    {
77
        $methods = get_class_methods($this);
78
79
        $links = [];
80
        $router = app(Router::class);
81
82
        foreach ($methods as $method) {
83
            $method2 = new ReflectionMethod($this, $method);
84
            $return = (string)$method2->getReturnType();
85
86
            if (empty($return)) {
87
                continue;
88
            }
89
90
            $isRelationship = is_subclass_of($return, Relation::class);
1 ignored issue
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Illuminate\Database\Elo...lations\Relation::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
91
92
            if (!$isRelationship) {
93
                continue;
94
            }
95
96
            /** @var Relation $relationship */
97
            $relationship = $this->$method();
98
99
            $targetClass = $relationship->getRelated();
100
101
            if (!($targetClass instanceof HasLinks)) {
102
                continue;
103
            }
104
105
            $createLink = $this->buildLink($targetClass, 'create', $router);
106
            $storeLink = $this->buildLink($targetClass, 'store', $router);
107
108
            if ($createLink || $storeLink) {
109
                $links[$method] = [
110
                    'create' => $createLink,
111
                    'store' => $storeLink
112
                ];
113
            }
114
        }
115
116
        return $links;
117
    }
118
119
120
    /**
121
     * Builds a link if possible
122
     *
123
     * @param HasLinks $model
124
     * @param string $routePart
125
     * @param Router $router
126
     * @return mixed[]|bool
127
     */
128
    private function buildLink(HasLinks $model, string $routePart, Router $router)
129
    {
130
        $routeStub = $model->getRouteName();
131
132
        if (!$routeStub) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $routeStub of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
133
            return false;
134
        }
135
136
        // Make the route name, and check if it exists.
137
        $routeName = "$routeStub.$routePart";
138
139
        if (!$router->has($routeName)) {
140
            return false;
141
        }
142
143
        // Get any params needed to build the URL.
144
        $params = [];
145
        switch ($routePart) {
146
            case 'destroy':
147
            case 'update':
148
            case 'show':
149
                $params = [$this->getRouteKey() => $this->id];
0 ignored issues
show
Bug introduced by
The property id does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Bug introduced by
It seems like getRouteKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
150
                break;
151
        }
152
153
        // Get the route.
154
        $route = $router->getRoutes()->getByName($routeName);
155
156
        if (!$route) {
157
            return false;
158
        }
159
160
        // Get the methods applicable to the route, ignoring HEAD and PATCH.
161
        $methods = collect($route->methods());
162
        $methods = $methods->filter(static function ($item) {
163
            return !in_array($item, ['HEAD', 'PATCH']);
164
        })->map(static function ($str) {
165
            return strtolower($str);
166
        });
167
168
        // If there's only 1, return just that, otherwise, return an array.
169
        if ($methods->count() === 1) {
170
            $methods = $methods->first();
171
        }
172
173
        // Add!
174
        return [
175
            'method' => $methods,
176
            'href' => route($routeName, $params, false)
177
        ];
178
    }
179
180
    /**
181
     * Return the name for the resource route this model
182
     * @return string|null
183
     */
184
    public function getRouteName(): ?string
185
    {
186
        $name = $this->getRouteKey();
0 ignored issues
show
Bug introduced by
It seems like getRouteKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
187
        if (!$name) {
188
            return null;
189
        }
190
        return Str::kebab(Str::studly($name));
191
    }
192
}
193