Passed
Push — master ( 99ec44...efff6a )
by Thomas
02:16
created

BetterEmail::setFrom()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace LeKoala\EmailTemplates\Email;
4
5
use Exception;
6
use Swift_MimePart;
7
use BadMethodCallException;
8
use SilverStripe\i18n\i18n;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\View\SSViewer;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Security\Security;
15
use SilverStripe\View\Requirements;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Control\Email\Email;
19
use SilverStripe\SiteConfig\SiteConfig;
20
use LeKoala\EmailTemplates\Models\SentEmail;
21
use LeKoala\EmailTemplates\Helpers\EmailUtils;
22
use LeKoala\EmailTemplates\Models\EmailTemplate;
23
use LeKoala\EmailTemplates\Helpers\SubsiteHelper;
24
25
/**
26
 * An improved and more pleasant base Email class to use on your project
27
 *
28
 * This class is fully decoupled from the EmailTemplate class and keep be used
29
 * independantly
30
 *
31
 * Improvements are:
32
 *
33
 * - URL safe rewriting
34
 * - Configurable base template (base system use Email class with setHTMLTemplate to provide content)
35
 * - Send email according to member locale
36
 * - Check for subject
37
 * - Send to member or admin
38
 * - Persist emails
39
 * - Parse body (multi part body is supported)
40
 * - Plaintext takes template into account
41
 * - Disable emails
42
 * - Unified send methods that support hooks
43
 *
44
 * @author lekoala
45
 */
46
class BetterEmail extends Email
47
{
48
    const STATE_CANCELLED = 'cancelled';
49
    const STATE_NOT_SENT = 'not_sent';
50
    const STATE_SENT = 'sent';
51
    const STATE_FAILED = 'failed';
52
53
    /**
54
     * @var EmailTemplate
55
     */
56
    protected $emailTemplate;
57
58
    /**
59
     * @var string
60
     */
61
    protected $locale;
62
63
    /**
64
     * @var Member
65
     */
66
    protected $to_member;
67
68
    /**
69
     * @var Member
70
     */
71
    protected $from_member;
72
73
    /**
74
     * @var boolean
75
     */
76
    protected $disabled = false;
77
78
    /**
79
     * @var SentMail
0 ignored issues
show
Bug introduced by
The type LeKoala\EmailTemplates\Email\SentMail was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
80
     */
81
    protected $sentMail = null;
82
83
    /**
84
     * @var boolean
85
     */
86
    protected $sendingCancelled = false;
87
88
    /**
89
     * Email constructor.
90
     * @param string|array|null $from
91
     * @param string|array|null $to
92
     * @param string|null $subject
93
     * @param string|null $body
94
     * @param string|array|null $cc
95
     * @param string|array|null $bcc
96
     * @param string|null $returnPath
97
     */
98
    public function __construct(
99
        $from = null,
100
        $to = null,
101
        $subject = null,
102
        $body = null,
103
        $cc = null,
104
        $bcc = null,
105
        $returnPath = null
106
    ) {
107
        parent::__construct($from, $to, $subject, $body, $cc, $bcc, $returnPath);
108
109
        // Use template as a layout
110
        if ($defaultTemplate = self::config()->template) {
111
            // Call method because variable is private
112
            parent::setHTMLTemplate($defaultTemplate);
113
        }
114
    }
115
116
    /**
117
     * Persists the email to the database
118
     *
119
     * @param bool|array $results
120
     * @return SentEmail
121
     */
122
    protected function persist($results)
123
    {
124
        $record = SentEmail::create(array(
125
            'To' => EmailUtils::format_email_addresses($this->getTo()),
126
            'From' => EmailUtils::format_email_addresses($this->getFrom()),
127
            'ReplyTo' => $this->getReplyTo(),
128
            'Subject' => $this->getSubject(),
129
            'Body' => $this->getRenderedBody(),
130
            'Headers' => $this->getSwiftMessage()->getHeaders()->toString(),
131
            'CC' => EmailUtils::format_email_addresses($this->getCC()),
132
            'BCC' => EmailUtils::format_email_addresses($this->getBCC()),
133
            'Results' => json_encode($results),
134
        ));
135
        $record->write();
136
137
        // TODO: migrate this to a cron task
138
        SentEmail::cleanup();
139
140
        return $record;
141
    }
142
143
144
    /**
145
     * Get body of message after rendering
146
     * Useful for previews
147
     *
148
     * @return string
149
     */
150
    public function getRenderedBody()
151
    {
152
        $this->render();
153
        return $this->getSwiftMessage()->getBody();
154
    }
155
156
    /**
157
     * Don't forget that setBody will erase content of html template
158
     * Prefer to use this instead. Basically you can replace setBody calls with this method
159
     * URLs are rewritten by render process
160
     *
161
     * @param string $body
162
     * @return $this
163
     */
164
    public function addBody($body)
165
    {
166
        return $this->addData("EmailContent", $body);
167
    }
168
169
    /**
170
     * @param array|ViewableData $data The template data to set
0 ignored issues
show
Bug introduced by
The type LeKoala\EmailTemplates\Email\ViewableData was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
171
     * @return $this
172
     */
173
    public function setData($data)
174
    {
175
        // Merge data!
176
        if ($this->emailTemplate) {
177
            if (is_array($data)) {
178
                parent::addData($data);
179
            } elseif ($data instanceof DataObject) {
180
                parent::addData($data->toMap());
181
            } else {
182
                parent::setData($data);
183
            }
184
        } else {
185
            parent::setData($data);
186
        }
187
        return $this;
188
    }
189
190
    /**
191
     * Sends a HTML email
192
     *
193
     * @return bool true if successful or array of failed recipients
194
     */
195
    public function send()
196
    {
197
        return $this->doSend(false);
198
    }
199
200
    /**
201
     * Sends a plain text email
202
     *
203
     * @return bool true if successful or array of failed recipients
204
     */
205
    public function sendPlain()
206
    {
207
        return $this->doSend(true);
208
    }
209
210
    /**
211
     * Send this email
212
     *
213
     * @param bool $plain
214
     * @return bool true if successful or array of failed recipients
215
     * @throws Exception
216
     */
217
    public function doSend($plain = false)
218
    {
219
        if ($this->disabled) {
220
            $this->sendingCancelled = true;
221
            return false;
222
        }
223
224
        // Check for Subject
225
        if (!$this->getSubject()) {
226
            throw new BadMethodCallException('You must set a subject');
227
        }
228
229
        // This hook can prevent email from being sent
230
        $result = $this->extend('onBeforeDoSend', $this);
231
        if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
232
            $this->sendingCancelled = true;
233
            return false;
234
        }
235
236
        $SiteConfig = SiteConfig::current_site_config();
237
238
        // Check for Sender and use default if necessary
239
        $from = $this->getFrom();
240
        if (empty($from)) {
241
            $this->setFrom($SiteConfig->EmailDefaultSender());
242
        }
243
244
        // Check for Recipient and use default if necessary
245
        $to = $this->getTo();
246
        if (empty($to)) {
247
            $this->addTo($SiteConfig->EmailDefaultRecipient());
248
        }
249
250
        // Set language to use for the email
251
        $restore_locale = null;
252
        if ($this->locale) {
253
            $restore_locale = i18n::get_locale();
254
            i18n::set_locale($this->locale);
255
        }
256
257
        $member = $this->to_member;
258
        if ($member) {
259
            // Maybe this member doesn't want to receive emails?
260
            if ($member->hasMethod('canReceiveEmails') && !$member->canReceiveEmails()) {
261
                return false;
262
            }
263
        }
264
265
        // Make sure we have a full render with current locale
266
        if ($this->emailTemplate) {
267
            $this->clearBody();
268
        }
269
270
        if ($plain) {
271
            // sendPlain will trigger our updated generatePlainPartFromBody
272
            $res = parent::sendPlain();
273
        } else {
274
            $res = parent::send();
275
        }
276
277
        if ($restore_locale) {
278
            i18n::set_locale($restore_locale);
279
        }
280
281
        $this->extend('onAfterDoSend', $this, $res);
282
        $this->sentMail = $this->persist($res);
283
284
        return $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res also could return the type array which is incompatible with the documented return type boolean.
Loading history...
285
    }
286
287
    /**
288
     * Returns one of the STATE_xxxx constant
289
     *
290
     * @return string
291
     */
292
    public function getSendStatus()
293
    {
294
        if ($this->sendingCancelled) {
295
            return self::STATE_CANCELLED;
296
        }
297
        if ($this->sentMail) {
298
            if ($this->sentMail->IsSuccess()) {
299
                return self::STATE_SENT;
300
            }
301
            return self::STATE_FAILED;
302
        }
303
        return self::STATE_NOT_SENT;
304
    }
305
306
    /**
307
     * Was sending cancelled ?
308
     *
309
     * @return bool
310
     */
311
    public function getSendingCancelled()
312
    {
313
        return $this->sendingCancelled;
314
    }
315
316
    /**
317
     * The last result from "send" method. Null if not sent yet or sending was cancelled
318
     *
319
     * @return SentMail
320
     */
321
    public function getSentMail()
322
    {
323
        return $this->sentMail;
324
    }
325
326
    /**
327
     * Automatically adds a plain part to the email generated from the current Body
328
     *
329
     * @return $this
330
     */
331
    public function generatePlainPartFromBody()
332
    {
333
        $plainPart = $this->findPlainPart();
334
        if ($plainPart) {
335
            $this->getSwiftMessage()->detach($plainPart);
336
        }
337
        unset($plainPart);
338
339
        $this->getSwiftMessage()->addPart(
340
            EmailUtils::convert_html_to_text($this->getBody()),
341
            'text/plain',
342
            'utf-8'
343
        );
344
345
        return $this;
346
    }
347
348
    /**
349
     * @return $this
350
     */
351
    public function clearBody()
352
    {
353
        $this->getSwiftMessage()->setBody(null);
354
        return $this;
355
    }
356
357
    /**
358
     * Set the template to render the email with
359
     *
360
     * This method is overidden in order to look for email templates to provide
361
     * content to
362
     *
363
     * @param string $template
364
     * @return $this
365
     */
366
    public function setHTMLTemplate($template)
367
    {
368
        if (substr($template, -3) == '.ss') {
369
            $template = substr($template, 0, -3);
370
        }
371
372
        // Do we have a custom template matching this code?
373
        $code = self::makeTemplateCode($template);
374
        $emailTemplate = EmailTemplate::getByCode($code, false);
375
        if ($emailTemplate) {
0 ignored issues
show
introduced by
$emailTemplate is of type LeKoala\EmailTemplates\Models\EmailTemplate, thus it always evaluated to true.
Loading history...
376
            $emailTemplate->applyTemplate($this);
377
            return $this;
378
        }
379
380
        // If not, keep default behaviour (call method because var is private)
381
        return parent::setHTMLTemplate($template);
382
    }
383
384
    /**
385
     * Make a template code
386
     *
387
     * @param string $str
388
     * @return string
389
     */
390
    public static function makeTemplateCode($str)
391
    {
392
        // If we get a class name
393
        $parts = explode('\\', $str);
394
        $str = end($parts);
395
        $code = preg_replace('/Email$/', '', $str);
396
        return $code;
397
    }
398
399
    /**
400
     * Helper method to render string with data
401
     *
402
     * @param string $content
403
     * @return string
404
     */
405
    public function renderWithData($content)
406
    {
407
        $viewer = SSViewer::fromString($content);
408
        $data = $this->getData();
409
        // SSViewer_DataPresenter requires array
410
        if (is_object($data)) {
411
            if (method_exists($data, 'toArray')) {
412
                $data = $data->toArray();
413
            } else {
414
                $data = (array) $data;
415
            }
416
        }
417
        $result = (string) $viewer->process($this, $data);
418
        $result = self::rewriteURLs($result);
419
        return $result;
420
    }
421
422
    /**
423
     * Render the email
424
     * @param bool $plainOnly Only render the message as plain text
425
     * @return $this
426
     */
427
    public function render($plainOnly = false)
428
    {
429
        if ($existingPlainPart = $this->findPlainPart()) {
430
            $this->getSwiftMessage()->detach($existingPlainPart);
431
        }
432
        unset($existingPlainPart);
433
434
        // Respect explicitly set body
435
        $htmlPart = $plainPart = null;
436
437
        // Only respect if we don't have an email template
438
        if ($this->emailTemplate) {
439
            $htmlPart = $plainOnly ? null : $this->getBody();
440
            $plainPart = $plainOnly ? $this->getBody() : null;
441
        }
442
443
        // Ensure we can at least render something
444
        $htmlTemplate = $this->getHTMLTemplate();
445
        $plainTemplate = $this->getPlainTemplate();
446
        if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) {
447
            return $this;
448
        }
449
450
        // Do not interfere with emails styles
451
        Requirements::clear();
452
453
        // Render plain part
454
        if ($plainTemplate && !$plainPart) {
455
            $plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain();
0 ignored issues
show
Bug introduced by
It seems like $this->getData() can also be of type SilverStripe\View\ViewableData; however, parameter $customFields of SilverStripe\View\ViewableData::renderWith() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

455
            $plainPart = $this->renderWith($plainTemplate, /** @scrutinizer ignore-type */ $this->getData())->Plain();
Loading history...
456
            // Do another round of rendering to render our variables inside
457
            $plainPart = $this->renderWithData($plainPart);
458
        }
459
460
        // Render HTML part, either if sending html email, or a plain part is lacking
461
        if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) {
462
            $htmlPart = $this->renderWith($htmlTemplate, $this->getData());
463
            // Do another round of rendering to render our variables inside
464
            $htmlPart = $this->renderWithData($htmlPart);
465
        }
466
467
        // Render subject with data as well
468
        $subject = $this->renderWithData($this->getSubject());
469
        // Html entities in email titles is not a good idea
470
        $subject = html_entity_decode($subject, ENT_QUOTES | ENT_XML1, 'UTF-8');
471
        parent::setSubject($subject);
472
473
        // Plain part fails over to generated from html
474
        if (!$plainPart && $htmlPart) {
475
            $plainPart = EmailUtils::convert_html_to_text($htmlPart);
476
        }
477
478
        // Rendering is finished
479
        Requirements::restore();
480
481
        // Fail if no email to send
482
        if (!$plainPart && !$htmlPart) {
483
            return $this;
484
        }
485
486
        // Build HTML / Plain components
487
        if ($htmlPart && !$plainOnly) {
488
            $this->setBody($htmlPart);
489
            $this->getSwiftMessage()->setContentType('text/html');
490
            $this->getSwiftMessage()->setCharset('utf-8');
491
            if ($plainPart) {
492
                $this->getSwiftMessage()->addPart($plainPart, 'text/plain', 'utf-8');
493
            }
494
        } else {
495
            if ($plainPart) {
496
                $this->setBody($plainPart);
497
            }
498
            $this->getSwiftMessage()->setContentType('text/plain');
499
            $this->getSwiftMessage()->setCharset('utf-8');
500
        }
501
502
        return $this;
503
    }
504
505
    /**
506
     * Get locale set before email is sent
507
     *
508
     * @return string
509
     */
510
    public function getLocale()
511
    {
512
        return $this->locale;
513
    }
514
515
    /**
516
     *  Set locale to set before email is sent
517
     *
518
     * @param string $val
519
     */
520
    public function setLocale($val)
521
    {
522
        $this->locale = $val;
523
    }
524
525
    /**
526
     * Is this email disabled ?
527
     *
528
     * @return boolean
529
     */
530
    public function getDisabled()
531
    {
532
        return $this->disabled;
533
    }
534
535
    /**
536
     * Disable this email (sending will have no effect)
537
     *
538
     * @param bool $disabled
539
     * @return $this
540
     */
541
    public function setDisabled($disabled)
542
    {
543
        $this->disabled = (bool) $disabled;
544
        return $this;
545
    }
546
547
    /**
548
     * Get recipient as member
549
     *
550
     * @return Member
551
     */
552
    public function getToMember()
553
    {
554
        if (!$this->to_member && $this->to) {
0 ignored issues
show
Bug Best Practice introduced by
The property to does not exist on LeKoala\EmailTemplates\Email\BetterEmail. Since you implemented __get, consider adding a @property annotation.
Loading history...
555
            $email = EmailUtils::get_email_from_rfc_email($this->to);
556
            $member = Member::get()->filter(array('Email' => $email))->first();
557
            if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
558
                $this->setToMember($member);
559
            }
560
        }
561
        return $this->to_member;
562
    }
563
564
    /**
565
     * Set recipient(s) of the email
566
     *
567
     * To send to many, pass an array:
568
     * array('[email protected]' => 'My Name', '[email protected]');
569
     *
570
     * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
571
     * @param string|null $name The name of the recipient (if one)
572
     * @return $this
573
     */
574
    public function setTo($address, $name = null)
575
    {
576
        // Make sure this doesn't conflict with to_member property
577
        if ($this->to_member) {
578
            $this->to_member = null;
579
        }
580
        return parent::setTo($address, $name);
581
    }
582
583
    /**
584
     * @param string $subject The Subject line for the email
585
     * @return $this
586
     */
587
    public function setSubject($subject)
588
    {
589
        // Do not allow changing subject if a template is set
590
        if ($this->emailTemplate && $this->getSubject()) {
591
            return $this;
592
        }
593
        return parent::setSubject($subject);
594
    }
595
596
    /**
597
     * Send to admin
598
     *
599
     * @return Email
600
     */
601
    public function setToAdmin()
602
    {
603
        return $this->setToMember(Security::findAnAdministrator());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Se...::findAnAdministrator() has been deprecated: 4.0.0:5.0.0 Please use DefaultAdminService::findOrCreateDefaultAdmin() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

603
        return $this->setToMember(/** @scrutinizer ignore-deprecated */ Security::findAnAdministrator());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
604
    }
605
606
    /**
607
     * Set a member as a recipient.
608
     *
609
     * It will also set the $Recipient variable in the template
610
     *
611
     * @param Member $member
612
     * @param string $locale Locale to use, set to false to keep current locale
613
     * @return BetterEmail
614
     */
615
    public function setToMember(Member $member, $locale = null)
616
    {
617
        if ($locale === null) {
618
            $this->locale = $member->Locale;
619
        } else {
620
            $this->locale = $locale;
621
        }
622
        $this->to_member = $member;
623
624
        $this->addData(array('Recipient' => $member));
625
626
        return parent::setTo($member->Email, $member->getTitle());
627
    }
628
629
    /**
630
     * Get sender as member
631
     *
632
     * @return Member
633
     */
634
    public function getFromMember()
635
    {
636
        if (!$this->from_member && $this->from) {
0 ignored issues
show
Bug Best Practice introduced by
The property from does not exist on LeKoala\EmailTemplates\Email\BetterEmail. Since you implemented __get, consider adding a @property annotation.
Loading history...
637
            $email = EmailUtils::get_email_from_rfc_email($this->from);
638
            $member = Member::get()->filter(array('Email' => $email))->first();
639
            if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
640
                $this->setFromMember($member);
641
            }
642
        }
643
        return $this->from_member;
644
    }
645
646
    /**
647
     * Set From Member
648
     *
649
     * It will also set the $Sender variable in the template
650
     *
651
     * @param Member $member
652
     * @return BetterEmail
653
     */
654
    public function setFromMember(Member $member)
655
    {
656
        $this->from_member = $member;
657
658
        $this->addData(array('Sender' => $member));
659
660
        return $this->setFrom($member->Email, $member->getTitle());
661
    }
662
663
    /**
664
     * Improved set from that supports Name <[email protected]> notation
665
     *
666
     * @param string|array $address
667
     * @param string|null $name
668
     * @return $this
669
     */
670
    public function setFrom($address, $name = null)
671
    {
672
        if ($name === null && is_string($address)) {
673
            $name = EmailUtils::get_displayname_from_rfc_email($address);
674
            $address = EmailUtils::get_email_from_rfc_email($address);
675
        }
676
        return parent::setFrom($address, $name);
677
    }
678
679
    /**
680
     * Bug safe absolute url that support subsites
681
     *
682
     * @param string $url
683
     * @param bool $relativeToSiteBase
684
     * @return string
685
     */
686
    protected static function safeAbsoluteURL($url, $relativeToSiteBase = false)
687
    {
688
        if (empty($url)) {
689
            return Director::baseURL();
690
        }
691
        $absUrl = Director::absoluteURL($url, $relativeToSiteBase);
692
693
        // If we use subsite, absolute url may not use the proper url
694
        if (SubsiteHelper::usesSubsite()) {
695
            $subsite = SubsiteHelper::currentSubsite();
696
            if ($subsite->hasMethod('getPrimarySubsiteDomain')) {
697
                $domain = $subsite->getPrimarySubsiteDomain();
698
                $link = $subsite->domain();
699
                $protocol = $domain->getFullProtocol();
700
            } else {
701
                $protocol = Director::protocol();
702
                $link = $subsite->domain();
703
            }
704
            $absUrl = preg_replace('/\/\/[^\/]+\//', '//' . $link . '/', $absUrl);
705
            $absUrl = preg_replace('/http(s)?:\/\//', $protocol, $absUrl);
706
        }
707
708
        return $absUrl;
709
    }
710
711
    /**
712
     * Turn all relative URLs in the content to absolute URLs
713
     */
714
    protected static function rewriteURLs($html)
715
    {
716
        if (isset($_SERVER['REQUEST_URI'])) {
717
            $html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
718
        }
719
        return HTTP::urlRewriter($html, function ($url) {
720
            //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
721
            if (preg_match('/^\w+:/', $url)) {
722
                return $url;
723
            }
724
            return self::safeAbsoluteURL($url, true);
725
        });
726
    }
727
728
    /**
729
     * Get the value of emailTemplate
730
     * @return EmailTemplate
731
     */
732
    public function getEmailTemplate()
733
    {
734
        return $this->emailTemplate;
735
    }
736
737
    /**
738
     * Set the value of emailTemplate
739
     *
740
     * @param EmailTemplate $emailTemplate
741
     * @return $this
742
     */
743
    public function setEmailTemplate(EmailTemplate $emailTemplate)
744
    {
745
        $this->emailTemplate = $emailTemplate;
746
        return $this;
747
    }
748
}
749