Issues (150)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/model/Attendee.php (12 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Attendee.php
4
 *
5
 * @author Bram de Leeuw
6
 * Date: 09/03/17
7
 */
8
9
namespace Broarm\EventTickets;
10
11
use ArrayList;
12
use BaconQrCode;
13
use BetterButtonCustomAction;
14
use CalendarEvent;
15
use Config;
16
use DataObject;
17
use Director;
18
use Dompdf\Dompdf;
19
use Email;
20
use FieldList;
21
use File;
22
use Folder;
23
use Image;
24
use ManyManyList;
25
use Member;
26
use ReadonlyField;
27
use SilverStripe\Omnipay\Exception\Exception;
28
use SSViewer;
29
use Tab;
30
use TabSet;
31
use ViewableData;
32
33
/**
34
 * Class Attendee
35
 *
36
 * @package Broarm\EventTickets
37
 *
38
 * @property string    Title
39
 * @property string    TicketCode
40
 * @property boolean   TicketReceiver
41
 * @property boolean   CheckedIn
42
 * @property FieldList SavableFields    Field to be set in AttendeesField
43
 *
44
 * @property int       TicketID
45
 * @property int       TicketQRCodeID
46
 * @property int       TicketFileID
47
 * @property int       ReservationID
48
 * @property int       EventID
49
 * @property int       MemberID
50
 *
51
 * @method Reservation Reservation()
52
 * @method Ticket Ticket()
53
 * @method Image TicketQRCode()
54
 * @method File TicketFile()
55
 * @method Member Member()
56
 * @method CalendarEvent|TicketExtension Event()
57
 * @method ManyManyList Fields()
58
 */
59
class Attendee extends DataObject
60
{
61
    /**
62
     * Set this to true when you want to have a QR code that opens the check in page and validates the code.
63
     * The validation is only done with proper authorisation so guest cannot check themselves in by mistake.
64
     * By default only the ticket number is translated to an QR code. (for use with USB QR scanners)
65
     *
66
     * @var bool
67
     */
68
    private static $qr_as_link = false;
69
70
    private static $default_fields = array(
71
        'FirstName' => array(
72
            'Title' => 'First name',
73
            'FieldType' => 'UserTextField',
74
            'Required' => true,
75
            'Editable' => false
76
        ),
77
        'Surname' => array(
78
            'Title' => 'Surname',
79
            'FieldType' => 'UserTextField',
80
            'Required' => true,
81
            'Editable' => false
82
        ),
83
        'Email' => array(
84
            'Title' => 'Email',
85
            'FieldType' => 'UserEmailField',
86
            'Required' => true,
87
            'Editable' => false
88
        )
89
    );
90
91
    private static $table_fields = array(
92
        'Title',
93
        'Email'
94
    );
95
96
    private static $db = array(
97
        'Title' => 'Varchar(255)',
98
        'TicketReceiver' => 'Boolean',
99
        'TicketCode' => 'Varchar(255)',
100
        'CheckedIn' => 'Boolean'
101
    );
102
103
    private static $default_sort = 'Created DESC';
104
105
    private static $indexes = array(
106
        'TicketCode' => 'unique("TicketCode")'
107
    );
108
109
    private static $has_one = array(
110
        'Reservation' => 'Broarm\EventTickets\Reservation',
111
        'Ticket' => 'Broarm\EventTickets\Ticket',
112
        'Event' => 'CalendarEvent',
113
        'Member' => 'Member',
114
        'TicketQRCode' => 'Image',
115
        'TicketFile' => 'File'
116
    );
117
118
    private static $many_many = array(
119
        'Fields' => 'Broarm\EventTickets\UserField'
120
    );
121
122
    private static $many_many_extraFields = array(
123
        'Fields' => array(
124
            'Value' => 'Varchar(255)'
125
        )
126
    );
127
128
    private static $summary_fields = array(
129
        'Title' => 'Name',
130
        'Ticket.Title' => 'Ticket',
131
        'TicketCode' => 'Ticket #',
132
        'CheckedIn.Nice' => 'Checked in',
133
    );
134
135
    /**
136
     * Actions usable on the cms detail view
137
     *
138
     * @var array
139
     */
140
    private static $better_buttons_actions = array(
141
        'sendTicket',
142
        'createTicketFile'
143
    );
144
145
    protected static $cachedFields = array();
146
147
    public function getCMSFields()
148
    {
149
        $fields = new FieldList(new TabSet('Root', $mainTab = new Tab('Main')));
150
151
        $fields->addFieldsToTab('Root.Main', array(
152
            ReadonlyField::create('TicketCode', _t('Attendee.Ticket', 'Ticket')),
153
            ReadonlyField::create('MyCheckedIn', _t('Attendee.CheckedIn', 'Checked in'), $this->dbObject('CheckedIn')->Nice())
154
        ));
155
156
        foreach ($this->Fields() as $field) {
157
            $fieldType = $field->getFieldType();
158
            $fields->addFieldToTab(
159
                'Root.Main',
160
                $fieldType::create("{$field->Name}[$field->ID]", $field->Title, $field->getValue())
161
            );
162
        }
163
164
        if ($this->TicketFile()->exists()) {
165
            $fields->addFieldToTab('Root.Main', $reservationFileField = ReadonlyField::create(
166
                'ReservationFile',
167
                _t('Attendee.Reservation', 'Reservation'),
168
                "<a class='readonly' href='{$this->TicketFile()->Link()}' target='_blank'>Download reservation PDF</a>"
169
            ));
170
            $reservationFileField->dontEscape = true;
171
        }
172
173
        $this->extend('updateCMSFields', $fields);
174
        return $fields;
175
    }
176
177
    /**
178
     * Add utility actions to the attendee details view
179
     *
180
     * @return FieldList
181
     */
182
    public function getBetterButtonsActions()
183
    {
184
        /** @var FieldList $fields */
185
        $fields = parent::getBetterButtonsActions();
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class DataObject as the method getBetterButtonsActions() does only exist in the following sub-classes of DataObject: Broarm\EventTickets\Attendee, Broarm\EventTickets\Reservation. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
186
        if ($this->TicketFile()->exists() && !empty($this->getEmail())) {
187
            $fields->push(BetterButtonCustomAction::create('sendTicket', _t('Attendee.SEND', 'Send the ticket')));
188
        }
189
190
        if (!empty($this->getName()) && !empty($this->getEmail())) {
191
            $fields->push(BetterButtonCustomAction::create('createTicketFile', _t('Attendee.CREATE_TICKET', 'Create the ticket')));
192
        }
193
194
        return $fields;
195
    }
196
197
    /**
198
     * Set the title and ticket code before writing
199
     */
200
    public function onBeforeWrite()
201
    {
202
        // Set the title of the attendee
203
        $this->Title = $this->getName();
204
205
        // Generate the ticket code
206
        if ($this->exists() && empty($this->TicketCode)) {
207
            $this->TicketCode = $this->generateTicketCode();
208
        }
209
210
        if (
211
            $this->getEmail()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getEmail() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
212
            && $this->getName()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
213
            && !$this->TicketFile()->exists()
214
            && !$this->TicketQRCode()->exists()
215
        ) {
216
            $this->createQRCode();
217
            $this->createTicketFile();
218
        }
219
220
        if ($fields = $this->Fields()) {
221
            foreach ($fields as $field) {
222
                if ($value = $this->{"$field->Name[$field->ID]"}) {
223
                    //$cache = self::getCacheFactory();
224
                    //$cache->save(serialize($value), $this->getFieldCacheKey($field));
225
                    $fields->add($field->ID, array('Value' => $value));
226
                }
227
            }
228
        }
229
230
        parent::onBeforeWrite();
231
    }
232
233
    public function onAfterWrite()
234
    {
235
        parent::onAfterWrite();
236
        if (($event = $this->Event()) && $event->exists() && !$this->Fields()->exists()) {
237
            $this->Fields()->addMany($event->Fields()->column());
238
        }
239
    }
240
241
    /**
242
     * Delete any stray files before deleting the object
243
     */
244
    public function onBeforeDelete()
245
    {
246
        // If an attendee is deleted from the guest list remove it's qr code
247
        // after deleting the code it's not validatable anymore, simply here for cleanup
248
        if ($this->TicketQRCode()->exists()) {
249
            $this->TicketQRCode()->delete();
250
        }
251
252
        // cleanup the ticket file
253
        if ($this->TicketFile()->exists()) {
254
            $this->TicketFile()->delete();
255
        }
256
257
        if ($this->Fields()->exists()) {
258
            $this->Fields()->removeAll();
259
        }
260
261
        parent::onBeforeDelete();
262
    }
263
264
    /**
265
     * Create the folder for the qr code and ticket file
266
     *
267
     * @return Folder|DataObject|null
268
     */
269
    public function fileFolder()
270
    {
271
        return Folder::find_or_make("/event-tickets/{$this->Event()->URLSegment}/{$this->TicketCode}/");
272
    }
273
274
    /**
275
     * Utility method for fetching the default field, FirstName, value
276
     *
277
     * @return string|null
278
     */
279
    public function getFirstName()
280
    {
281
        return self::getUserField('FirstName');
282
    }
283
284
    /**
285
     * Utility method for fetching the default field, Surname, value
286
     *
287
     * @return string|null
288
     */
289
    public function getSurname()
290
    {
291
        return self::getUserField('Surname');
292
    }
293
294
    /**
295
     * Utility method for fetching the default field, Email, value
296
     *
297
     * @return string|null
298
     */
299
    public function getEmail()
300
    {
301
        return self::getUserField('Email');
302
    }
303
304
    /**
305
     * Get the combined first and last nave for display on the ticket and attendee list
306
     *
307
     * @return string|null
308
     */
309
    public function getName()
310
    {
311
        $mainContact = $this->Reservation()->MainContact();
312
        if ($this->getSurname()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getSurname() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
313
            return trim("{$this->getFirstName()} {$this->getSurname()}");
314
        } elseif ($mainContact->exists() && $mainContact->getSurname()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mainContact->getSurname() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
315
            return _t('Attendee.GUEST_OF', 'Guest of {name}', null, array('name' => $mainContact->getName()));
0 ignored issues
show
array('name' => $mainContact->getName()) is of type array<string,?,{"name":"?"}>, 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...
316
        } else {
317
            return null;
318
        }
319
    }
320
321
    /**
322
     * Get the user field and store it in a static cache
323
     * todo: add a cache that saves the field value on save and retrieves the values here, dumb, so empty fields don't trigger queries
324
     *
325
     * @param $field
326
     * @return mixed|null|string
327
     */
328
    public function getUserField($field)
329
    {
330
        //$cache = self::getCacheFactory();
331
        //return unserialize($cache->load($this->getFieldCacheKey($field)));
332
333
        if (isset(self::$cachedFields[$this->ID][$field])) {
334
            return self::$cachedFields[$this->ID][$field];
335
        } elseif ($userField = $this->Fields()->find('Name', $field)) {
336
            return self::$cachedFields[$this->ID][$field] = (string)$userField->getField('Value');
337
        }
338
339
        return null;
340
    }
341
342
    protected static function getCacheFactory()
343
    {
344
        return \SS_Cache::factory('event_tickets_user_field');
345
    }
346
347
    protected function getFieldCacheKey($field)
348
    {
349
        return md5(serialize(array($this->ID, $field)));
350
    }
351
352
    /**
353
     * Get the table fields for this attendee
354
     *
355
     * @return ArrayList
356
     */
357
    public function getTableFields()
358
    {
359
        $fields = new ArrayList();
360
        foreach (self::config()->get('table_fields') as $field) {
361
            $data = new ViewableData();
362
            $data->Header = _t("Attendee.$field", $field);
363
            $data->Value = $this->{$field};
364
            $fields->add($data);
365
        }
366
        return $fields;
367
    }
368
369
    /**
370
     * Get the unnamespaced singular name for display in the CMS
371
     *
372
     * @return string
373
     */
374
    public function singular_name()
375
    {
376
        $name = explode('\\', parent::singular_name());
377
        return trim(end($name));
378
    }
379
380
    /**
381
     * Generate a unique ticket id
382
     * Serves as the base for the QR code and ticket file
383
     *
384
     * @return string
385
     */
386
    public function generateTicketCode()
387
    {
388
        return uniqid($this->ID);
389
    }
390
391
    /**
392
     * Create a QRCode for the attendee based on the Ticket code
393
     *
394
     * @return Image
395
     */
396
    public function createQRCode()
397
    {
398
        $folder = $this->fileFolder();
399
        $relativeFilePath = "/{$folder->Filename}{$this->TicketCode}.png";
400
        $absoluteFilePath = Director::baseFolder() . $relativeFilePath;
401
402
        if (!$image = Image::get()->find('Filename', $relativeFilePath)) {
403
            // Generate the QR code
404
            $renderer = new BaconQrCode\Renderer\Image\Png();
405
            $renderer->setHeight(256);
406
            $renderer->setWidth(256);
407
            $writer = new BaconQrCode\Writer($renderer);
408
            if (self::config()->get('qr_as_link')) {
409
                $writer->writeFile($this->getCheckInLink(), $absoluteFilePath);
410
            } else {
411
                $writer->writeFile($this->TicketCode, $absoluteFilePath);
412
            }
413
414
            // Store the image in an image object
415
            $image = Image::create();
416
            $image->ParentID = $folder->ID;
417
            $image->OwnerID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
418
            $image->Title = $this->TicketCode;
419
            $image->setFilename($relativeFilePath);
420
            $image->write();
421
422
            // Attach the QR code to the Attendee
423
            $this->TicketQRCodeID = $image->ID;
424
            $this->write();
425
        }
426
427
        return $image;
428
    }
429
430
    /**
431
     * Creates a printable ticket for the attendee
432
     *
433
     * @return File
434
     */
435
    public function createTicketFile()
436
    {
437
        // Find or make a folder
438
        $folder = $this->fileFolder();
439
        $relativeFilePath = "/{$folder->Filename}{$this->TicketCode}.pdf";
440
        $absoluteFilePath = Director::baseFolder() . $relativeFilePath;
441
442
        if (!$this->TicketQRCode()->exists()) {
443
            $this->createQRCode();
444
        }
445
446
        if (!$file = File::get()->find('Filename', $relativeFilePath)) {
447
            $file = File::create();
448
            $file->ParentID = $folder->ID;
449
            $file->OwnerID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
450
            $file->Title = $this->TicketCode;
451
            $file->setFilename($relativeFilePath);
452
            $file->write();
453
        }
454
455
        // Set the template and parse the data
456
        $template = new SSViewer('PrintableTicket');
457
        $html = $template->process($this->data());// getViewableData());
458
459
        // Create a DomPDF instance
460
        $domPDF = new Dompdf();
461
        $domPDF->loadHtml($html);
462
        $domPDF->setPaper('A4');
463
        $domPDF->getOptions()->setDpi(150);
464
        $domPDF->render();
465
466
        // Save the pdf stream as a file
467
        file_put_contents($absoluteFilePath, $domPDF->output());
468
469
        // Attach the ticket file to the Attendee
470
        $this->TicketFileID = $file->ID;
471
        $this->write();
472
473
        return $file;
474
    }
475
476
    /**
477
     * Send the attendee ticket
478
     *
479
     * @return mixed
480
     */
481
    public function sendTicket()
482
    {
483
        // Get the mail sender or fallback to the admin email
484
        if (empty($from = Reservation::config()->get('mail_sender'))) {
485
            $from = Config::inst()->get('Email', 'admin_email');
486
        }
487
488
        $email = new Email();
489
        $email->setSubject(_t(
490
            'AttendeeMail.TITLE',
491
            'Your ticket for {event}',
492
            null,
493
            array(
0 ignored issues
show
array('event' => $this->Event()->Title) is of type array<string,?,{"event":"?"}>, 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...
494
                'event' => $this->Event()->Title
495
            )
496
        ));
497
        $email->setFrom($from);
498
        $email->setTo($this->getEmail());
499
        $email->setTemplate('AttendeeMail');
500
        $email->populateTemplate($this);
501
        $this->extend('updateTicketMail', $email);
502
        return $email->send();
503
    }
504
505
    /**
506
     * Get the checkin link
507
     *
508
     * @return string
509
     */
510
    public function getCheckInLink()
511
    {
512
        return $this->Event()->AbsoluteLink("checkin/ticket/{$this->TicketCode}");
513
    }
514
515
    /**
516
     * Check the attendee out
517
     */
518
    public function checkIn()
519
    {
520
        $this->CheckedIn = true;
521
        $this->write();
522
    }
523
524
    public function canCheckOut()
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
525
    {
526
        return CheckInValidator::config()->get('allow_checkout');
527
    }
528
529
    /**
530
     * Check the attendee in
531
     */
532
    public function checkOut()
533
    {
534
        if ($this->canCheckOut()) {
535
            $this->CheckedIn = false;
536
            $this->write();
537
        }
538
    }
539
540
    public function canView($member = null)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
541
    {
542
        return $this->Reservation()->canView($member);
543
    }
544
545
    public function canEdit($member = null)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
546
    {
547
        return $this->Reservation()->canEdit($member);
548
    }
549
550
    public function canDelete($member = null)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
551
    {
552
        return $this->Reservation()->canDelete($member);
553
    }
554
555
    public function canCreate($member = null)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
556
    {
557
        return $this->Reservation()->canCreate($member);
558
    }
559
}
560