Completed
Pull Request — master (#110)
by Franco
02:25
created

DMSDocument::addPermissionsFields()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 20
nc 4
nop 1
1
<?php
2
3
/**
4
 * @package dms
5
 *
6
 * @property Varchar Filename
7
 * @property Varchar Folder
8
 * @property Varchar Title
9
 * @property Text Description
10
 * @property int ViewCount
11
 * @property DateTime LastChanged
12
 * @property Boolean EmbargoedIndefinitely
13
 * @property Boolean EmbargoedUntilPublished
14
 * @property DateTime EmbargoedUntilDate
15
 * @property DateTime ExpireAtDate
16
 * @property Enum DownloadBehavior
17
 * @property Enum CanViewType
18
 * @property Enum CanEditType
19
 *
20
 * @method ManyManyList RelatedDocuments
21
 * @method ManyManyList Tags
22
 * @method ManyManyList ViewerGroups
23
 * @method ManyManyList EditorGroups
24
 *
25
 */
26
class DMSDocument extends DataObject implements DMSDocumentInterface
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
27
{
28
    private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
        "Filename" => "Varchar(255)", // eg. 3469~2011-energysaving-report.pdf
30
        "Folder" => "Varchar(255)",    // eg.	0
31
        "Title" => 'Varchar(1024)', // eg. "Energy Saving Report for Year 2011, New Zealand LandCorp"
32
        "Description" => 'Text',
33
        "ViewCount" => 'Int',
34
        // When this document's file was created or last replaced (small changes like updating title don't count)
35
        "LastChanged" => 'SS_DateTime',
36
37
        "EmbargoedIndefinitely" => 'Boolean(false)',
38
        "EmbargoedUntilPublished" => 'Boolean(false)',
39
        "EmbargoedUntilDate" => 'SS_DateTime',
40
        "ExpireAtDate" => 'SS_DateTime',
41
        "DownloadBehavior" => 'Enum(array("open","download"), "download")',
42
        "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')",
43
        "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
44
    );
45
46
    private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
47
        'Pages' => 'SiteTree',
48
        'RelatedDocuments' => 'DMSDocument',
49
        'Tags' => 'DMSTag',
50
        'ViewerGroups' => 'Group',
51
        'EditorGroups' => 'Group',
52
    );
53
54
    private static $many_many_extraFields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $many_many_extraFields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
55
        'Pages' => array(
56
            'DocumentSort' => 'Int'
57
        )
58
    );
59
60
    private static $display_fields = array(
0 ignored issues
show
Unused Code introduced by
The property $display_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
61
        'ID' => 'ID',
62
        'Title' => 'Title',
63
        'FilenameWithoutID' => 'Filename',
64
        'LastChanged' => 'LastChanged'
65
    );
66
67
    private static $singular_name = 'Document';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $singular_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
68
69
    private static $plural_name = 'Documents';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $plural_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
70
71
    private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $searchable_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
72
        'ID' => array(
73
            'filter' => 'ExactMatchFilter',
74
            'field' => 'NumericField'
75
        ),
76
        'Title',
77
        'Filename',
78
        'LastChanged'
79
    );
80
81
    /**
82
     * @var string download|open
83
     * @config
84
     */
85
    private static $default_download_behaviour = 'download';
0 ignored issues
show
Unused Code introduced by
The property $default_download_behaviour is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
86
87
    public function canView($member = null)
88
    {
89
        if (! $member || ! (is_a($member, 'Member')) || is_numeric($member)) {
90
            $member = Member::currentUser();
91
        }
92
93
        if (! $this->CanViewType || $this->CanViewType == 'Anyone') {
94
            return true;
95
        }
96
97
        if ($this->isHidden()) {
98
            return false;
99
        }
100
101
        if ($this->CanViewType == 'LoggedInUsers') {
102
            return $member && $member->exists();
103
        }
104
105
        if ($this->CanViewType == 'OnlyTheseUsers') {
106
            $result = ($member && $member->inGroups($this->ViewerGroups()));
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
107
108
            return $result;
109
        }
110
111
        if ($member && Permission::checkMember(
112
                $member,
113
                    array(
114
                        'ADMIN',
115
                        'SITETREE_EDIT_ALL',
116
                        'SITETREE_VIEW_ALL'
117
                    )
118
            )
119
        ) {
120
            return true;
121
        }
122
123
        if ($this->canEdit($member)) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $this->canEdit($member);.
Loading history...
124
            return true;
125
        }
126
127
        return false;
128
    }
129
130
    public function canEdit($member = null)
131
    {
132
        if (! $member || ! (is_a($member, 'Member')) || is_numeric($member)) {
133
            $member = Member::currentUser();
134
        }
135
136
        if ($this->CanEditType == 'LoggedInUsers') {
137
            return $member && $member->exists();
138
        }
139
        if ($this->CanEditType == 'OnlyTheseUsers') {
140
            return ($member && $member->inGroups($this->EditorGroups()));
0 ignored issues
show
Documentation Bug introduced by
The method EditorGroups does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
141
        }
142
        if ($member &&
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $member && \Permi... 'SITETREE_EDIT_ALL'));.
Loading history...
143
            Permission::checkMember(
144
                $member,
145
                    array('ADMIN', 'SITETREE_EDIT_ALL')
146
            )
147
        ) {
148
            return true;
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
156
     *
157
     * @return boolean
158
     */
159
    public function canCreate($member = null)
160
    {
161
        return $this->canEdit($member);
162
    }
163
164
    /**
165
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
166
     *
167
     * @return boolean
168
     */
169
    public function canDelete($member = null)
170
    {
171
        if (! $member || ! (is_a($member, 'Member')) || is_numeric($member)) {
172
            $member = Member::currentUser();
173
        }
174
175
        $results = $this->extend('canDelete', $member);
176
177
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
178
            if (! min($results)) {
179
                return false;
180
            }
181
        }
182
183
        return $this->canView();
184
    }
185
186
187
188
    /**
189
     * Associates this document with a Page. This method does nothing if the
190
     * association already exists.
191
     *
192
     * This could be a simple wrapper around $myDoc->Pages()->add($myPage) to
193
     * add a many_many relation.
194
     *
195
     * @param SiteTree $pageObject Page object to associate this Document with
196
     *
197
     * @return DMSDocument
198
     */
199
    public function addPage($pageObject)
200
    {
201
        $this->Pages()->add($pageObject);
202
203
        DB::query(
204
            "UPDATE \"DMSDocument_Pages\" SET \"DocumentSort\"=\"DocumentSort\"+1"
205
            . " WHERE \"SiteTreeID\" = $pageObject->ID"
206
        );
207
208
        return $this;
209
    }
210
211
    /**
212
     * Associates this DMSDocument with a set of Pages. This method loops
213
     * through a set of page ids, and then associates this DMSDocument with the
214
     * individual Page with the each page id in the set.
215
     *
216
     * @param array $pageIDs
217
     *
218
     * @return DMSDocument
219
     */
220
    public function addPages($pageIDs)
221
    {
222
        foreach ($pageIDs as $id) {
223
            $pageObject = DataObject::get_by_id("SiteTree", $id);
224
225
            if ($pageObject && $pageObject->exists()) {
226
                $this->addPage($pageObject);
0 ignored issues
show
Compatibility introduced by
$pageObject of type object<DataObject> is not a sub-type of object<SiteTree>. It seems like you assume a child class of the class DataObject 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...
227
            }
228
        }
229
230
        return $this;
231
    }
232
233
    /**
234
     * Removes the association between this Document and a Page. This method
235
     * does nothing if the association does not exist.
236
     *
237
     * @param SiteTree $pageObject Page object to remove the association to
238
     *
239
     * @return DMSDocument
240
     */
241
    public function removePage($pageObject)
242
    {
243
        $this->Pages()->remove($pageObject);
0 ignored issues
show
Documentation introduced by
$pageObject is of type object<SiteTree>, but the function expects a object<DataClass>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
244
245
        return $this;
246
    }
247
248
    /**
249
     * @see getPages()
250
     *
251
     * @return DataList
252
     */
253
    public function Pages()
254
    {
255
        $pages = $this->getManyManyComponents('Pages');
256
        $this->extend('updatePages', $pages);
257
258
        return $pages;
259
    }
260
261
    /**
262
     * Returns a list of the Page objects associated with this Document.
263
     *
264
     * @return DataList
265
     */
266
    public function getPages()
267
    {
268
        return $this->Pages();
269
    }
270
271
    /**
272
     * Removes all associated Pages from the DMSDocument
273
     *
274
     * @return DMSDocument
275
     */
276
    public function removeAllPages()
277
    {
278
        $this->Pages()->removeAll();
279
280
        return $this;
281
    }
282
283
    /**
284
     * Increase ViewCount by 1, without update any other record fields such as
285
     * LastEdited.
286
     *
287
     * @return DMSDocument
288
     */
289
    public function trackView()
290
    {
291
        if ($this->ID > 0) {
292
            $count = $this->ViewCount + 1;
293
294
            $this->ViewCount = $count;
295
296
            DB::query("UPDATE \"DMSDocument\" SET \"ViewCount\"='$count' WHERE \"ID\"={$this->ID}");
297
        }
298
299
        return $this;
300
    }
301
302
303
    /**
304
     * Adds a metadata tag to the Document. The tag has a category and a value.
305
     *
306
     * Each category can have multiple values by default. So:
307
     * addTag("fruit","banana") addTag("fruit", "apple") will add two items.
308
     *
309
     * However, if the third parameter $multiValue is set to 'false', then all
310
     * updates to a category only ever update a single value. So:
311
     * addTag("fruit","banana") addTag("fruit", "apple") would result in a
312
     * single metadata tag: fruit->apple.
313
     *
314
     * Can could be implemented as a key/value store table (although it is more
315
     * like category/value, because the same category can occur multiple times)
316
     *
317
     * @param string $category of a metadata category to add (required)
318
     * @param string $value of a metadata value to add (required)
319
     * @param bool $multiValue Boolean that determines if the category is
320
     * 					multi-value or single-value (optional)
321
     *
322
     * @return DMSDocument
323
     */
324
    public function addTag($category, $value, $multiValue = true)
325
    {
326
        if ($multiValue) {
327
            //check for a duplicate tag, don't add the duplicate
328
            $currentTag = $this->Tags()->filter(array('Category' => $category, 'Value' => $value));
0 ignored issues
show
Documentation Bug introduced by
The method Tags does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
329
            if ($currentTag->Count() == 0) {
330
                //multi value tag
331
                $tag = new DMSTag();
332
                $tag->Category = $category;
0 ignored issues
show
Documentation introduced by
The property Category does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
333
                $tag->Value = $value;
0 ignored issues
show
Documentation introduced by
The property Value does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
334
                $tag->MultiValue = true;
0 ignored issues
show
Documentation introduced by
The property MultiValue does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
335
                $tag->write();
336
                $tag->Documents()->add($this);
0 ignored issues
show
Documentation Bug introduced by
The method Documents does not exist on object<DMSTag>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
337
            } else {
338
                //add the relation between the tag and document
339
                foreach ($currentTag as $tagObj) {
340
                    $tagObj->Documents()->add($this);
341
                }
342
            }
343
        } else {
344
            //single value tag
345
            $currentTag = $this->Tags()->filter(array('Category' => $category));
0 ignored issues
show
Documentation Bug introduced by
The method Tags does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
346
            $tag = null;
0 ignored issues
show
Unused Code introduced by
$tag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
347
            if ($currentTag->Count() == 0) {
348
                //create the single-value tag
349
                $tag = new DMSTag();
350
                $tag->Category = $category;
0 ignored issues
show
Documentation introduced by
The property Category does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
351
                $tag->Value = $value;
0 ignored issues
show
Documentation introduced by
The property Value does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
352
                $tag->MultiValue = false;
0 ignored issues
show
Documentation introduced by
The property MultiValue does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

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...
353
                $tag->write();
354
            } else {
355
                //update the single value tag
356
                $tag = $currentTag->first();
357
                $tag->Value = $value;
358
                $tag->MultiValue = false;
359
                $tag->write();
360
            }
361
362
            // regardless of whether we created a new tag or are just updating an
363
            // existing one, add the relation
364
            $tag->Documents()->add($this);
365
        }
366
367
        return $this;
368
    }
369
370
    /**
371
     * @param string $category
372
     * @param string $value
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
373
     *
374
     * @return DataList
375
     */
376
    protected function getTagsObjects($category, $value = null)
377
    {
378
        $valueFilter = array("Category" => $category);
379
        if (!empty($value)) {
380
            $valueFilter['Value'] = $value;
381
        }
382
383
        $tags = $this->Tags()->filter($valueFilter);
0 ignored issues
show
Documentation Bug introduced by
The method Tags does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
384
        return $tags;
385
    }
386
387
    /**
388
     * Fetches all tags associated with this DMSDocument within a given
389
     * category. If a value is specified this method tries to fetch that
390
     * specific tag.
391
     *
392
     * @param string $category metadata category to get
393
     * @param string $value value of the tag to get
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
394
     *
395
     * @return array Strings of all the tags or null if there is no match found
0 ignored issues
show
Documentation introduced by
Should the return type not be array|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
396
     */
397
    public function getTagsList($category, $value = null)
398
    {
399
        $tags = $this->getTagsObjects($category, $value);
400
401
        $returnArray = null;
402
403
        if ($tags->Count() > 0) {
404
            $returnArray = array();
405
406
            foreach ($tags as $t) {
407
                $returnArray[] = $t->Value;
408
            }
409
        }
410
411
        return $returnArray;
412
    }
413
414
    /**
415
     * Removes a tag from the Document. If you only set a category, then all
416
     * values in that category are deleted.
417
     *
418
     * If you specify both a category and a value, then only that single
419
     * category/value pair is deleted.
420
     *
421
     * Nothing happens if the category or the value do not exist.
422
     *
423
     * @param string $category Category to remove
424
     * @param string $value Value to remove
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
425
     *
426
     * @return DMSDocument
427
     */
428
    public function removeTag($category, $value = null)
429
    {
430
        $tags = $this->getTagsObjects($category, $value);
431
432
        if ($tags->Count() > 0) {
433
            foreach ($tags as $t) {
434
                $documentList = $t->Documents();
435
436
                //remove the relation between the tag and the document
437
                $documentList->remove($this);
438
439
                //delete the entire tag if it has no relations left
440
                if ($documentList->Count() == 0) {
441
                    $t->delete();
442
                }
443
            }
444
        }
445
446
        return $this;
447
    }
448
449
    /**
450
     * Deletes all tags associated with this Document.
451
     *
452
     * @return DMSDocument
453
     */
454
    public function removeAllTags()
455
    {
456
        $allTags = $this->Tags();
0 ignored issues
show
Documentation Bug introduced by
The method Tags does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
457
458
        foreach ($allTags as $tag) {
459
            $documentlist = $tag->Documents();
460
            $documentlist->remove($this);
461
            if ($tag->Documents()->Count() == 0) {
462
                $tag->delete();
463
            }
464
        }
465
466
        return $this;
467
    }
468
469
    /**
470
     * Returns a link to download this document from the DMS store.
471
     *
472
     * @return string
473
     */
474
    public function getLink()
475
    {
476
        return Controller::join_links(Director::baseURL(), 'dmsdocument/'.$this->ID);
477
    }
478
479
    /**
480
     * @return string
481
     */
482
    public function Link()
483
    {
484
        return $this->getLink();
485
    }
486
487
    /**
488
     * Hides the document, so it does not show up when getByPage($myPage) is
489
     * called (without specifying the $showEmbargoed = true parameter).
490
     *
491
     * This is similar to expire, except that this method should be used to hide
492
     * documents that have not yet gone live.
493
     *
494
     * @param bool $write Save change to the database
495
     *
496
     * @return DMSDocument
497
     */
498
    public function embargoIndefinitely($write = true)
499
    {
500
        $this->EmbargoedIndefinitely = true;
501
502
        if ($write) {
503
            $this->write();
504
        }
505
506
        return $this;
507
    }
508
509
    /**
510
     * Hides the document until any page it is linked to is published
511
     *
512
     * @param bool $write Save change to database
513
     *
514
     * @return DMSDocument
515
     */
516
    public function embargoUntilPublished($write = true)
517
    {
518
        $this->EmbargoedUntilPublished = true;
519
520
        if ($write) {
521
            $this->write();
522
        }
523
524
        return $this;
525
    }
526
527
    /**
528
     * Returns if this is Document is embargoed or expired.
529
     *
530
     * Also, returns if the document should be displayed on the front-end,
531
     * respecting the current reading mode of the site and the embargo status.
532
     *
533
     * I.e. if a document is embargoed until published, then it should still
534
     * show up in draft mode.
535
     *
536
     * @return bool
537
     */
538
    public function isHidden()
539
    {
540
        $hidden = $this->isEmbargoed() || $this->isExpired();
541
        $readingMode = Versioned::get_reading_mode();
542
543
        if ($readingMode == "Stage.Stage") {
544
            if ($this->EmbargoedUntilPublished == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
545
                $hidden = false;
546
            }
547
        }
548
549
        return $hidden;
550
    }
551
552
    /**
553
     * Returns if this is Document is embargoed.
554
     *
555
     * @return bool
556
     */
557
    public function isEmbargoed()
558
    {
559
        if (is_object($this->EmbargoedUntilDate)) {
560
            $this->EmbargoedUntilDate = $this->EmbargoedUntilDate->Value;
0 ignored issues
show
Bug introduced by
The property Value does not seem to exist in DateTime.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
561
        }
562
563
        $embargoed = false;
564
565
        if ($this->EmbargoedIndefinitely) {
566
            $embargoed = true;
567
        } elseif ($this->EmbargoedUntilPublished) {
568
            $embargoed = true;
569
        } elseif (!empty($this->EmbargoedUntilDate)) {
570
            if (SS_Datetime::now()->Value < $this->EmbargoedUntilDate) {
571
                $embargoed = true;
572
            }
573
        }
574
575
        return $embargoed;
576
    }
577
578
    /**
579
     * Hides the document, so it does not show up when getByPage($myPage) is
580
     * called. Automatically un-hides the Document at a specific date.
581
     *
582
     * @param string $datetime date time value when this Document should expire.
583
     * @param bool $write
584
     *
585
     * @return DMSDocument
586
     */
587 View Code Duplication
    public function embargoUntilDate($datetime, $write = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
588
    {
589
        $this->EmbargoedUntilDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
590
591
        if ($write) {
592
            $this->write();
593
        }
594
595
        return $this;
596
    }
597
598
    /**
599
     * Clears any previously set embargos, so the Document always shows up in
600
     * all queries.
601
     *
602
     * @param bool $write
603
     *
604
     * @return DMSDocument
605
     */
606
    public function clearEmbargo($write = true)
607
    {
608
        $this->EmbargoedIndefinitely = false;
609
        $this->EmbargoedUntilPublished = false;
610
        $this->EmbargoedUntilDate = null;
611
612
        if ($write) {
613
            $this->write();
614
        }
615
616
        return $this;
617
    }
618
619
    /**
620
     * Returns if this is Document is expired.
621
     *
622
     * @return bool
623
     */
624
    public function isExpired()
625
    {
626
        if (is_object($this->ExpireAtDate)) {
627
            $this->ExpireAtDate = $this->ExpireAtDate->Value;
0 ignored issues
show
Bug introduced by
The property Value does not seem to exist in DateTime.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
628
        }
629
630
        $expired = false;
631
632
        if (!empty($this->ExpireAtDate)) {
633
            if (SS_Datetime::now()->Value >= $this->ExpireAtDate) {
634
                $expired = true;
635
            }
636
        }
637
638
        return $expired;
639
    }
640
641
    /**
642
     * Hides the document at a specific date, so it does not show up when
643
     * getByPage($myPage) is called.
644
     *
645
     * @param string $datetime date time value when this Document should expire
646
     * @param bool $write
647
     *
648
     * @return DMSDocument
649
     */
650 View Code Duplication
    public function expireAtDate($datetime, $write = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
651
    {
652
        $this->ExpireAtDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
653
654
        if ($write) {
655
            $this->write();
656
        }
657
658
        return $this;
659
    }
660
661
    /**
662
     * Clears any previously set expiry.
663
     *
664
     * @param bool $write
665
     *
666
     * @return DMSDocument
667
     */
668
    public function clearExpiry($write = true)
669
    {
670
        $this->ExpireAtDate = null;
671
672
        if ($write) {
673
            $this->write();
674
        }
675
676
        return $this;
677
    }
678
679
    /**
680
     * Returns a DataList of all previous Versions of this document (check the
681
     * LastEdited date of each object to find the correct one).
682
     *
683
     * If {@link DMSDocument_versions::$enable_versions} is disabled then an
684
     * Exception is thrown
685
     *
686
     * @throws Exception
687
     *
688
     * @return DataList List of Document objects
689
     */
690
    public function getVersions()
691
    {
692
        if (!DMSDocument_versions::$enable_versions) {
693
            throw new Exception("DMSDocument versions are disabled");
694
        }
695
696
        return DMSDocument_versions::get_versions($this);
697
    }
698
699
    /**
700
     * Returns the full filename of the document stored in this object.
701
     *
702
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
703
     */
704
    public function getFullPath()
705
    {
706
        if ($this->Filename) {
707
            return DMS::get_dms_path() . DIRECTORY_SEPARATOR . $this->Folder . DIRECTORY_SEPARATOR . $this->Filename;
708
        }
709
710
        return null;
711
    }
712
713
    /**
714
     * Returns the filename of this asset.
715
     *
716
     * @return string
717
     */
718
    public function getFileName()
719
    {
720
        if ($this->getField('Filename')) {
721
            return $this->getField('Filename');
722
        } else {
723
            return ASSETS_DIR . '/';
724
        }
725
    }
726
727
    /**
728
     * @return string
729
     */
730
    public function getName()
731
    {
732
        return $this->getField('Title');
733
    }
734
735
736
    /**
737
     * @return string
738
     */
739
    public function getFilenameWithoutID()
740
    {
741
        $filenameParts = explode('~', $this->Filename);
742
        $filename = array_pop($filenameParts);
743
744
        return $filename;
745
    }
746
747
    /**
748
     * @return string
749
     */
750
    public function getStorageFolder()
751
    {
752
        return DMS::get_dms_path() . DIRECTORY_SEPARATOR . DMS::get_storage_folder($this->ID);
753
    }
754
755
    /**
756
     * Deletes the DMSDocument, its underlying file, as well as any tags related
757
     * to this DMSDocument. Also calls the parent DataObject's delete method in
758
     * order to complete an cascade.
759
     *
760
     * @return void
761
     */
762
    public function delete()
763
    {
764
        // remove tags
765
        $this->removeAllTags();
766
767
        // delete the file (and previous versions of files)
768
        $filesToDelete = array();
769
        $storageFolder = $this->getStorageFolder();
770
771
        if (file_exists($storageFolder)) {
772
            if ($handle = opendir($storageFolder)) {
773
                while (false !== ($entry = readdir($handle))) {
774
                    // only delete if filename starts the the relevant ID
775
                    if (strpos($entry, $this->ID.'~') === 0) {
776
                        $filesToDelete[] = $entry;
777
                    }
778
                }
779
780
                closedir($handle);
781
782
                //delete all this files that have the id of this document
783
                foreach ($filesToDelete as $file) {
784
                    $filePath = $storageFolder .DIRECTORY_SEPARATOR . $file;
785
786
                    if (is_file($filePath)) {
787
                        unlink($filePath);
788
                    }
789
                }
790
            }
791
        }
792
793
        $this->removeAllPages();
794
795
        // get rid of any versions have saved for this DMSDocument, too
796
        if (DMSDocument_versions::$enable_versions) {
797
            $versions = $this->getVersions();
798
799
            if ($versions->Count() > 0) {
800
                foreach ($versions as $v) {
801
                    $v->delete();
802
                }
803
            }
804
        }
805
806
        parent::delete();
807
    }
808
809
810
811
    /**
812
     * Relate an existing file on the filesystem to the document.
813
     *
814
     * Copies the file to the new destination, as defined in {@link get_DMS_path()}.
815
     *
816
     * @param string $filePath Path to file, relative to webroot.
817
     *
818
     * @return DMSDocument
819
     */
820
    public function storeDocument($filePath)
821
    {
822
        if (empty($this->ID)) {
823
            user_error("Document must be written to database before it can store documents", E_USER_ERROR);
824
        }
825
826
        // calculate all the path to copy the file to
827
        $fromFilename = basename($filePath);
828
        $toFilename = $this->ID. '~' . $fromFilename; //add the docID to the start of the Filename
829
        $toFolder = DMS::get_storage_folder($this->ID);
830
        $toPath = DMS::get_dms_path() . DIRECTORY_SEPARATOR . $toFolder . DIRECTORY_SEPARATOR . $toFilename;
831
832
        DMS::create_storage_folder(DMS::get_dms_path() . DIRECTORY_SEPARATOR . $toFolder);
833
834
        //copy the file into place
835
        $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
836
837
        //version the existing file (copy it to a new "very specific" filename
838
        if (DMSDocument_versions::$enable_versions) {
839
            DMSDocument_versions::create_version($this);
840
        } else {    //otherwise delete the old document file
841
            $oldPath = $this->getFullPath();
842
            if (file_exists($oldPath)) {
843
                unlink($oldPath);
844
            }
845
        }
846
847
        copy($fromPath, $toPath);   //this will overwrite the existing file (if present)
848
849
        //write the filename of the stored document
850
        $this->Filename = $toFilename;
851
        $this->Folder = strval($toFolder);
852
853
        $extension = pathinfo($this->Filename, PATHINFO_EXTENSION);
854
855
        if (empty($this->Title)) {
856
            // don't overwrite existing document titles
857
            $this->Title = basename($filePath, '.'.$extension);
858
        }
859
860
        $this->LastChanged = SS_Datetime::now()->Rfc2822();
861
        $this->write();
862
863
        return $this;
864
    }
865
866
    /**
867
     * Takes a File object or a String (path to a file) and copies it into the
868
     * DMS, replacing the original document file but keeping the rest of the
869
     * document unchanged.
870
     *
871
     * @param File|string $file path to a file to store
872
     *
873
     * @return DMSDocument object that we replaced the file in
874
     */
875
    public function replaceDocument($file)
876
    {
877
        $filePath = DMS::transform_file_to_file_path($file);
878
        $doc = $this->storeDocument($filePath); // replace the document
879
880
        return $doc;
881
    }
882
883
884
    /**
885
     * Return the type of file for the given extension
886
     * on the current file name.
887
     *
888
     * @param string $ext
889
     *
890
     * @return string
891
     */
892
    public static function get_file_type($ext)
893
    {
894
        $types = array(
895
            'gif' => 'GIF image - good for diagrams',
896
            'jpg' => 'JPEG image - good for photos',
897
            'jpeg' => 'JPEG image - good for photos',
898
            'png' => 'PNG image - good general-purpose format',
899
            'ico' => 'Icon image',
900
            'tiff' => 'Tagged image format',
901
            'doc' => 'Word document',
902
            'xls' => 'Excel spreadsheet',
903
            'zip' => 'ZIP compressed file',
904
            'gz' => 'GZIP compressed file',
905
            'dmg' => 'Apple disk image',
906
            'pdf' => 'Adobe Acrobat PDF file',
907
            'mp3' => 'MP3 audio file',
908
            'wav' => 'WAV audo file',
909
            'avi' => 'AVI video file',
910
            'mpg' => 'MPEG video file',
911
            'mpeg' => 'MPEG video file',
912
            'js' => 'Javascript file',
913
            'css' => 'CSS file',
914
            'html' => 'HTML file',
915
            'htm' => 'HTML file'
916
        );
917
918
        return isset($types[$ext]) ? $types[$ext] : $ext;
919
    }
920
921
922
    /**
923
     * Returns the Description field with HTML <br> tags added when there is a
924
     * line break.
925
     *
926
     * @return string
927
     */
928
    public function getDescriptionWithLineBreak()
929
    {
930
        return nl2br($this->getField('Description'));
931
    }
932
933
    /**
934
     * @return FieldList
935
     */
936
    public function getCMSFields()
937
    {
938
        //include JS to handling showing and hiding of bottom "action" tabs
939
        Requirements::javascript(DMS_DIR.'/javascript/DMSDocumentCMSFields.js');
940
        Requirements::css(DMS_DIR.'/css/DMSDocumentCMSFields.css');
941
942
        $fields = new FieldList();  //don't use the automatic scaffolding, it is slow and unnecessary here
943
944
        $extraTasks = '';   //additional text to inject into the list of tasks at the bottom of a DMSDocument CMSfield
945
946
        //get list of shortcode page relations
947
        $relationFinder = new ShortCodeRelationFinder();
948
        $relationList = $relationFinder->getList($this->ID);
949
950
        $fieldsTop = $this->getFieldsForFile($relationList->count());
951
        $fields->add($fieldsTop);
952
953
        $fields->add(new TextField('Title', 'Title'));
954
        $fields->add(new TextareaField('Description', 'Description'));
955
956
        $downloadBehaviorSource = array(
957
            'open' => _t('DMSDocument.OPENINBROWSER', 'Open in browser'),
958
            'download' => _t('DMSDocument.FORCEDOWNLOAD', 'Force download'),
959
        );
960
        $defaultDownloadBehaviour = Config::inst()->get('DMSDocument', 'default_download_behaviour');
961
        if (!isset($downloadBehaviorSource[$defaultDownloadBehaviour])) {
962
            user_error('Default download behaviour "' . $defaultDownloadBehaviour . '" not supported.', E_USER_WARNING);
963
        } else {
964
            $downloadBehaviorSource[$defaultDownloadBehaviour] .= ' (' . _t('DMSDocument.DEFAULT', 'default') . ')';
965
        }
966
967
        $fields->add(
968
            OptionsetField::create(
969
                'DownloadBehavior',
970
                _t('DMSDocument.DOWNLOADBEHAVIOUR', 'Download behavior'),
971
                $downloadBehaviorSource,
972
                $defaultDownloadBehaviour
973
            )
974
            ->setDescription(
975
                'How the visitor will view this file. <strong>Open in browser</strong> '
976
                . 'allows files to be opened in a new tab.'
977
            )
978
        );
979
980
        //create upload field to replace document
981
        $uploadField = new DMSUploadField('ReplaceFile', 'Replace file');
982
        $uploadField->setConfig('allowedMaxFileNumber', 1);
983
        $uploadField->setConfig('downloadTemplateName', 'ss-dmsuploadfield-downloadtemplate');
984
        $uploadField->setRecord($this);
985
986
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
987
            new GridFieldToolbarHeader(),
988
            new GridFieldSortableHeader(),
989
            new GridFieldDataColumns(),
990
            new GridFieldPaginator(30),
991
            //new GridFieldEditButton(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
992
            new GridFieldDetailForm()
993
        );
994
995
        $gridFieldConfig->getComponentByType('GridFieldDataColumns')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns, GridFieldEditableColumns, GridFieldExternalLink.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
996
            ->setDisplayFields(array(
997
                'Title'=>'Title',
998
                'ClassName'=>'Page Type',
999
                'ID'=>'Page ID'
1000
            ))
1001
            ->setFieldFormatting(array(
1002
                'Title'=>sprintf(
1003
                    '<a class=\"cms-panel-link\" href=\"%s/$ID\">$Title</a>',
1004
                    singleton('CMSPageEditController')->Link('show')
1005
                )
1006
            ));
1007
1008
        $pagesGrid = GridField::create(
1009
            'Pages',
1010
            _t('DMSDocument.RelatedPages', 'Related Pages'),
1011
            $this->Pages(),
1012
            $gridFieldConfig
1013
        );
1014
1015
        $referencesGrid = GridField::create(
1016
            'References',
1017
            _t('DMSDocument.RelatedReferences', 'Related References'),
1018
            $relationList,
1019
            $gridFieldConfig
1020
        );
1021
1022
        if (DMSDocument_versions::$enable_versions) {
1023
            $versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
1024
                new GridFieldToolbarHeader(),
1025
                new GridFieldSortableHeader(),
1026
                new GridFieldDataColumns(),
1027
                new GridFieldPaginator(30)
1028
            );
1029
            $versionsGridFieldConfig->getComponentByType('GridFieldDataColumns')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns, GridFieldEditableColumns, GridFieldExternalLink.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1030
                ->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
1031
                ->setFieldCasting(array('LastChanged'=>"Datetime->Ago"))
1032
                ->setFieldFormatting(
1033
                    array(
1034
                        'FilenameWithoutID' => '<a target=\'_blank\' class=\'file-url\' href=\'$Link\'>'
1035
                            . '$FilenameWithoutID</a>'
1036
                    )
1037
                );
1038
1039
            $versionsGrid =  GridField::create(
1040
                'Versions',
1041
                _t('DMSDocument.Versions', 'Versions'),
1042
                $this->getVersions(),
1043
                $versionsGridFieldConfig
1044
            );
1045
            $extraTasks .= '<li class="ss-ui-button" data-panel="find-versions">Versions</li>';
1046
        }
1047
1048
        $fields->add(new LiteralField(
1049
            'BottomTaskSelection',
1050
            '<div id="Actions" class="field actions"><label class="left">Actions</label><ul>'
1051
            . '<li class="ss-ui-button" data-panel="embargo">Embargo</li>'
1052
            . '<li class="ss-ui-button" data-panel="expiry">Expiry</li>'
1053
            . '<li class="ss-ui-button" data-panel="replace">Replace</li>'
1054
            . '<li class="ss-ui-button" data-panel="find-usage">Usage</li>'
1055
            . '<li class="ss-ui-button" data-panel="find-references">References</li>'
1056
            . '<li class="ss-ui-button" data-panel="find-relateddocuments">Related Documents</li>'
1057
            . $extraTasks
1058
            . '</ul></div>'
1059
        ));
1060
1061
        $embargoValue = 'None';
1062
        if ($this->EmbargoedIndefinitely) {
1063
            $embargoValue = 'Indefinitely';
1064
        } elseif ($this->EmbargoedUntilPublished) {
1065
            $embargoValue = 'Published';
1066
        } elseif (!empty($this->EmbargoedUntilDate)) {
1067
            $embargoValue = 'Date';
1068
        }
1069
        $embargo = new OptionsetField(
1070
            'Embargo',
1071
            'Embargo',
1072
            array(
1073
                'None' => 'None',
1074
                'Published' => 'Hide document until page is published',
1075
                'Indefinitely' => 'Hide document indefinitely',
1076
                'Date' => 'Hide until set date'
1077
            ),
1078
            $embargoValue
1079
        );
1080
        $embargoDatetime = DatetimeField::create('EmbargoedUntilDate', '');
1081
        $embargoDatetime->getDateField()
1082
            ->setConfig('showcalendar', true)
1083
            ->setConfig('dateformat', 'dd-MM-yyyy')
1084
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
1085
1086
        $expiryValue = 'None';
1087
        if (!empty($this->ExpireAtDate)) {
1088
            $expiryValue = 'Date';
1089
        }
1090
        $expiry = new OptionsetField(
1091
            'Expiry',
1092
            'Expiry',
1093
            array(
1094
                'None' => 'None',
1095
                'Date' => 'Set document to expire on'
1096
            ),
1097
            $expiryValue
1098
        );
1099
        $expiryDatetime = DatetimeField::create('ExpireAtDate', '');
1100
        $expiryDatetime->getDateField()
1101
            ->setConfig('showcalendar', true)
1102
            ->setConfig('dateformat', 'dd-MM-yyyy')
1103
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
1104
1105
        // This adds all the actions details into a group.
1106
        // Embargo, History, etc to go in here
1107
        // These are toggled on and off via the Actions Buttons above
1108
        // exit('hit');
0 ignored issues
show
Unused Code Comprehensibility introduced by
84% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1109
        $actionsPanel = FieldGroup::create(
1110
            FieldGroup::create($embargo, $embargoDatetime)->addExtraClass('embargo'),
1111
            FieldGroup::create($expiry, $expiryDatetime)->addExtraClass('expiry'),
1112
            FieldGroup::create($uploadField)->addExtraClass('replace'),
1113
            FieldGroup::create($pagesGrid)->addExtraClass('find-usage'),
1114
            FieldGroup::create($referencesGrid)->addExtraClass('find-references'),
1115
            FieldGroup::create($versionsGrid)->addExtraClass('find-versions'),
0 ignored issues
show
Bug introduced by
The variable $versionsGrid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1116
            FieldGroup::create($this->getRelatedDocumentsGridField())->addExtraClass('find-relateddocuments')
1117
        );
1118
1119
        $actionsPanel->setName("ActionsPanel");
1120
        $actionsPanel->addExtraClass("DMSDocumentActionsPanel");
1121
        $fields->push($actionsPanel);
1122
1123
        $this->addPermissionsFields($fields);
1124
        $this->extend('updateCMSFields', $fields);
1125
1126
        return $fields;
1127
    }
1128
1129
    /**
1130
     * Adds permissions selection fields to the FieldList.
1131
     *
1132
     * @param FieldList $fields
1133
     */
1134
    public function addPermissionsFields($fields)
1135
    {
1136
        $showFields = array(
1137
            'CanViewType'  => '',
1138
            'ViewerGroups' => 'hide',
1139
            'CanEditType'  => '',
1140
            'EditorGroups' => 'hide',
1141
        );
1142
        /** @var SiteTree $siteTree */
1143
        $siteTree = singleton('SiteTree');
1144
        $settingsFields = $siteTree->getSettingsFields();
1145
1146
        foreach ($showFields as $name => $extraCss) {
1147
            $compositeName = "Root.Settings.$name";
1148
            /** @var FormField $field */
1149
            if ($field = $settingsFields->fieldByName($compositeName)) {
1150
                $field->addExtraClass($extraCss);
1151
                $title = str_replace('page', 'document', $field->Title());
1152
                $field->setTitle($title);
1153
1154
                //Remove Inherited source option from DropdownField
1155
                if ($field instanceof DropdownField) {
1156
                    $options = $field->getSourceAsArray();
1157
                    unset($options['Inherit']);
1158
                    $field->setSource($options);
1159
                }
1160
                $fields->push($field);
1161
            }
1162
        }
1163
1164
        $this->extend('updatePermissionsFields', $fields);
1165
    }
1166
1167
    public function onBeforeWrite()
1168
    {
1169
        parent::onBeforeWrite();
1170
1171
        if (isset($this->Embargo)) {
1172
            //set the embargo options from the OptionSetField created in the getCMSFields method
1173
            //do not write after clearing the embargo (write happens automatically)
1174
            $savedDate = $this->EmbargoedUntilDate;
1175
            $this->clearEmbargo(false); //clear all previous settings and re-apply them on save
1176
1177
            if ($this->Embargo == 'Published') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1178
                $this->embargoUntilPublished(false);
1179
            }
1180
            if ($this->Embargo == 'Indefinitely') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1181
                $this->embargoIndefinitely(false);
1182
            }
1183
            if ($this->Embargo == 'Date') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1184
                $this->embargoUntilDate($savedDate, false);
0 ignored issues
show
Documentation introduced by
$savedDate is of type object<DateTime>|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1185
            }
1186
        }
1187
1188
        if (isset($this->Expiry)) {
1189
            if ($this->Expiry == 'Date') {
0 ignored issues
show
Documentation introduced by
The property Expiry does not exist on object<DMSDocument>. 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...
1190
                $this->expireAtDate($this->ExpireAtDate, false);
0 ignored issues
show
Documentation introduced by
$this->ExpireAtDate is of type object<DateTime>|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1191
            } else {
1192
                $this->clearExpiry(false);
1193
            } //clear all previous settings
1194
        }
1195
1196
    }
1197
1198
    /**
1199
     * Return the relative URL of an icon for the file type, based on the
1200
     * {@link appCategory()} value.
1201
     *
1202
     * Images are searched for in "dms/images/app_icons/".
1203
     *
1204
     * @return string
1205
     */
1206
    public function Icon($ext)
1207
    {
1208
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1209
            $ext = File::get_app_category($ext);
1210
        }
1211
1212
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1213
            $ext = "generic";
1214
        }
1215
1216
        return DMS_DIR."/images/app_icons/{$ext}_32.png";
1217
    }
1218
1219
    /**
1220
     * Return the extension of the file associated with the document
1221
     *
1222
     * @return string
1223
     */
1224
    public function getExtension()
1225
    {
1226
        return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
1227
    }
1228
1229
    /**
1230
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1231
     */
1232
    public function getSize()
1233
    {
1234
        $size = $this->getAbsoluteSize();
1235
        return ($size) ? File::format_size($size) : false;
1236
    }
1237
1238
    /**
1239
     * Return the size of the file associated with the document.
1240
     *
1241
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1242
     */
1243
    public function getAbsoluteSize()
1244
    {
1245
        return file_exists($this->getFullPath()) ? filesize($this->getFullPath()) : null;
1246
    }
1247
1248
    /**
1249
     * An alias to DMSDocument::getSize()
1250
     *
1251
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1252
     */
1253
    public function getFileSizeFormatted()
1254
    {
1255
        return $this->getSize();
1256
    }
1257
1258
1259
    /**
1260
     * @return FieldList
0 ignored issues
show
Documentation introduced by
Should the return type not be FieldGroup?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1261
     */
1262
    protected function getFieldsForFile($relationListCount)
1263
    {
1264
        $extension = $this->getExtension();
1265
1266
        $previewField = new LiteralField(
1267
            "ImageFull",
1268
            "<img id='thumbnailImage' class='thumbnail-preview' src='{$this->Icon($extension)}?r="
1269
            . rand(1, 100000) . "' alt='{$this->Title}' />\n"
1270
        );
1271
1272
        //count the number of pages this document is published on
1273
        $publishedOnCount = $this->Pages()->Count();
1274
        $publishedOnValue = "$publishedOnCount pages";
1275
        if ($publishedOnCount == 1) {
1276
            $publishedOnValue = "$publishedOnCount page";
1277
        }
1278
1279
        $relationListCountValue = "$relationListCount pages";
1280
        if ($relationListCount == 1) {
1281
            $relationListCountValue = "$relationListCount page";
1282
        }
1283
1284
        $fields = new FieldGroup(
1285
            $filePreview = CompositeField::create(
1286
                CompositeField::create(
1287
                    $previewField
1288
                )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
1289
                CompositeField::create(
1290
                    CompositeField::create(
1291
                        new ReadonlyField("ID", "ID number". ':', $this->ID),
1292
                        new ReadonlyField(
1293
                            "FileType",
1294
                            _t('AssetTableField.TYPE', 'File type') . ':',
1295
                            self::get_file_type($extension)
1296
                        ),
1297
                        new ReadonlyField(
1298
                            "Size",
1299
                            _t('AssetTableField.SIZE', 'File size') . ':',
1300
                            $this->getFileSizeFormatted()
1301
                        ),
1302
                        $urlField = new ReadonlyField(
1303
                            'ClickableURL',
1304
                            _t('AssetTableField.URL', 'URL'),
1305
                            sprintf(
1306
                                '<a href="%s" target="_blank" class="file-url">%s</a>',
1307
                                $this->getLink(),
1308
                                $this->getLink()
1309
                            )
1310
                        ),
1311
                        new ReadonlyField("FilenameWithoutIDField", "Filename". ':', $this->getFilenameWithoutID()),
1312
                        new DateField_Disabled(
1313
                            "Created",
1314
                            _t('AssetTableField.CREATED', 'First uploaded') . ':',
1315
                            $this->Created
1316
                        ),
1317
                        new DateField_Disabled(
1318
                            "LastEdited",
1319
                            _t('AssetTableField.LASTEDIT', 'Last changed') . ':',
1320
                            $this->LastEdited
1321
                        ),
1322
                        new DateField_Disabled(
1323
                            "LastChanged",
1324
                            _t('AssetTableField.LASTCHANGED', 'Last replaced') . ':',
1325
                            $this->LastChanged
1326
                        ),
1327
                        new ReadonlyField("PublishedOn", "Published on". ':', $publishedOnValue),
1328
                        new ReadonlyField("ReferencedOn", "Referenced on". ':', $relationListCountValue),
1329
                        new ReadonlyField("ViewCount", "View count". ':', $this->ViewCount)
1330
                    )
1331
                )->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
1332
            )->setName("FilePreview")->addExtraClass('cms-file-info')
1333
        );
1334
1335
        $fields->setName('FileP');
1336
        $urlField->dontEscape = true;
1337
1338
        return $fields;
1339
    }
1340
1341
    /**
1342
     * Takes a file and adds it to the DMSDocument storage, replacing the
1343
     * current file.
1344
     *
1345
     * @param File $file
1346
     *
1347
     * @return $this
1348
     */
1349
    public function ingestFile($file)
1350
    {
1351
        $this->replaceDocument($file);
1352
        $file->delete();
1353
1354
        return $this;
1355
    }
1356
1357
    /**
1358
     * Get a data list of documents related to this document
1359
     *
1360
     * @return DataList
1361
     */
1362
    public function getRelatedDocuments()
1363
    {
1364
        $documents = $this->RelatedDocuments();
0 ignored issues
show
Bug introduced by
The method RelatedDocuments() does not exist on DMSDocument. Did you maybe mean getRelatedDocuments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1365
1366
        $this->extend('updateRelatedDocuments', $documents);
1367
1368
        return $documents;
1369
    }
1370
1371
    /**
1372
     * Get a GridField for managing related documents
1373
     *
1374
     * @return GridField
1375
     */
1376
    protected function getRelatedDocumentsGridField()
1377
    {
1378
        $gridField = GridField::create(
1379
            'RelatedDocuments',
1380
            _t('DMSDocument.RELATEDDOCUMENTS', 'Related Documents'),
1381
            $this->RelatedDocuments(),
0 ignored issues
show
Bug introduced by
The method RelatedDocuments() does not exist on DMSDocument. Did you maybe mean getRelatedDocuments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1382
            new GridFieldConfig_RelationEditor
1383
        );
1384
1385
        $gridField->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1386
        // Move the autocompleter to the left
1387
        $gridField->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1388
        $gridField->getConfig()->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-left'));
1389
1390
        $this->extend('updateRelatedDocumentsGridField', $gridField);
1391
1392
        return $gridField;
1393
    }
1394
}
1395