Passed
Branch master (410c7b)
by Michael
03:30
created

Id3v1::getGenreList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace XoopsModules\Suico;
6
7
/*
8
    Id3v1 - Class for manipulating Id3v1 tags
9
    Copyright (C) 2007  Karol Babioch
10
11
    This program is free software; you can
12
    redistribute it  and/or modify it under the terms
13
    of the GNU General Public License as published by
14
    the Free Software Foundation; either version 2 of
15
    the License, or (at your option) any later version.
16
17
    This program is distributed in the hope that it
18
    will be useful, but WITHOUT ANY WARRANTY; without
19
    even the implied warranty of MERCHANTABILITY or
20
    ITNESS FOR A PARTICULAR PURPOSE. See the GNU
21
    General Public License for more details.
22
23
    You should have received a copy of the GNU
24
    General Public License along with this program;
25
    if not, write to the Free Software Foundation,
26
    Inc., 51 Franklin St, Fifth Floor, Boston,
27
    MA 02110, USA
28
*/
29
30
use RuntimeException;
31
32
/**
33
 * @author      Karol Babioch <[email protected]>
34
 * @copyright   Karol Babioch
35
 * @license     http://www.gnu.org/licenses/gpl-3.0.html GNU GPL
36
 * @package     Id3v1
37
 */
38
39
/**
40
 * This class offers you the posibility to manipulate Id3v1 tags
41
 * in a modern, object oriented, way.
42
 *
43
 * This class offers you to read and write Id3v1 in the version
44
 * 1.0 and 1.1. It implements a so called "fluent interface", so
45
 * the access is easy and effective.
46
 *
47
 * @version      1.0
48
 * @link         http://www.babioch.de/
49
 * @package      Id3v1
50
 * @author       Karol Babioch <[email protected]>
51
 * @copyright    Karol Babioch
52
 * @license      http://www.gnu.org/licenses/gpl-3.0.html GNU GPL
53
 */
54
class Id3v1
55
{
56
    /**
57
     * Represents Id3v1.0
58
     */
59
60
    public const ID3V1_0 = 'ID3V1_0';
61
62
    /**
63
     * Represents Id3v1.1
64
     */
65
66
    public const ID3V1_1 = 'ID3V1_1';
67
68
    /**
69
     * Holds the tags
70
     *
71
     * @see __construct
72
     * @see getTitle()
73
     * @see getArtist()
74
     * @see getAlbum()
75
     * @see getYear()
76
     * @see getComment()
77
     * @see getTrack()
78
     * @see getGenre()
79
     * @see getGenreId()
80
     * @see setTitle()
81
     * @see setArtist()
82
     * @see setAlbum()
83
     * @see setYear()
84
     * @see setComment()
85
     * @see setTrack()
86
     * @see setGenre()
87
     * @see setGenreId()
88
     */
89
90
    protected $_tags = [];
91
92
    /**
93
     * Holds the PHP stream
94
     *
95
     * @see __construct()
96
     */
97
98
    protected $_stream = null;
99
100
    /**
101
     * Holds the Id3v1 version
102
     *
103
     * @see self::ID3V1_0
104
     * @see self::ID3V1_1
105
     * @see setId3v1Version()
106
     * @see getId3v1Version()
107
     */
108
109
    protected $_version = null;
110
111
    /**
112
     * Indicates if the source is read-only
113
     *
114
     * @see __construct()
115
     */
116
117
    protected $_readOnly = false;
118
119
    /**
120
     * Holds all known ID3 Genres
121
     *
122
     * @link http://id3.org/d3v2.3.0
123
     * @see  getGenreList()
124
     * @see  getGenreNameByid()
125
     * @see  getGenreIdByName()
126
     */
127
128
    protected static $_genres = [
129
        'Blues',
130
        'Classic Rock',
131
        'Country',
132
        'Dance',
133
        'Disco',
134
        'Funk',
135
        'Grunge',
136
        'Hip-Hop',
137
        'Jazz',
138
        'Metal',
139
        'New Age',
140
        'Oldies',
141
        'Other',
142
        'Pop',
143
        'R&B',
144
        'Rap',
145
        'Reggae',
146
        'Rock',
147
        'Techno',
148
        'Industrial',
149
        'Alternative',
150
        'Ska',
151
        'Death Metal',
152
        'Pranks',
153
        'Soundtrack',
154
        'Euro-Techno',
155
        'Ambient',
156
        'Trip-Hop',
157
        'Vocal',
158
        'Jazz+Funk',
159
        'Fusion',
160
        'Trance',
161
        'Classical',
162
        'Instrumental',
163
        'Acid',
164
        'House',
165
        'Game',
166
        'Sound Clip',
167
        'Gospel',
168
        'Noise',
169
        'Alternative Rock',
170
        'Bass',
171
        'Soul',
172
        'Punk',
173
        'Space',
174
        'Meditative',
175
        'Instrumental Pop',
176
        'Instrumental Rock',
177
        'Ethnic',
178
        'Gothic',
179
        'Darkwave',
180
        'Techno-Industrial',
181
        'Electronic',
182
        'Pop-Folk',
183
        'Eurodance',
184
        'Dream',
185
        'Southern Rock',
186
        'Comedy',
187
        'Cult',
188
        'Gangsta',
189
        'Top 40',
190
        'Christian Rap',
191
        'Pop/Funk',
192
        'Jungle',
193
        'Native US',
194
        'Cabaret',
195
        'New Wave',
196
        'Psychedelic',
197
        'Rave',
198
        'Showtunes',
199
        'Trailer',
200
        'Lo-Fi',
201
        'Tribal',
202
        'Acid Punk',
203
        'Acid Jazz',
204
        'Polka',
205
        'Retro',
206
        'Musical',
207
        'Rock & Roll',
208
        'Hard Rock',
209
        'Folk',
210
        'Folk-Rock',
211
        'National Folk',
212
        'Swing',
213
        'Fast Fusion',
214
        'Bebob',
215
        'Latin',
216
        'Revival',
217
        'Celtic',
218
        'Bluegrass',
219
        'Avantgarde',
220
        'Gothic Rock',
221
        'Progressive Rock',
222
        'Psychedelic Rock',
223
        'Symphonic Rock',
224
        'Slow Rock',
225
        'Big Band',
226
        'Chorus',
227
        'Easy Listening',
228
        'Acoustic',
229
        'Humour',
230
        'Speech',
231
        'Chanson',
232
        'Opera',
233
        'Chamber Music',
234
        'Sonata',
235
        'Symphony',
236
        'Booty Bass',
237
        'Primus',
238
        'Porn Groove',
239
        'Satire',
240
        'Slow Jam',
241
        'Club',
242
        'Tango',
243
        'Samba',
244
        'Folklore',
245
        'Ballad',
246
        'Power Ballad',
247
        'Rhytmic Soul',
248
        'Freestyle',
249
        'Duet',
250
        'Punk Rock',
251
        'Drum Solo',
252
        'Acapella',
253
        'Euro-House',
254
        'Dance Hall',
255
    ];
256
257
    /**
258
     * Class constructor
259
     *
260
     * Checks if the parameters are valid and then gets ID3 tags, if there
261
     * are some.
262
     *
263
     * @param      $filename
264
     * @param bool $readOnly
265
     * @see $_tags
266
     */
267
268
    public function __construct(
269
        $filename,
270
        $readOnly = false
271
    ) {
272
        if (\is_bool($readOnly)) {
0 ignored issues
show
introduced by
The condition is_bool($readOnly) is always true.
Loading history...
273
            $this->_readOnly = $readOnly;
274
        }
275
276
        if (!\is_string($filename)) {
277
            throw new RuntimeException('Filename must be a string');
278
        }
279
280
        if (!\is_file($filename)) {
281
            throw new RuntimeException('File doesn\'t exist');
282
        }
283
284
        $mode = $this->_readOnly ? 'rb' : 'rb+';
285
286
        if (!$this->_stream = @\fopen($filename, $mode, false)) {
287
            throw new RuntimeException('File cannot be opened');
288
        }
289
290
        if (!$this->_readOnly) {
291
            \flock($this->_stream, \LOCK_SH);
292
        }
293
294
        \fseek($this->_stream, -128, \SEEK_END);
295
296
        $rawTag = \fread($this->_stream, 128);
297
298
        if ($rawTag[125] === \chr(0) && $rawTag[126] !== \chr(0)) {
299
            $format = 'a3marking/a30title/a30artist/a30album/a4year' . '/a28comment/x1/C1track/C1genre';
300
301
            $this->_version = self::ID3V1_1;
302
        } else {
303
            $format = 'a3marking/a30title/a30artist/a30album/a4year' . '/a30comment/C1genre';
304
305
            $this->_version = self::ID3V1_0;
306
        }
307
308
        $tags = \unpack($format, $rawTag);
309
310
        $this->clearAllTags();
311
312
        if ('TAG' === $tags['marking']) {
313
            $this->_tags = $tags;
314
        }
315
    }
316
317
    /**
318
     * Short way to access tags
319
     *
320
     * This method allows a shorter way to access tags.
321
     * You can access the tags like normal properties,
322
     * and don't have to call methods.
323
     *
324
     * Here an example:
325
     * <code>
326
     * <?php
327
     *     // two ways to do the same
328
     *     $id3v1->title;
329
     *     $id3v1->getTitle();
330
     * ?>
331
     * </code>
332
     *
333
     * @param string $name
334
     * @return mixed Depends on tag, which will be returned
335
     * @throws \Exception
336
     */
337
338
    public function __get(
339
        $name
340
    ) {
341
        $validNames = [
342
            'title',
343
            'artist',
344
            'album',
345
            'year',
346
            'comment',
347
            'track',
348
            'genre',
349
        ];
350
351
        if (\in_array($name, $validNames, true)) {
352
            return $this->{'get' . \ucfirst($name)}();
353
        }
354
355
        throw new RuntimeException('Property doesn\'t exist');
356
    }
357
358
    /**
359
     * Short way to assign tags
360
     *
361
     * This method allows a shorter way to assign tags.
362
     * You can assign the tags like normal properties,
363
     * and don't have to call methods.
364
     *
365
     * Here an example:
366
     * <code>
367
     * <?php
368
     *     // two ways to do the same
369
     *     $id3v1->title = 'Something';
370
     *     $id3v1->setTitle('Something);
371
     * ?>
372
     * </code>
373
     *
374
     * @param string $name
375
     * @param string $value
376
     * @return mixed Depends on tag
377
     * @throws \Exception
378
     */
379
380
    public function __set(
381
        $name,
382
        $value
383
    ) {
384
        $validNames = [
385
            'title',
386
            'artist',
387
            'album',
388
            'year',
389
            'comment',
390
            'track',
391
            'genre',
392
        ];
393
394
        if (\in_array($name, $validNames, true)) {
395
            return $this->{'set' . \ucfirst($name)}($value);
396
        }
397
398
        throw new RuntimeException('Property doesn\'t exist');
399
    }
400
401
    /**
402
     * Magic method, which gets called when casting to string
403
     *
404
     * This method will get called when the object will be used
405
     * in a context, which requires a string. You will get a
406
     * short overview over all meaningfully tags.
407
     *
408
     * Here an example:
409
     * <code>
410
     * <?php
411
     *     // casting to string
412
     *     echo $id3v1;
413
     * ?>
414
     * </code>
415
     *
416
     * @return string
417
     */
418
419
    public function __toString()
420
    {
421
        $returnedTags = [];
422
423
        foreach ($this->_tags as $tagKey => $tagVal) {
424
            if ('TAG' === $tagVal) {
425
                continue;
426
            }
427
428
            if (null === $tagVal || !$tagVal) {
429
                continue;
430
            }
431
432
            if ('genre' === $tagKey) {
433
                $returnedTags[] = self::getGenreNameById($tagVal);
434
435
                continue;
436
            }
437
438
            $returnedTags[] = $tagVal;
439
        }
440
441
        if (\count($returnedTags) > 0) {
442
            return \implode(', ', $returnedTags);
443
        }
444
    }
445
446
    /**
447
     * Gets the title out of the ID3 bytestream
448
     *
449
     * @return string
450
     */
451
452
    public function getTitle()
453
    {
454
        if (!empty($this->_tags['title'])) {
455
            return $this->_tags['title'];
456
        }
457
458
        return null;
459
    }
460
461
    /**
462
     * Gets the artist out of the ID3 bytestream
463
     *
464
     * @return string
465
     */
466
467
    public function getArtist()
468
    {
469
        if (!empty($this->_tags['artists'])) {
470
            return $this->_tags['artist'];
471
        }
472
473
        return null;
474
    }
475
476
    /**
477
     * Gets the album out of the ID3 bytestream
478
     *
479
     * @return string
480
     */
481
482
    public function getAlbum()
483
    {
484
        if (!empty($this->_tags['album'])) {
485
            return $this->_tags['album'];
486
        }
487
488
        return null;
489
    }
490
491
    /**
492
     * Gets the comment out of the ID3 bytestream
493
     *
494
     * @return string
495
     */
496
497
    public function getComment()
498
    {
499
        if (self::ID3V1_1 === $this->_version) {
500
            return mb_substr($this->_tags['comment'], 0, 28);
501
        }
502
503
        return $this->_tags['comment'];
504
    }
505
506
    /**
507
     * Gets the genre name in dependece of the genre id
508
     *
509
     * @return string
510
     * @uses getGenreNameById()
511
     */
512
513
    public function getGenre()
514
    {
515
        return self::getGenreNameById($this->_tags['genre']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::getGenreNam...($this->_tags['genre']) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
516
    }
517
518
    /**
519
     * Gets the genre id out of the ID3 bytestream
520
     *
521
     * @return int
522
     */
523
524
    public function getGenreId()
525
    {
526
        return $this->_tags['genre'];
527
    }
528
529
    /**
530
     * Gets the year out of the ID3 bytestream
531
     *
532
     * @return int
533
     */
534
535
    public function getYear()
536
    {
537
        if (!empty($this->_tags['year'])) {
538
            return (int)$this->_tags['year'];
539
        }
540
541
        return null;
542
    }
543
544
    /**
545
     * Gets the track number out of the ID3 bytestream
546
     *
547
     * @return mixed If there is no track false will be returned, else the track
548
     */
549
550
    public function getTrack()
551
    {
552
        if (self::ID3V1_0 === $this->_version || !isset($this->_tags['track'])) {
553
            return false;
554
        }
555
556
        return (int)$this->_tags['track'];
557
    }
558
559
    /**
560
     * Gets the Id3v1 version, which is used
561
     *
562
     * @return string
563
     * @see self::ID3V1_1
564
     * @see self::ID3V1_0
565
     */
566
567
    public function getId3v1Version()
568
    {
569
        return \constant('self::' . $this->_version);
570
    }
571
572
    /**
573
     * Sets the Id3v1 version, which will be used
574
     *
575
     * @param string $version The version you want to set
576
     * @return Id3v1 Implements fluent interface
577
     * @throws \Exception
578
     * @see self::ID3V1_1
579
     * @see self::ID3V1_0
580
     */
581
582
    public function setId3v1Version(
583
        $version
584
    ) {
585
        if ($this->_readOnly) {
586
            return $this;
587
        }
588
589
        switch ($version) {
590
            case self::ID3V1_0:
591
            case self::ID3V1_1:
592
                break;
593
            default:
594
                throw new RuntimeException('Invalid version');
595
        }
596
597
        $this->_version = $version;
598
599
        return $this;
600
    }
601
602
    /**
603
     * Sets the title
604
     *
605
     * The maximum length of this property, which will get stored is 30, even if
606
     * the method itself also accepts longer terms.
607
     *
608
     * @param string $title The title you want to set
609
     * @return Id3v1 Implements fluent interface
610
     * @throws \Exception
611
     * @see $_tags
612
     */
613
614
    public function setTitle(
615
        $title
616
    ) {
617
        if ($this->_readOnly) {
618
            return $this;
619
        }
620
621
        if (\is_string($title)) {
0 ignored issues
show
introduced by
The condition is_string($title) is always true.
Loading history...
622
            $this->_tags['title'] = $title;
623
        } else {
624
            throw new RuntimeException('Title has to be a string');
625
        }
626
627
        return $this;
628
    }
629
630
    /**
631
     * Sets the artist
632
     *
633
     * The maximum length of this property, which will get stored is 30, even if
634
     * the method itself also accepts longer terms.
635
     *
636
     * @param string $artist The artist you want to set
637
     * @return Id3v1 Implements fluent interface
638
     * @throws \Exception
639
     * @see $_tags
640
     */
641
642
    public function setArtist(
643
        $artist
644
    ) {
645
        if ($this->_readOnly) {
646
            return $this;
647
        }
648
649
        if (\is_string($artist)) {
0 ignored issues
show
introduced by
The condition is_string($artist) is always true.
Loading history...
650
            $this->_tags['artist'] = $artist;
651
        } else {
652
            throw new RuntimeException('Artist has to be a string');
653
        }
654
655
        return $this;
656
    }
657
658
    /**
659
     * Sets the album
660
     *
661
     * The maximum length of this property, which will get stored is 30, even if
662
     * the method itself also accepts longer terms.
663
     *
664
     * @param string $album The album you want to set
665
     * @return Id3v1 Implements fluent interface
666
     * @throws \Exception
667
     * @see $_tags
668
     */
669
670
    public function setAlbum(
671
        $album
672
    ) {
673
        if ($this->_readOnly) {
674
            return $this;
675
        }
676
677
        if (\is_string($album)) {
0 ignored issues
show
introduced by
The condition is_string($album) is always true.
Loading history...
678
            $this->_tags['album'] = $album;
679
        } else {
680
            throw new RuntimeException('Album has to be a string');
681
        }
682
683
        return $this;
684
    }
685
686
    /**
687
     * Sets the comment
688
     *
689
     * The maximum length of this property, which will get stored depends
690
     * on the used Id3v1 version. Where ID3V1_0 stores 30, ID3V1_1 just
691
     * stores 28 characters, in order to save place for the track number.
692
     * The method itselfs takes also longer terms.
693
     *
694
     * @param string $comment The comment you want to set
695
     * @return Id3v1 Implements fluent interface
696
     * @throws \Exception
697
     * @see $_tags
698
     */
699
700
    public function setComment(
701
        $comment
702
    ) {
703
        if ($this->_readOnly) {
704
            return $this;
705
        }
706
707
        if (\is_string($comment)) {
0 ignored issues
show
introduced by
The condition is_string($comment) is always true.
Loading history...
708
            $this->_tags['comment'] = $comment;
709
        } else {
710
            throw new RuntimeException('Comment has to be a string');
711
        }
712
713
        return $this;
714
    }
715
716
    /**
717
     * Sets the genre
718
     *
719
     * You can either use the genre id, or the genre name.
720
     *
721
     * @param string|int $genre The genre you want to set
722
     * @return Id3v1 Implements fluent interface
723
     * @throws \Exception
724
     * @see $_tags
725
     */
726
727
    public function setGenre(
728
        $genre
729
    ) {
730
        if ($this->_readOnly) {
731
            return $this;
732
        }
733
734
        if (\is_int($genre)) {
735
            $this->_tags['genre'] = $genre;
736
        } elseif (\is_string($genre)) {
0 ignored issues
show
introduced by
The condition is_string($genre) is always true.
Loading history...
737
            $this->_tags['genre'] = self::getGenreIdByName($genre);
738
        } else {
739
            throw new RuntimeException('Genre type invalid');
740
        }
741
742
        return $this;
743
    }
744
745
    /**
746
     * Sets the year
747
     *
748
     * @param int $year The year you want to set
749
     * @return Id3v1 Implements fluent interface
750
     * @throws \Exception
751
     * @see $_tags
752
     */
753
754
    public function setYear(
755
        $year
756
    ) {
757
        if ($this->_readOnly) {
758
            return $this;
759
        }
760
761
        if (\is_int($year)) {
0 ignored issues
show
introduced by
The condition is_int($year) is always true.
Loading history...
762
            $this->_tags['year'] = $year;
763
        } else {
764
            throw new RuntimeException('Year has to be an interger');
765
        }
766
767
        return $this;
768
    }
769
770
    /**
771
     * Sets the track
772
     *
773
     * The Id3v1 version will be set automatically to ID3V1_1, because
774
     * this property is just defined there. If you want to store explicit
775
     * ID3V1_0 you have to set it manually after calling this method.
776
     *
777
     * @param int $track The tracl you want to set
778
     * @return Id3v1 Implements fluent interface
779
     * @throws \Exception
780
     * @see $_tags
781
     * @see setId3v1Version()
782
     * @see getId3v1Version()
783
     */
784
785
    public function setTrack(
786
        $track
787
    ) {
788
        if ($this->_readOnly) {
789
            return $this;
790
        }
791
792
        if (\is_int($track) && 0 !== $track) {
793
            $this->_tags['track'] = $track;
794
795
            $this->_version = self::ID3V1_1;
796
        } else {
797
            throw new RuntimeException('Track type invalid or zero');
798
        }
799
800
        return $this;
801
    }
802
803
    /**
804
     * Clears all tags
805
     *
806
     * This method sets the default value for each tag.
807
     *
808
     * @return Id3v1 Implements fluent interface
809
     * @see $_tags
810
     */
811
812
    public function clearAllTags()
813
    {
814
        if ($this->_readOnly) {
815
            return $this;
816
        }
817
818
        $this->_tags['marking'] = 'TAG';
819
820
        $this->_tags['title'] = '';
821
822
        $this->_tags['artist'] = '';
823
824
        $this->_tags['album'] = '';
825
826
        $this->_tags['year'] = null;
827
828
        $this->_tags['comment'] = '';
829
830
        $this->_tags['track'] = null;
831
832
        $this->_tags['genre'] = 255;
833
834
        return $this;
835
    }
836
837
    /**
838
     * Gets the genre id out of a genre name
839
     *
840
     * @param $genreName
841
     * @return int
842
     * @see $_genres
843
     */
844
845
    public static function getGenreIdByName(
846
        $genreName
847
    ) {
848
        $genres = \array_flip(self::$_genres);
849
850
        if (!isset($genres[$genreName])) {
851
            return 255;
852
        }
853
854
        return (int)$genres[$genreName];
855
    }
856
857
    /**
858
     * Gets the genre name out of a genre id
859
     *
860
     * @param $genreId
861
     * @return string|bool
862
     * @see $_genres
863
     */
864
865
    public static function getGenreNameById(
866
        $genreId
867
    ) {
868
        return self::$_genres[$genreId] ?? false;
869
    }
870
871
    /**
872
     * Returns a array with all defined genres
873
     *
874
     * @return array
875
     * @see $_genres
876
     */
877
878
    public static function getGenreList()
879
    {
880
        return self::$_genres;
881
    }
882
883
    /**
884
     * Saves the set tags to the file
885
     *
886
     * This method saves the set tags to the file. Therefore it seeks to
887
     * the end of the file and writes the Id3v1 bytestream to it.
888
     *
889
     * @return Id3v1 Implements fluent interface
890
     * @throws \Exception
891
     * @see $_tags
892
     * @see setTitle()
893
     * @see setArtist()
894
     * @see setAlbum()
895
     * @see setComment()
896
     * @see setGenre()
897
     * @see setYear()
898
     * @see setTrack()
899
     */
900
901
    public function save()
902
    {
903
        if ($this->_readOnly) {
904
            return $this;
905
        }
906
907
        \fseek($this->_stream, -128, \SEEK_END);
908
909
        if ('TAG' !== $this->_tags['marking']) {
910
            \fseek($this->_stream, 0, \SEEK_END);
911
        }
912
913
        $newTag = '';
914
915
        if (self::ID3V1_0 === $this->_version) {
916
            $newTag = \pack(
917
                'a3a30a30a30a4a30C1',
918
                'TAG',
919
                $this->_tags['title'],
920
                $this->_tags['artist'],
921
                $this->_tags['album'],
922
                $this->_tags['year'],
923
                $this->_tags['comment'],
924
                $this->_tags['genre']
925
            );
926
        } else {
927
            $newTag = \pack(
928
                'a3a30a30a30a4a28x1C1C1',
929
                'TAG',
930
                $this->_tags['title'],
931
                $this->_tags['artist'],
932
                $this->_tags['album'],
933
                $this->_tags['year'],
934
                $this->_tags['comment'],
935
                $this->_tags['track'],
936
                $this->_tags['genre']
937
            );
938
        }
939
940
        if (false === \fwrite($this->_stream, $newTag, 128)) {
941
            throw new RuntimeException('Not possible to write ID3 tags');
942
        }
943
944
        return $this;
945
    }
946
}
947