Passed
Push — master ( 19e85c...7a6648 )
by Nicolaas
10:46
created

FindEditableObjects::classCanBeIncluded()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 1
dl 0
loc 14
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace Sunnysideup\SiteWideSearch\Helpers;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Extensible;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\UnsavedRelationList;
13
use Sunnysideup\CmsEditLinkField\Api\CMSEditLinkAPI;
14
15
class FindEditableObjects
16
{
17
    use Extensible;
18
    use Configurable;
19
    use Injectable;
20
21
    /**
22
     * @var string
23
     */
24
    private const CACHE_NAME = 'FindEditableObjectsCache';
25
26
    protected $relationTypesCovered = [];
27
28
    protected $excludedClasses = [];
29
    protected $includedClasses = [];
30
31
    /**
32
     * format is as follows:
33
     * ```php
34
     *      [
35
     *          'valid_methods_edit' => [
36
     *              ClassNameA => true, // tested and does not have any available methods
37
     *              ClassNameB => MethodName1, // tested found method MethodName1 that can be used.
38
     *              ClassNameC => MethodName2, // tested found method MethodName2 that can be used.
39
     *              ClassNameD => true, // tested and does not have any available methods
40
     *          ],
41
     *          'valid_methods_view' => [
42
     *              ClassNameA => true, // tested and does not have any available methods
43
     *              ClassNameB => MethodName1, // tested found method MethodName1 that can be used.
44
     *              ClassNameC => MethodName2, // tested found method MethodName2 that can be used.
45
     *              ClassNameD => true, // tested and does not have any available methods
46
     *          ],
47
     *          'valid_methods_view_links' => [
48
     *              [ClassNameX_IDY] => 'MyLinkView',
49
     *              [ClassNameX_IDZ] => 'MyLinkView',
50
     *          ],
51
     *          'valid_methods_edit_links' => [
52
     *              [ClassNameX_IDY] => 'MyLinkEdit',
53
     *              [ClassNameX_IDZ] => 'MyLinkEdit',
54
     *          ],
55
     *          'rels' =>
56
     *              'ClassNameY' => [
57
     *                  'MethodA' => RelationClassNameB,
58
     *                  'MethodC' => RelationClassNameD,
59
     *              ],
60
     *          ],
61
     *          'validMethods' => [
62
     *              'valid_methods_edit' => [
63
     *                  'A',
64
     *                  'B',
65
     *              ]
66
     *              'valid_methods_view' => [
67
     *                  'A',
68
     *                  'B',
69
     *              ]
70
     *          ]
71
     *     ]
72
     * ```
73
     * we use true rather than false to be able to use empty to work out if it has been tested before.
74
     *
75
     * @var array
76
     */
77
    protected $cache = [
78
        'valid_methods_edit',
79
        'valid_methods_view',
80
        'valid_methods_view_links',
81
        'valid_methods_edit_links',
82
        'rels',
83
        'validMethods' => [
84
            'valid_methods_edit' => [],
85
            'valid_methods_view' => [],
86
        ],
87
    ];
88
89
    private static $max_relation_depth = 3;
90
91
    private static $valid_methods_edit = [
92
        'CMSEditLink',
93
        'getCMSEditLink',
94
    ];
95
96
    private static $valid_methods_view = [
97
        'getLink',
98
        'Link',
99
    ];
100
101
    public function getFileCache()
102
    {
103
        return Injector::inst()->get(Cache::class);
104
    }
105
106
    public function initCache(): self
107
    {
108
        $this->cache = $this->getFileCache()->getCacheValues(self::CACHE_NAME);
109
110
        return $this;
111
    }
112
113
    public function setExcludedClasses(array $excludedClasses): self
114
    {
115
        $this->excludedClasses = $excludedClasses;
116
117
        return $this;
118
    }
119
120
    public function setIncludedClasses(array $includedClasses): self
121
    {
122
        $this->includedClasses = $includedClasses;
123
124
        return $this;
125
    }
126
127
    public function saveCache(): self
128
    {
129
        $this->getFileCache()->setCacheValues(self::CACHE_NAME, $this->cache);
130
131
        return $this;
132
    }
133
134
    /**
135
     * returns an link to an object that can be edited in the CMS.
136
     *
137
     * @param mixed $dataObject
138
     */
139
    public function getCMSEditLink($dataObject): string
140
    {
141
        return $this->getLinkInner($dataObject, 'valid_methods_edit');
142
    }
143
144
    /**
145
     * returns an link to an object that can be viewed.
146
     *
147
     * @param mixed $dataObject
148
     */
149
    public function getLink($dataObject): string
150
    {
151
        return $this->getLinkInner($dataObject, 'valid_methods_view');
152
    }
153
154
    /**
155
     * returns an link to an object that can be viewed.
156
     *
157
     * @param mixed $dataObject
158
     */
159
    protected function getLinkInner($dataObject, string $type): string
160
    {
161
        $typeKey = $type . '_links';
162
        $key = $dataObject->ClassName . $dataObject->ID;
163
        $result = $this->cache[$typeKey][$key] ?? false;
164
        if (false === $result) {
165
            $this->relationTypesCovered = [];
166
            $result = $this->checkForValidMethods($dataObject, $type);
167
            $this->cache[$typeKey][$key] = $result;
168
        }
169
170
        return $result;
171
    }
172
173
    protected function checkForValidMethods($dataObject, string $type, int $relationDepth = 0): string
174
    {
175
        //too many iterations!
176
        if ($relationDepth > $this->Config()->get('max_relation_depth')) {
177
            return '';
178
        }
179
180
        $validMethods = $this->getValidMethods($type);
181
182
        $this->relationTypesCovered[$dataObject->ClassName] = true;
183
184
        // quick return
185
        if (isset($this->cache[$type][$dataObject->ClassName]) && true !== $this->cache[$type][$dataObject->ClassName]) {
186
            $validMethod = $this->cache[$type][$dataObject->ClassName];
187
            if ($dataObject->hasMethod($validMethod)) {
188
                return (string) $dataObject->{$validMethod}();
189
            }
190
191
            return (string) $dataObject->{$validMethod};
192
        }
193
194
        if ($this->classCanBeIncluded($dataObject->ClassName)) {
195
            if (empty($this->cache[$type][$dataObject->ClassName]) || true !== $this->cache[$type][$dataObject->ClassName]) {
196
                foreach ($validMethods as $validMethod) {
197
                    $outcome = null;
198
                    if ($dataObject->hasMethod($validMethod)) {
199
                        $outcome = $dataObject->{$validMethod}();
200
                    } elseif (!empty($dataObject->{$validMethod})) {
201
                        $outcome = $dataObject->{$validMethod};
202
                    }
203
204
                    if ($outcome) {
205
                        $this->cache[$type][$dataObject->ClassName] = $validMethod;
206
207
                        return (string) $outcome;
208
                    }
209
                }
210
            }
211
212
            if ('valid_methods_edit' === $type) {
213
                if (class_exists(CMSEditLinkAPI::class)) {
214
                    $link = CMSEditLinkAPI::find_edit_link_for_object($dataObject);
215
                    if ($link) {
216
                        return (string) $link;
217
                    }
218
                }
219
            }
220
        }
221
222
        // there is no match for this one, but we can search relations ...
223
        $this->cache[$type][$dataObject->ClassName] = true;
224
        ++$relationDepth;
225
        foreach ($this->getRelations($dataObject) as $relationName => $relType) {
226
            $outcome = null;
227
            //no support for link through relations yet!
228
            if (is_array($relType)) {
229
                continue;
230
            }
231
232
            if (!isset($this->relationTypesCovered[$relType])) {
233
                $rels = $dataObject->{$relationName}();
234
                if ($rels) {
235
                    if ($rels instanceof DataList) {
236
                        $rels = $rels->first();
0 ignored issues
show
Unused Code introduced by
The assignment to $rels is dead and can be removed.
Loading history...
237
                    } elseif ($rels && $rels instanceof DataObject) {
238
                        $outcome = $this->checkForValidMethods($rels, $type, $relationDepth);
239
                    } elseif ($rels instanceof UnsavedRelationList) {
240
                        //do nothing;
241
                    } else {
242
                        print_r($rels);
243
                        user_error('Unexpected Relationship');
244
                        die('');
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
245
                    }
246
                }
247
            }
248
249
            if ($outcome) {
250
                return $outcome;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $outcome returns the type void which is incompatible with the type-hinted return string.
Loading history...
251
            }
252
        }
253
254
        return '';
255
    }
256
257
    protected function getRelations($dataObject): array
258
    {
259
        if (!isset($this->cache['rels'][$dataObject->ClassName])) {
260
            $this->cache['rels'][$dataObject->ClassName] = array_merge(
261
                Config::inst()->get($dataObject->ClassName, 'belongs_to'),
262
                Config::inst()->get($dataObject->ClassName, 'has_one'),
263
                Config::inst()->get($dataObject->ClassName, 'has_many'),
264
                Config::inst()->get($dataObject->ClassName, 'belongs_many_many'),
265
                Config::inst()->get($dataObject->ClassName, 'many_many')
266
            );
267
            foreach ($this->cache['rels'][$dataObject->ClassName] as $key => $value) {
268
                if (!(is_string($value) && class_exists($value) && $this->classCanBeIncluded($value))) {
269
                    unset($this->cache['rels'][$dataObject->ClassName][$key]);
270
                }
271
            }
272
        }
273
274
        return $this->cache['rels'][$dataObject->ClassName];
275
    }
276
277
    protected function getValidMethods(string $type): array
278
    {
279
        if (!isset($this->cache['validMethods'][$type])) {
280
            $this->cache['validMethods'][$type] = $this->Config()->get($type);
281
        }
282
283
        return $this->cache['validMethods'][$type];
284
    }
285
286
    protected function classCanBeIncluded(string $dataObjectClassName): bool
287
    {
288
        if(count($this->excludedClasses) || count($this->includedClasses)) {
289
            if(!class_exists($dataObjectClassName)) {
290
                return false;
291
            }
292
            if (count($this->includedClasses)) {
293
                return in_array($dataObjectClassName, $this->includedClasses, true);
294
            }
295
296
            return !in_array($dataObjectClassName, $this->excludedClasses, true);
297
        }
298
        user_error('Please set either excludedClasses or includedClasses', E_USER_NOTICE);
299
        return false;
300
    }
301
}
302