Completed
Push — master ( b27c78...58969d )
by Bram
01:55
created

Attendee::getBetterButtonsActions()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 14
rs 8.8571
cc 5
eloc 7
nc 4
nop 0
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 SSViewer;
28
use Tab;
29
use TabSet;
30
use ViewableData;
31
32
/**
33
 * Class Attendee
34
 *
35
 * @package Broarm\EventTickets
36
 *
37
 * @property string    Title
38
 * @property string    TicketCode
39
 * @property boolean   TicketReceiver
40
 * @property boolean   CheckedIn
41
 * @property FieldList SavableFields    Field to be set in AttendeesField
42
 *
43
 * @property int       TicketID
44
 * @property int       TicketQRCodeID
45
 * @property int       TicketFileID
46
 * @property int       ReservationID
47
 * @property int       EventID
48
 * @property int       MemberID
49
 *
50
 * @method Reservation Reservation()
51
 * @method Ticket Ticket()
52
 * @method Image TicketQRCode()
53
 * @method File TicketFile()
54
 * @method Member Member()
55
 * @method CalendarEvent|TicketExtension Event()
56
 * @method ManyManyList Fields()
57
 */
58
class Attendee extends DataObject
59
{
60
    /**
61
     * Set this to true when you want to have a QR code that opens the check in page and validates the code.
62
     * The validation is only done with proper authorisation so guest cannot check themselves in by mistake.
63
     * By default only the ticket number is translated to an QR code. (for use with USB QR scanners)
64
     *
65
     * @var bool
66
     */
67
    private static $qr_as_link = false;
68
69
    private static $default_fields = array(
70
        'FirstName' => array(
71
            'Title' => 'First name',
72
            'FieldType' => 'UserTextField',
73
            'Required' => true,
74
            'Editable' => false
75
        ),
76
        'Surname' => array(
77
            'Title' => 'Surname',
78
            'FieldType' => 'UserTextField',
79
            'Required' => true,
80
            'Editable' => false
81
        ),
82
        'Email' => array(
83
            'Title' => 'Email',
84
            'FieldType' => 'UserEmailField',
85
            'Required' => true,
86
            'Editable' => false
87
        )
88
    );
89
90
    private static $table_fields = array(
91
        'Title',
92
        'Email'
93
    );
94
95
    private static $db = array(
96
        'Title' => 'Varchar(255)',
97
        'TicketReceiver' => 'Boolean',
98
        'TicketCode' => 'Varchar(255)',
99
        'CheckedIn' => 'Boolean'
100
    );
101
102
    private static $default_sort = 'Created DESC';
103
104
    private static $indexes = array(
105
        'TicketCode' => 'unique("TicketCode")'
106
    );
107
108
    private static $has_one = array(
109
        'Reservation' => 'Broarm\EventTickets\Reservation',
110
        'Ticket' => 'Broarm\EventTickets\Ticket',
111
        'Event' => 'CalendarEvent',
112
        'Member' => 'Member',
113
        'TicketQRCode' => 'Image',
114
        'TicketFile' => 'File'
115
    );
116
117
    private static $many_many = array(
118
        'Fields' => 'Broarm\EventTickets\UserField'
119
    );
120
121
    private static $many_many_extraFields = array(
122
        'Fields' => array(
123
            'Value' => 'Varchar(255)'
124
        )
125
    );
126
127
    private static $summary_fields = array(
128
        'Title' => 'Name',
129
        'Ticket.Title' => 'Ticket',
130
        'TicketCode' => 'Ticket #',
131
        'CheckedIn.Nice' => 'Checked in',
132
    );
133
134
    /**
135
     * Actions usable on the cms detail view
136
     *
137
     * @var array
138
     */
139
    private static $better_buttons_actions = array(
1 ignored issue
show
Unused Code introduced by
The property $better_buttons_actions 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...
140
        'sendTicket',
141
        'createTicketFile'
142
    );
143
144
    public function getCMSFields()
0 ignored issues
show
Documentation introduced by
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...
145
    {
146
        $fields = new FieldList(new TabSet('Root', $mainTab = new Tab('Main')));
147
148
        $fields->addFieldsToTab('Root.Main', array(
149
            ReadonlyField::create('TicketCode', _t('Attendee.Ticket', 'Ticket')),
150
            ReadonlyField::create('MyCheckedIn', _t('Attendee.CheckedIn', 'Checked in'), $this->dbObject('CheckedIn')->Nice())
151
        ));
152
153
        foreach ($this->Fields() as $field) {
154
            $fieldType = $field->getFieldType();
155
            $fields->addFieldToTab(
156
                'Root.Main',
157
                $fieldType::create("{$field->Name}[$field->ID]", $field->Title, $field->getValue())
158
            );
159
        }
160
161
        if ($this->TicketFile()->exists()) {
162
            $fields->addFieldToTab('Root.Main', $reservationFileField = ReadonlyField::create(
163
                'ReservationFile',
164
                _t('Attendee.Reservation', 'Reservation'),
165
                "<a class='readonly' href='{$this->TicketFile()->Link()}' target='_blank'>Download reservation PDF</a>"
166
            ));
167
            $reservationFileField->dontEscape = true;
168
        }
169
170
        $this->extend('updateCMSFields', $fields);
171
        return $fields;
172
    }
173
174
    /**
175
     * Add utility actions to the attendee details view
176
     *
177
     * @return FieldList
178
     */
179
    public function getBetterButtonsActions()
180
    {
181
        /** @var FieldList $fields */
182
        $fields = parent::getBetterButtonsActions();
0 ignored issues
show
Bug introduced by
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...
183
        if ($this->TicketFile()->exists() && !empty($this->getEmail())) {
184
            $fields->push(BetterButtonCustomAction::create('sendTicket', _t('Attendee.SEND', 'Send the ticket')));
185
        }
186
187
        if (!empty($this->getName()) && !empty($this->getEmail())) {
188
            $fields->push(BetterButtonCustomAction::create('createTicketFile', _t('Attendee.CREATE_TICKET', 'Create the ticket')));
189
        }
190
191
        return $fields;
192
    }
193
194
    /**
195
     * Attendee constructor
196
     * If the fields don't exist yet add them
197
     * If they do populate the record with the set data
198
     *
199
     * @param null $record
200
     * @param bool $isSingleton
201
     * @param null $model
202
     */
203
    public function __construct($record = null, $isSingleton = false, $model = null)
204
    {
205
        parent::__construct($record, $isSingleton, $model);
206
        if (($event = $this->Event()) && $event->exists() && !$this->Fields()->exists()) {
207
            $this->Fields()->addMany($event->Fields()->column());
208
        }
209
        /* todo populate the records with the set UserFields
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% 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...
210
        elseif ($this->Fields()->exists()) {
211
            // Populate the record with the set field names
212
            foreach ($this->Fields() as $field) {
213
                if (!isset($this->record[$field->Name])) {
214
                    $this->record[$field->Name] = $field->Value;
215
                }
216
            }
217
        }
218
        */
219
    }
220
221
    /**
222
     * Set the title and ticket code before writing
223
     */
224
    public function onBeforeWrite()
225
    {
226
        // Set the title of the attendee
227
        $this->Title = $this->getName();
228
229
        // Generate the ticket code
230
        if ($this->exists() && empty($this->TicketCode)) {
231
            $this->TicketCode = $this->generateTicketCode();
232
        }
233
234
        if (
235
            !empty($this->getEmail())
236
            && !empty($this->getName())
237
            && !$this->TicketFile()->exists()
238
            && !$this->TicketQRCode()->exists()
239
        ) {
240
            $this->createQRCode();
241
            $this->createTicketFile();
242
        }
243
244
        if ($fields = $this->Fields()) {
245
            foreach ($fields as $field) {
246
                if ($value = $this->{"$field->Name[$field->ID]"}) {
247
                    $this->Fields()->add($field->ID, array('Value' => $value));
248
                }
249
            }
250
        }
251
252
        parent::onBeforeWrite();
253
    }
254
255
    /**
256
     * Delete any stray files before deleting the object
257
     */
258
    public function onBeforeDelete()
259
    {
260
        // If an attendee is deleted from the guest list remove it's qr code
261
        // after deleting the code it's not validatable anymore, simply here for cleanup
262
        if ($this->TicketQRCode()->exists()) {
263
            $this->TicketQRCode()->delete();
264
        }
265
266
        // cleanup the ticket file
267
        if ($this->TicketFile()->exists()) {
268
            $this->TicketFile()->delete();
269
        }
270
271
        parent::onBeforeDelete();
272
    }
273
274
    /**
275
     * Create the folder for the qr code and ticket file
276
     *
277
     * @return Folder|DataObject|null
278
     */
279
    public function fileFolder()
280
    {
281
        return Folder::find_or_make("/event-tickets/{$this->Event()->URLSegment}/{$this->TicketCode}/");
282
    }
283
284
    /**
285
     * todo Extend the get field property to lookup data from the User field set
286
     *
287
     * @param string $field
288
     *
289
     * @return mixed|string
290
     * /
291
    public function getField($field)
292
    {} */
293
294
295
    /**
296
     * Utility method for fetching the default field, FirstName, value
297
     *
298
     * @return string|null
299
     */
300
    public function getFirstName()
301
    {
302
        if ($firstName = $this->Fields()->find('Name', 'FirstName')) {
303
            return (string)$firstName->getField('Value');
304
        }
305
306
        return null;
307
    }
308
309
    /**
310
     * Utility method for fetching the default field, Surname, value
311
     *
312
     * @return string|null
313
     */
314
    public function getSurname()
315
    {
316
        if ($surname = $this->Fields()->find('Name', 'Surname')) {
317
            return (string)$surname->getField('Value');
318
        }
319
320
        return null;
321
    }
322
323
    /**
324
     * Get the combined first and last nave for display on the ticket and attendee list
325
     *
326
     * @return string|null
327
     */
328
    public function getName()
329
    {
330
        $mainContact = $this->Reservation()->MainContact();
331
        if (!empty($this->getSurname())) {
332
            return trim("{$this->getFirstName()} {$this->getSurname()}");
333
        } elseif ($mainContact->exists() && !empty($mainContact->getSurname())) {
334
            return _t('Attendee.GUEST_OF', 'Guest of {name}', null, array('name' => $mainContact->getName()));
0 ignored issues
show
Documentation introduced by
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...
335
        } else {
336
            return null;
337
        }
338
    }
339
340
    /**
341
     * Utility method for fetching the default field, Email, value
342
     *
343
     * @return string|null
344
     */
345
    public function getEmail()
346
    {
347
        if ($email = $this->Fields()->find('Name', 'Email')) {
348
            return (string)$email->getField('Value');
349
        }
350
351
        return null;
352
    }
353
354
    /**
355
     * Get the table fields for this attendee
356
     *
357
     * @return ArrayList
358
     */
359
    public function getTableFields()
360
    {
361
        $fields = new ArrayList();
362
        foreach (self::config()->get('table_fields') as $field) {
363
            $data = new ViewableData();
364
            $data->Header = _t("Attendee.$field", $field);
365
            $data->Value = $this->{$field};
366
            $fields->add($data);
367
        }
368
        return $fields;
369
    }
370
371
    /**
372
     * Get the unnamespaced singular name for display in the CMS
373
     *
374
     * @return string
375
     */
376
    public function singular_name()
377
    {
378
        $name = explode('\\', parent::singular_name());
379
        return trim(end($name));
380
    }
381
382
    /**
383
     * Generate a unique ticket id
384
     * Serves as the base for the QR code and ticket file
385
     *
386
     * @return string
387
     */
388
    public function generateTicketCode()
389
    {
390
        return uniqid($this->ID);
391
    }
392
393
    /**
394
     * Create a QRCode for the attendee based on the Ticket code
395
     *
396
     * @return Image
397
     */
398
    public function createQRCode()
399
    {
400
        $folder = $this->fileFolder();
401
        $relativeFilePath = "/{$folder->Filename}{$this->TicketCode}.png";
402
        $absoluteFilePath = Director::baseFolder() . $relativeFilePath;
403
404
        if (!$image = Image::get()->find('Filename', $relativeFilePath)) {
405
            // Generate the QR code
406
            $renderer = new BaconQrCode\Renderer\Image\Png();
407
            $renderer->setHeight(256);
408
            $renderer->setWidth(256);
409
            $writer = new BaconQrCode\Writer($renderer);
410
            if (self::config()->get('qr_as_link')) {
411
                $writer->writeFile($this->getCheckInLink(), $absoluteFilePath);
412
            } else {
413
                $writer->writeFile($this->TicketCode, $absoluteFilePath);
414
            }
415
416
            // Store the image in an image object
417
            $image = Image::create();
418
            $image->ParentID = $folder->ID;
419
            $image->OwnerID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
420
            $image->Title = $this->TicketCode;
421
            $image->setFilename($relativeFilePath);
422
            $image->write();
423
424
            // Attach the QR code to the Attendee
425
            $this->TicketQRCodeID = $image->ID;
426
            $this->write();
427
        }
428
429
        return $image;
430
    }
431
432
    /**
433
     * Creates a printable ticket for the attendee
434
     *
435
     * @return File
436
     */
437
    public function createTicketFile()
438
    {
439
        // Find or make a folder
440
        $folder = $this->fileFolder();
441
        $relativeFilePath = "/{$folder->Filename}{$this->TicketCode}.pdf";
442
        $absoluteFilePath = Director::baseFolder() . $relativeFilePath;
443
444
        if (!$file = File::get()->find('Filename', $relativeFilePath)) {
445
            $file = File::create();
446
            $file->ParentID = $folder->ID;
447
            $file->OwnerID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
448
            $file->Title = $this->TicketCode;
449
            $file->setFilename($relativeFilePath);
450
            $file->write();
451
        }
452
453
        // Set the template and parse the data
454
        $template = new SSViewer('PrintableTicket');
455
        $html = $template->process($this->data());// getViewableData());
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...
456
457
        // Create a DomPDF instance
458
        $domPDF = new Dompdf();
459
        $domPDF->loadHtml($html);
460
        $domPDF->setPaper('A4');
461
        $domPDF->getOptions()->setDpi(150);
462
        $domPDF->render();
463
464
        // Save the pdf stream as a file
465
        file_put_contents($absoluteFilePath, $domPDF->output());
466
467
        // Attach the ticket file to the Attendee
468
        $this->TicketFileID = $file->ID;
469
        $this->write();
470
471
        return $file;
472
    }
473
474
    /**
475
     * Send the attendee ticket
476
     */
477 View Code Duplication
    public function sendTicket()
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...
478
    {
479
        // Get the mail sender or fallback to the admin email
480
        if (empty($from = Reservation::config()->get('mail_sender'))) {
481
            $from = Config::inst()->get('Email', 'admin_email');
482
        }
483
484
        $email = new Email();
485
        $email->setSubject(_t(
486
            'AttendeeMail.TITLE',
487
            'Your ticket for {event}',
488
            null,
489
            array(
0 ignored issues
show
Documentation introduced by
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...
490
                'event' => $this->Event()->Title
491
            )
492
        ));
493
        $email->setFrom($from);
494
        $email->setTo($this->getEmail());
495
        $email->setTemplate('AttendeeMail');
496
        $email->populateTemplate($this);
497
        $this->extend('updateTicketMail', $email);
498
        $email->send();
499
    }
500
501
    /**
502
     * Get the checkin link
503
     *
504
     * @return string
505
     */
506
    public function getCheckInLink()
507
    {
508
        return $this->Event()->AbsoluteLink("checkin/{$this->TicketCode}");
509
    }
510
511
    /**
512
     * Check the attendee out
513
     */
514
    public function checkIn()
515
    {
516
        $this->CheckedIn = true;
517
        $this->write();
518
    }
519
520
    public function canCheckOut()
0 ignored issues
show
Documentation introduced by
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...
521
    {
522
        return CheckInValidator::config()->get('allow_checkout');
523
    }
524
525
    /**
526
     * Check the attendee in
527
     */
528
    public function checkOut()
529
    {
530
        if ($this->canCheckOut()) {
531
            $this->CheckedIn = false;
532
            $this->write();
533
        }
534
    }
535
536
    public function canView($member = null)
0 ignored issues
show
Documentation introduced by
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...
537
    {
538
        return $this->Reservation()->canView($member);
539
    }
540
541
    public function canEdit($member = null)
0 ignored issues
show
Documentation introduced by
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...
542
    {
543
        return $this->Reservation()->canEdit($member);
544
    }
545
546
    public function canDelete($member = null)
0 ignored issues
show
Documentation introduced by
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...
547
    {
548
        return $this->Reservation()->canDelete($member);
549
    }
550
551
    public function canCreate($member = null)
0 ignored issues
show
Documentation introduced by
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...
552
    {
553
        return $this->Reservation()->canCreate($member);
554
    }
555
}
556