Attendee   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 25

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 25
dl 0
loc 501
rs 3.44
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getCMSFields() 0 29 3
A getBetterButtonsActions() 0 14 5
B onBeforeWrite() 0 32 10
A onAfterWrite() 0 7 4
A onBeforeDelete() 0 19 4
A fileFolder() 0 4 1
A getFirstName() 0 4 1
A getSurname() 0 4 1
A getEmail() 0 4 1
A getName() 0 11 4
A getUserField() 0 13 3
A getCacheFactory() 0 4 1
A getFieldCacheKey() 0 4 1
A getTableFields() 0 11 2
A singular_name() 0 5 1
A generateTicketCode() 0 4 1
A createQRCode() 0 33 4
A createTicketFile() 0 40 4
A getCheckInLink() 0 4 1
A checkIn() 0 5 1
A canCheckOut() 0 4 1
A checkOut() 0 7 2
A canView() 0 4 1
A canEdit() 0 4 1
A canDelete() 0 4 1
A canCreate() 0 4 1
A sendTicket() 0 23 2

How to fix   Complexity   

Complex Class

Complex classes like Attendee often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Attendee, and based on these observations, apply Extract Interface, too.

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
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...
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
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...
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
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...
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
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...
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
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...
541
    {
542
        return $this->Reservation()->canView($member);
543
    }
544
545
    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...
546
    {
547
        return $this->Reservation()->canEdit($member);
548
    }
549
550
    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...
551
    {
552
        return $this->Reservation()->canDelete($member);
553
    }
554
555
    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...
556
    {
557
        return $this->Reservation()->canCreate($member);
558
    }
559
}
560