Completed
Pull Request — master (#349)
by
unknown
05:52
created

PDFObject::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
 * @file
5
 *          This file is part of the PdfParser library.
6
 *
7
 * @author  Sébastien MALOT <[email protected]>
8
 * @date    2017-01-03
9
 *
10
 * @license LGPLv3
11
 * @url     <https://github.com/smalot/pdfparser>
12
 *
13
 *  PdfParser is a pdf library written in PHP, extraction oriented.
14
 *  Copyright (C) 2017 - Sébastien MALOT <[email protected]>
15
 *
16
 *  This program is free software: you can redistribute it and/or modify
17
 *  it under the terms of the GNU Lesser General Public License as published by
18
 *  the Free Software Foundation, either version 3 of the License, or
19
 *  (at your option) any later version.
20
 *
21
 *  This program is distributed in the hope that it will be useful,
22
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 *  GNU Lesser General Public License for more details.
25
 *
26
 *  You should have received a copy of the GNU Lesser General Public License
27
 *  along with this program.
28
 *  If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
29
 */
30
31
namespace Smalot\PdfParser;
32
33
use Smalot\PdfParser\XObject\Form;
34
use Smalot\PdfParser\XObject\Image;
35
36
/**
37
 * Class PDFObject
38
 */
39
class PDFObject
40
{
41
    const TYPE = 't';
42
43
    const OPERATOR = 'o';
44
45
    const COMMAND = 'c';
46
47
    /**
48
     * The recursion stack.
49
     *
50
     * @var array
51
     */
52
    public static $recursionStack = [];
53
54
    /**
55
     * @var Document
56
     */
57
    protected $document = null;
58
59
    /**
60
     * @var Header
61
     */
62
    protected $header = null;
63
64
    /**
65
     * @var string
66
     */
67
    protected $content = null;
68
69
    /**
70
     * @param Header $header
71
     * @param string $content
72
     */
73 22
    public function __construct(Document $document, Header $header = null, $content = null)
74
    {
75 22
        $this->document = $document;
76 22
        $this->header = null !== $header ? $header : new Header();
77 22
        $this->content = $content;
78 22
    }
79
80 18
    public function init()
81
    {
82 18
    }
83
84
    /**
85
     * @return Header|null
86
     */
87 18
    public function getHeader()
88
    {
89 18
        return $this->header;
90
    }
91
92
    /**
93
     * @param string $name
94
     *
95
     * @return Element|PDFObject
96
     */
97 13
    public function get($name)
98
    {
99 13
        return $this->header->get($name);
100
    }
101
102
    /**
103
     * @param string $name
104
     *
105
     * @return bool
106
     */
107 12
    public function has($name)
108
    {
109 12
        return $this->header->has($name);
110
    }
111
112
    /**
113
     * @param bool $deep
114
     *
115
     * @return array
116
     */
117 1
    public function getDetails($deep = true)
118
    {
119 1
        return $this->header->getDetails($deep);
120
    }
121
122
    /**
123
     * @return string|null
124
     */
125 10
    public function getContent()
126
    {
127 10
        return $this->content;
128
    }
129
130
    /**
131
     * @param string $content
132
     */
133 5
    public function cleanContent($content, $char = 'X')
134
    {
135 5
        $char = $char[0];
136 5
        $content = str_replace(['\\\\', '\\)', '\\('], $char.$char, $content);
137
138
        // Remove image bloc with binary content
139 5
        preg_match_all('/\s(BI\s.*?(\sID\s).*?(\sEI))\s/s', $content, $matches, PREG_OFFSET_CAPTURE);
140 5
        foreach ($matches[0] as $part) {
141
            $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
142
        }
143
144
        // Clean content in square brackets [.....]
145 5
        preg_match_all('/\[((\(.*?\)|[0-9\.\-\s]*)*)\]/s', $content, $matches, PREG_OFFSET_CAPTURE);
146 5
        foreach ($matches[1] as $part) {
147 4
            $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
148
        }
149
150
        // Clean content in round brackets (.....)
151 5
        preg_match_all('/\((.*?)\)/s', $content, $matches, PREG_OFFSET_CAPTURE);
152 5
        foreach ($matches[1] as $part) {
153 4
            $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
154
        }
155
156
        // Clean structure
157 5
        if ($parts = preg_split('/(<|>)/s', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)) {
158 5
            $content = '';
159 5
            $level = 0;
160 5
            foreach ($parts as $part) {
161 5
                if ('<' == $part) {
162 4
                    ++$level;
163
                }
164
165 5
                $content .= (0 == $level ? $part : str_repeat($char, \strlen($part)));
166
167 5
                if ('>' == $part) {
168 4
                    --$level;
169
                }
170
            }
171
        }
172
173
        // Clean BDC and EMC markup
174 5
        preg_match_all(
175 5
            '/(\/[A-Za-z0-9\_]*\s*'.preg_quote($char).'*BDC)/s',
176
            $content,
177
            $matches,
178 5
            PREG_OFFSET_CAPTURE
179
        );
180 5
        foreach ($matches[1] as $part) {
181 2
            $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
182
        }
183
184 5
        preg_match_all('/\s(EMC)\s/s', $content, $matches, PREG_OFFSET_CAPTURE);
185 5
        foreach ($matches[1] as $part) {
186 3
            $content = substr_replace($content, str_repeat($char, \strlen($part[0])), $part[1], \strlen($part[0]));
187
        }
188
189 5
        return $content;
190
    }
191
192
    /**
193
     * @param string $content
194
     *
195
     * @return array
196
     */
197 4
    public function getSectionsText($content)
198
    {
199 4
        $sections = [];
200 4
        $content = ' '.$content.' ';
201 4
        $textCleaned = $this->cleanContent($content, '_');
202
203
        // Extract text blocks.
204 4
        if (preg_match_all('/\s+BT[\s|\(|\[]+(.*?)\s*ET/s', $textCleaned, $matches, PREG_OFFSET_CAPTURE)) {
205 4
            foreach ($matches[1] as $part) {
206 4
                $text = $part[0];
207 4
                if ('' === $text) {
208
                    continue;
209
                }
210 4
                $offset = $part[1];
211 4
                $section = substr($content, $offset, \strlen($text));
212
213
                // Removes BDC and EMC markup.
214 4
                $section = preg_replace('/(\/[A-Za-z0-9]+\s*<<.*?)(>>\s*BDC)(.*?)(EMC\s+)/s', '${3}', $section.' ');
215
216 4
                $sections[] = $section;
217
            }
218
        }
219
220
        // Extract 'do' commands.
221 4
        if (preg_match_all('/(\/[A-Za-z0-9\.\-_]+\s+Do)\s/s', $textCleaned, $matches, PREG_OFFSET_CAPTURE)) {
222 1
            foreach ($matches[1] as $part) {
223 1
                $text = $part[0];
224 1
                $offset = $part[1];
225 1
                $section = substr($content, $offset, \strlen($text));
226
227 1
                $sections[] = $section;
228
            }
229
        }
230
231 4
        return $sections;
232
    }
233
234 2
    private function getDefaultFont(Page $page = null)
235
    {
236 2
        $fonts = [];
237 2
        if (null !== $page) {
238 2
            $fonts = $page->getFonts();
239
        }
240
241 2
        $fonts = array_merge($fonts, array_values($this->document->getFonts()));
242
243 2
        if (\count($fonts) > 0) {
244 2
            return reset($fonts);
245
        }
246
247
        return new Font($this->document);
248
    }
249
250
    /**
251
     * @param Page $page
252
     *
253
     * @return string
254
     *
255
     * @throws \Exception
256
     */
257 2
    public function getText(Page $page = null)
258
    {
259 2
        $text = '';
260 2
        $sections = $this->getSectionsText($this->content);
261 2
        $current_font = $this->getDefaultFont($page);
262
263 2
        $current_position_td = ['x' => false, 'y' => false];
264 2
        $current_position_tm = ['x' => false, 'y' => false];
265
266 2
        array_push(self::$recursionStack, $this->getUniqueId());
267
268 2
        foreach ($sections as $section) {
269 2
            $commands = $this->getCommandsText($section);
270
271 2
            foreach ($commands as $command) {
272 2
                switch ($command[self::OPERATOR]) {
273
                    // set character spacing
274 2
                    case 'Tc':
275
                        break;
276
277
                    // move text current point
278 2
                    case 'Td':
279
                        $args = preg_split('/\s/s', $command[self::COMMAND]);
280
                        $y = array_pop($args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type false; however, parameter $array of array_pop() 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

280
                        $y = array_pop(/** @scrutinizer ignore-type */ $args);
Loading history...
281
                        $x = array_pop($args);
282
                        if (((float) $x <= 0) ||
283
                            (false !== $current_position_td['y'] && (float) $y < (float) ($current_position_td['y']))
284
                        ) {
285
                            // vertical offset
286
                            $text .= "\n";
287
                        } elseif (false !== $current_position_td['x'] && (float) $x > (float) (
288
                                $current_position_td['x']
289
                            )
290
                        ) {
291
                            // horizontal offset
292
                            $text .= ' ';
293
                        }
294
                        $current_position_td = ['x' => $x, 'y' => $y];
295
                        break;
296
297
                    // move text current point and set leading
298 2
                    case 'TD':
299
                        $args = preg_split('/\s/s', $command[self::COMMAND]);
300
                        $y = array_pop($args);
301
                        $x = array_pop($args);
302
                        if ((float) $y < 0) {
303
                            $text .= "\n";
304
                        } elseif ((float) $x <= 0) {
305
                            $text .= ' ';
306
                        }
307
                        break;
308
309 2
                    case 'Tf':
310 2
                        list($id) = preg_split('/\s/s', $command[self::COMMAND]);
311 2
                        $id = trim($id, '/');
312 2
                        if (null !== $page) {
313 2
                            $new_font = $page->getFont($id);
314
                            // If an invalid font ID is given, do not update the font.
315
                            // This should theoretically never happen, as the PDF spec states for the Tf operator:
316
                            // "The specified font value shall match a resource name in the Font entry of the default resource dictionary"
317
                            // (https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf, page 435)
318
                            // But we want to make sure that malformed PDFs do not simply crash.
319 2
                            if (null !== $new_font) {
320 2
                                $current_font = $new_font;
321
                            }
322
                        }
323 2
                        break;
324
325 2
                    case "'":
326 2
                    case 'Tj':
327
                        $command[self::COMMAND] = [$command];
328
                        // no break
329 2
                    case 'TJ':
330 2
                        $sub_text = $current_font->decodeText($command[self::COMMAND]);
331
                        $text .= $sub_text;
332
                        break;
333
334
                    // set leading
335 2
                    case 'TL':
336
                        $text .= ' ';
337
                        break;
338
339 2
                    case 'Tm':
340 2
                        $args = preg_split('/\s/s', $command[self::COMMAND]);
341 2
                        $y = array_pop($args);
342 2
                        $x = array_pop($args);
343 2
                        if (false !== $current_position_tm['x']) {
344
                            $delta = abs((float) $x - (float) ($current_position_tm['x']));
345
                            if ($delta > 10) {
346
                                $text .= "\t";
347
                            }
348
                        }
349 2
                        if (false !== $current_position_tm['y']) {
350
                            $delta = abs((float) $y - (float) ($current_position_tm['y']));
351
                            if ($delta > 10) {
352
                                $text .= "\n";
353
                            }
354
                        }
355 2
                        $current_position_tm = ['x' => $x, 'y' => $y];
356 2
                        break;
357
358
                    // set super/subscripting text rise
359 2
                    case 'Ts':
360
                        break;
361
362
                    // set word spacing
363 2
                    case 'Tw':
364
                        break;
365
366
                    // set horizontal scaling
367 2
                    case 'Tz':
368
                        $text .= "\n";
369
                        break;
370
371
                    // move to start of next line
372 2
                    case 'T*':
373
                        $text .= "\n";
374
                        break;
375
376 2
                    case 'Da':
377
                        break;
378
379 2
                    case 'Do':
380
                        if (null !== $page) {
381
                            $args = preg_split('/\s/s', $command[self::COMMAND]);
382
                            $id = trim(array_pop($args), '/ ');
383
                            $xobject = $page->getXObject($id);
384
385
                            // @todo $xobject could be a ElementXRef object, which would then throw an error
386
                            if (\is_object($xobject) && $xobject instanceof self && !\in_array($xobject->getUniqueId(), self::$recursionStack)) {
387
                                // Not a circular reference.
388
                                $text .= $xobject->getText($page);
389
                            }
390
                        }
391
                        break;
392
393 2
                    case 'rg':
394 2
                    case 'RG':
395
                        break;
396
397 2
                    case 're':
398
                        break;
399
400 2
                    case 'co':
401
                        break;
402
403 2
                    case 'cs':
404
                        break;
405
406 2
                    case 'gs':
407 1
                        break;
408
409 2
                    case 'en':
410
                        break;
411
412 2
                    case 'sc':
413 2
                    case 'SC':
414
                        break;
415
416 2
                    case 'g':
417 2
                    case 'G':
418 1
                        break;
419
420 1
                    case 'V':
421
                        break;
422
423 1
                    case 'vo':
424 1
                    case 'Vo':
425
                        break;
426
427
                    default:
428
                }
429
            }
430
        }
431
432
        array_pop(self::$recursionStack);
433
434
        return $text.' ';
435
    }
436
437
    /**
438
     * @param Page $page
439
     *
440
     * @return array
441
     *
442
     * @throws \Exception
443
     */
444
    public function getTextArray(Page $page = null)
445
    {
446
        $text = [];
447
        $sections = $this->getSectionsText($this->content);
448
        $current_font = new Font($this->document);
449
450
        foreach ($sections as $section) {
451
            $commands = $this->getCommandsText($section);
452
453
            foreach ($commands as $command) {
454
                switch ($command[self::OPERATOR]) {
455
                    // set character spacing
456
                    case 'Tc':
457
                        break;
458
459
                    // move text current point
460
                    case 'Td':
461
                        break;
462
463
                    // move text current point and set leading
464
                    case 'TD':
465
                        break;
466
467
                    case 'Tf':
468
                        if (null !== $page) {
469
                            list($id) = preg_split('/\s/s', $command[self::COMMAND]);
470
                            $id = trim($id, '/');
471
                            $current_font = $page->getFont($id);
472
                        }
473
                        break;
474
475
                    case "'":
476
                    case 'Tj':
477
                        $command[self::COMMAND] = [$command];
478
                        // no break
479
                    case 'TJ':
480
                        $sub_text = $current_font->decodeText($command[self::COMMAND]);
481
                        $text[] = $sub_text;
482
                        break;
483
484
                    // set leading
485
                    case 'TL':
486
                        break;
487
488
                    case 'Tm':
489
                        break;
490
491
                    // set super/subscripting text rise
492
                    case 'Ts':
493
                        break;
494
495
                    // set word spacing
496
                    case 'Tw':
497
                        break;
498
499
                    // set horizontal scaling
500
                    case 'Tz':
501
                        //$text .= "\n";
502
                        break;
503
504
                    // move to start of next line
505
                    case 'T*':
506
                        //$text .= "\n";
507
                        break;
508
509
                    case 'Da':
510
                        break;
511
512
                    case 'Do':
513
                        if (null !== $page) {
514
                            $args = preg_split('/\s/s', $command[self::COMMAND]);
515
                            $id = trim(array_pop($args), '/ ');
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type false; however, parameter $array of array_pop() 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

515
                            $id = trim(array_pop(/** @scrutinizer ignore-type */ $args), '/ ');
Loading history...
516
                            if ($xobject = $page->getXObject($id)) {
517
                                $text[] = $xobject->getText($page);
518
                            }
519
                        }
520
                        break;
521
522
                    case 'rg':
523
                    case 'RG':
524
                        break;
525
526
                    case 're':
527
                        break;
528
529
                    case 'co':
530
                        break;
531
532
                    case 'cs':
533
                        break;
534
535
                    case 'gs':
536
                        break;
537
538
                    case 'en':
539
                        break;
540
541
                    case 'sc':
542
                    case 'SC':
543
                        break;
544
545
                    case 'g':
546
                    case 'G':
547
                        break;
548
549
                    case 'V':
550
                        break;
551
552
                    case 'vo':
553
                    case 'Vo':
554
                        break;
555
556
                    default:
557
                }
558
            }
559
        }
560
561
        return $text;
562
    }
563
564
    /**
565
     * @param string $text_part
566
     * @param int    $offset
567
     *
568
     * @return array
569
     */
570 4
    public function getCommandsText($text_part, &$offset = 0)
571
    {
572 4
        $commands = $matches = [];
573
574 4
        while ($offset < \strlen($text_part)) {
575 4
            $offset += strspn($text_part, "\x00\x09\x0a\x0c\x0d\x20", $offset);
576 4
            $char = $text_part[$offset];
577
578 4
            $operator = '';
579 4
            $type = '';
580 4
            $command = false;
581
582 4
            switch ($char) {
583 4
                case '/':
584 4
                    $type = $char;
585 4
                    if (preg_match(
586 4
                        '/^\/([A-Z0-9\._,\+]+\s+[0-9.\-]+)\s+([A-Z]+)\s*/si',
587 4
                        substr($text_part, $offset),
588
                        $matches
589
                    )
590
                    ) {
591 4
                        $operator = $matches[2];
592 4
                        $command = $matches[1];
593 4
                        $offset += \strlen($matches[0]);
594
                    } elseif (preg_match(
595 1
                        '/^\/([A-Z0-9\._,\+]+)\s+([A-Z]+)\s*/si',
596 1
                        substr($text_part, $offset),
597
                        $matches
598
                    )
599
                    ) {
600 1
                        $operator = $matches[2];
601 1
                        $command = $matches[1];
602 1
                        $offset += \strlen($matches[0]);
603
                    }
604 4
                    break;
605
606 4
                case '[':
607 4
                case ']':
608
                    // array object
609 4
                    $type = $char;
610 4
                    if ('[' == $char) {
611 4
                        ++$offset;
612
                        // get elements
613 4
                        $command = $this->getCommandsText($text_part, $offset);
614
615 4
                        if (preg_match('/^\s*[A-Z]{1,2}\s*/si', substr($text_part, $offset), $matches)) {
616 4
                            $operator = trim($matches[0]);
617 4
                            $offset += \strlen($matches[0]);
618
                        }
619
                    } else {
620 4
                        ++$offset;
621 4
                        break;
622
                    }
623 4
                    break;
624
625 4
                case '<':
626 4
                case '>':
627
                    // array object
628 2
                    $type = $char;
629 2
                    ++$offset;
630 2
                    if ('<' == $char) {
631 2
                        $strpos = strpos($text_part, '>', $offset);
632 2
                        $command = substr($text_part, $offset, ($strpos - $offset));
633 2
                        $offset = $strpos + 1;
634
                    }
635
636 2
                    if (preg_match('/^\s*[A-Z]{1,2}\s*/si', substr($text_part, $offset), $matches)) {
637 1
                        $operator = trim($matches[0]);
638 1
                        $offset += \strlen($matches[0]);
639
                    }
640 2
                    break;
641
642 4
                case '(':
643 4
                case ')':
644 3
                    ++$offset;
645 3
                    $type = $char;
646 3
                    $strpos = $offset;
647 3
                    if ('(' == $char) {
648 3
                        $open_bracket = 1;
649 3
                        while ($open_bracket > 0) {
650 3
                            if (!isset($text_part[$strpos])) {
651
                                break;
652
                            }
653 3
                            $ch = $text_part[$strpos];
654 3
                            switch ($ch) {
655 3
                                case '\\':
656
                                 // REVERSE SOLIDUS (5Ch) (Backslash)
657
                                    // skip next character
658 2
                                    ++$strpos;
659 2
                                    break;
660
661 3
                                case '(':
662
                                 // LEFT PARENHESIS (28h)
663
                                    ++$open_bracket;
664
                                    break;
665
666 3
                                case ')':
667
                                 // RIGHT PARENTHESIS (29h)
668 3
                                    --$open_bracket;
669 3
                                    break;
670
                            }
671 3
                            ++$strpos;
672
                        }
673 3
                        $command = substr($text_part, $offset, ($strpos - $offset - 1));
674 3
                        $offset = $strpos;
675
676 3
                        if (preg_match('/^\s*([A-Z\']{1,2})\s*/si', substr($text_part, $offset), $matches)) {
677 2
                            $operator = $matches[1];
678 2
                            $offset += \strlen($matches[0]);
679
                        }
680
                    }
681 3
                    break;
682
683
                default:
684
685 4
                    if ('ET' == substr($text_part, $offset, 2)) {
686 1
                        break;
687
                    } elseif (preg_match(
688 4
                        '/^\s*(?P<data>([0-9\.\-]+\s*?)+)\s+(?P<id>[A-Z]{1,3})\s*/si',
689 4
                        substr($text_part, $offset),
690
                        $matches
691
                    )
692
                    ) {
693 4
                        $operator = trim($matches['id']);
694 4
                        $command = trim($matches['data']);
695 4
                        $offset += \strlen($matches[0]);
696 4
                    } elseif (preg_match('/^\s*([0-9\.\-]+\s*?)+\s*/si', substr($text_part, $offset), $matches)) {
697 4
                        $type = 'n';
698 4
                        $command = trim($matches[0]);
699 4
                        $offset += \strlen($matches[0]);
700 2
                    } elseif (preg_match('/^\s*([A-Z\*]+)\s*/si', substr($text_part, $offset), $matches)) {
701 2
                        $type = '';
702 2
                        $operator = $matches[1];
703 2
                        $command = '';
704 2
                        $offset += \strlen($matches[0]);
705
                    }
706
            }
707
708 4
            if (false !== $command) {
709 4
                $commands[] = [
710 4
                    self::TYPE => $type,
711 4
                    self::OPERATOR => $operator,
712 4
                    self::COMMAND => $command,
713
                ];
714
            } else {
715 4
                break;
716
            }
717
        }
718
719 4
        return $commands;
720
    }
721
722
    /**
723
     * @param string $content
724
     *
725
     * @return PDFObject
726
     */
727 10
    public static function factory(Document $document, Header $header, $content)
728
    {
729 10
        switch ($header->get('Type')->getContent()) {
730 10
            case 'XObject':
731 2
                switch ($header->get('Subtype')->getContent()) {
732 2
                    case 'Image':
733 1
                        return new Image($document, $header, $content);
734
735 1
                    case 'Form':
736 1
                        return new Form($document, $header, $content);
737
                }
738
739
                return new self($document, $header, $content);
740
741 10
            case 'Pages':
742 10
                return new Pages($document, $header, $content);
743
744 10
            case 'Page':
745 10
                return new Page($document, $header, $content);
746
747 10
            case 'Encoding':
748 1
                return new Encoding($document, $header, $content);
749
750 10
            case 'Font':
751 10
                $subtype = $header->get('Subtype')->getContent();
752 10
                $classname = '\Smalot\PdfParser\Font\Font'.$subtype;
753
754 10
                if (class_exists($classname)) {
755 10
                    return new $classname($document, $header, $content);
756
                }
757
758
                return new Font($document, $header, $content);
759
760
            default:
761 10
                return new self($document, $header, $content);
762
        }
763
    }
764
765
    /**
766
     * Returns unique id identifying the object.
767
     *
768
     * @return string
769
     */
770 2
    protected function getUniqueId()
771
    {
772 2
        return spl_object_hash($this);
773
    }
774
}
775