Project

General

Profile

1
#!/usr/bin/env php
2
<?php
3

    
4
/*
5
 * This file is part of the Predis package.
6
 *
7
 * (c) Daniele Alessandri <suppakilla@gmail.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

    
13
// -------------------------------------------------------------------------- //
14
// This script can be used to automatically glue all the .php files of Predis
15
// into a single monolithic script file that can be used without an autoloader,
16
// just like the other previous versions of the library.
17
//
18
// Much of its complexity is due to the fact that we cannot simply join PHP
19
// files, but namespaces and classes definitions must follow a precise order
20
// when dealing with subclassing and inheritance.
21
//
22
// The current implementation is pretty na?ve, but it should do for now.
23
// -------------------------------------------------------------------------- //
24

    
25
class CommandLine
26
{
27
    public static function getOptions()
28
    {
29
        $parameters = array(
30
            's:'  => 'source:',
31
            'o:'  => 'output:',
32
            'e:'  => 'exclude:',
33
            'E:'  => 'exclude-classes:',
34
        );
35

    
36
        $getops = getopt(implode(array_keys($parameters)), $parameters);
37

    
38
        $options = array(
39
            'source'  => __DIR__ . "/../lib/",
40
            'output'  => PredisFile::NS_ROOT . '.php',
41
            'exclude' => array(),
42
        );
43

    
44
        foreach ($getops as $option => $value) {
45
            switch ($option) {
46
                case 's':
47
                case 'source':
48
                    $options['source'] = $value;
49
                    break;
50

    
51
                case 'o':
52
                case 'output':
53
                    $options['output'] = $value;
54
                    break;
55

    
56
                case 'E':
57
                case 'exclude-classes':
58
                    $options['exclude'] = @file($value, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: $value;
59
                    break;
60

    
61
                case 'e':
62
                case 'exclude':
63
                    $options['exclude'] = is_array($value) ? $value : array($value);
64
                    break;
65
            }
66
        }
67

    
68
        return $options;
69
    }
70
}
71

    
72
class PredisFile
73
{
74
    const NS_ROOT = 'Predis';
75

    
76
    private $namespaces;
77

    
78
    public function __construct()
79
    {
80
        $this->namespaces = array();
81
    }
82

    
83
    public static function from($libraryPath, Array $exclude = array())
84
    {
85
        $nsroot = self::NS_ROOT;
86
        $predisFile = new PredisFile();
87
        $libIterator = new RecursiveDirectoryIterator("$libraryPath$nsroot");
88

    
89
        foreach (new RecursiveIteratorIterator($libIterator) as $classFile)
90
        {
91
            if (!$classFile->isFile()) {
92
                continue;
93
            }
94

    
95
            $namespace = strtr(str_replace($libraryPath, '', $classFile->getPath()), '/', '\\');
96

    
97
            if (in_array(sprintf('%s\\%s', $namespace, $classFile->getBasename('.php')), $exclude)) {
98
                continue;
99
            }
100

    
101
            $phpNamespace = $predisFile->getNamespace($namespace);
102

    
103
            if ($phpNamespace === false) {
104
                $phpNamespace = new PhpNamespace($namespace);
105
                $predisFile->addNamespace($phpNamespace);
106
            }
107

    
108
            $phpClass = new PhpClass($phpNamespace, $classFile);
109
        }
110

    
111
        return $predisFile;
112
    }
113

    
114
    public function addNamespace(PhpNamespace $namespace)
115
    {
116
        if (isset($this->namespaces[(string)$namespace])) {
117
            throw new InvalidArgumentException("Duplicated namespace");
118
        }
119
        $this->namespaces[(string)$namespace] = $namespace;
120
    }
121

    
122
    public function getNamespaces()
123
    {
124
        return $this->namespaces;
125
    }
126

    
127
    public function getNamespace($namespace)
128
    {
129
        if (!isset($this->namespaces[$namespace])) {
130
            return false;
131
        }
132

    
133
        return $this->namespaces[$namespace];
134
    }
135

    
136
    public function getClassByFQN($classFqn)
137
    {
138
        if (($nsLastPos = strrpos($classFqn, '\\')) !== false) {
139
            $namespace = $this->getNamespace(substr($classFqn, 0, $nsLastPos));
140
            if ($namespace === false) {
141
                return null;
142
            }
143
            $className = substr($classFqn, $nsLastPos + 1);
144

    
145
            return $namespace->getClass($className);
146
        }
147

    
148
        return null;
149
    }
150

    
151
    private function calculateDependencyScores(&$classes, $fqn)
152
    {
153
        if (!isset($classes[$fqn])) {
154
            $classes[$fqn] = 0;
155
        }
156

    
157
        $classes[$fqn] += 1;
158

    
159
        if (($phpClass = $this->getClassByFQN($fqn)) === null) {
160
            throw new RuntimeException(
161
                "Cannot found the class $fqn which is required by other subclasses. Are you missing a file?"
162
            );
163
        }
164

    
165
        foreach ($phpClass->getDependencies() as $fqn) {
166
            $this->calculateDependencyScores($classes, $fqn);
167
        }
168
    }
169

    
170
    private function getDependencyScores()
171
    {
172
        $classes = array();
173

    
174
        foreach ($this->getNamespaces() as $phpNamespace) {
175
            foreach ($phpNamespace->getClasses() as $phpClass) {
176
                $this->calculateDependencyScores($classes, $phpClass->getFQN());
177
            }
178
        }
179

    
180
        return $classes;
181
    }
182

    
183
    private function getOrderedNamespaces($dependencyScores)
184
    {
185
        $namespaces = array_fill_keys(array_unique(
186
            array_map(
187
                function ($fqn) { return PhpNamespace::extractName($fqn); },
188
                array_keys($dependencyScores)
189
            )
190
        ), 0);
191

    
192
        foreach ($dependencyScores as $classFqn => $score) {
193
            $namespaces[PhpNamespace::extractName($classFqn)] += $score;
194
        }
195

    
196
        arsort($namespaces);
197

    
198
        return array_keys($namespaces);
199
    }
200

    
201
    private function getOrderedClasses(PhpNamespace $phpNamespace, $classes)
202
    {
203
        $nsClassesFQNs = array_map(function ($cl) { return $cl->getFQN(); }, $phpNamespace->getClasses());
204
        $nsOrderedClasses = array();
205

    
206
        foreach ($nsClassesFQNs as $nsClassFQN) {
207
            $nsOrderedClasses[$nsClassFQN] = $classes[$nsClassFQN];
208
        }
209

    
210
        arsort($nsOrderedClasses);
211

    
212
        return array_keys($nsOrderedClasses);
213
    }
214

    
215
    public function getPhpCode()
216
    {
217
        $buffer = array("<?php\n\n", PhpClass::LICENSE_HEADER, "\n\n");
218
        $classes = $this->getDependencyScores();
219
        $namespaces = $this->getOrderedNamespaces($classes);
220

    
221
        foreach ($namespaces as $namespace) {
222
            $phpNamespace = $this->getNamespace($namespace);
223

    
224
            // generate namespace directive
225
            $buffer[] = $phpNamespace->getPhpCode();
226
            $buffer[] = "\n";
227

    
228
            // generate use directives
229
            $useDirectives = $phpNamespace->getUseDirectives();
230
            if (count($useDirectives) > 0) {
231
                $buffer[] = $useDirectives->getPhpCode();
232
                $buffer[] = "\n";
233
            }
234

    
235
            // generate classes bodies
236
            $nsClasses = $this->getOrderedClasses($phpNamespace, $classes);
237
            foreach ($nsClasses as $classFQN) {
238
                $buffer[] = $this->getClassByFQN($classFQN)->getPhpCode();
239
                $buffer[] = "\n\n";
240
            }
241

    
242
            $buffer[] = "/* " . str_repeat("-", 75) . " */";
243
            $buffer[] = "\n\n";
244
        }
245

    
246
        return implode($buffer);
247
    }
248

    
249
    public function saveTo($outputFile)
250
    {
251
        // TODO: add more sanity checks
252
        if ($outputFile === null || $outputFile === '') {
253
            throw new InvalidArgumentException('You must specify a valid output file');
254
        }
255
        file_put_contents($outputFile, $this->getPhpCode());
256
    }
257
}
258

    
259
class PhpNamespace implements IteratorAggregate
260
{
261
    private $namespace;
262
    private $classes;
263

    
264
    public function __construct($namespace)
265
    {
266
        $this->namespace = $namespace;
267
        $this->classes = array();
268
        $this->useDirectives = new PhpUseDirectives($this);
269
    }
270

    
271
    public static function extractName($fqn)
272
    {
273
        $nsSepLast = strrpos($fqn, '\\');
274
        if ($nsSepLast === false) {
275
            return $fqn;
276
        }
277
        $ns = substr($fqn, 0, $nsSepLast);
278

    
279
        return $ns !== '' ? $ns : null;
280
    }
281

    
282
    public function addClass(PhpClass $class)
283
    {
284
        $this->classes[$class->getName()] = $class;
285
    }
286

    
287
    public function getClass($className)
288
    {
289
        if (isset($this->classes[$className])) {
290
            return $this->classes[$className];
291
        }
292
    }
293

    
294
    public function getClasses()
295
    {
296
        return array_values($this->classes);
297
    }
298

    
299
    public function getIterator()
300
    {
301
        return new \ArrayIterator($this->getClasses());
302
    }
303

    
304
    public function getUseDirectives()
305
    {
306
        return $this->useDirectives;
307
    }
308

    
309
    public function getPhpCode()
310
    {
311
        return "namespace $this->namespace;\n";
312
    }
313

    
314
    public function __toString()
315
    {
316
        return $this->namespace;
317
    }
318
}
319

    
320
class PhpUseDirectives implements Countable, IteratorAggregate
321
{
322
    private $use;
323
    private $aliases;
324
    private $reverseAliases;
325
    private $namespace;
326

    
327
    public function __construct(PhpNamespace $namespace)
328
    {
329
        $this->namespace = $namespace;
330
        $this->use = array();
331
        $this->aliases = array();
332
        $this->reverseAliases = array();
333
    }
334

    
335
    public function add($use, $as = null)
336
    {
337
        if (in_array($use, $this->use)) {
338
            return;
339
        }
340

    
341
        $rename = null;
342
        $this->use[] = $use;
343
        $aliasedClassName = $as ?: PhpClass::extractName($use);
344

    
345
        if (isset($this->aliases[$aliasedClassName])) {
346
            $parentNs = $this->getParentNamespace();
347

    
348
            if ($parentNs && false !== $pos = strrpos($parentNs, '\\')) {
349
                $parentNs = substr($parentNs, $pos);
350
            }
351

    
352
            $newAlias = "{$parentNs}_{$aliasedClassName}";
353
            $rename = (object) array(
354
                'namespace' => $this->namespace,
355
                'from' => $aliasedClassName,
356
                'to' => $newAlias,
357
            );
358

    
359
            $this->aliases[$newAlias] = $use;
360
            $as = $newAlias;
361
        } else {
362
            $this->aliases[$aliasedClassName] = $use;
363
        }
364

    
365
        if ($as !== null) {
366
            $this->reverseAliases[$use] = $as;
367
        }
368

    
369
        return $rename;
370
    }
371

    
372
    public function getList()
373
    {
374
        return $this->use;
375
    }
376

    
377
    public function getIterator()
378
    {
379
        return new \ArrayIterator($this->getList());
380
    }
381

    
382
    public function getPhpCode()
383
    {
384
        $reverseAliases = $this->reverseAliases;
385

    
386
        $reducer = function ($str, $use) use ($reverseAliases) {
387
            if (isset($reverseAliases[$use])) {
388
                return $str .= "use $use as {$reverseAliases[$use]};\n";
389
            } else {
390
                return $str .= "use $use;\n";
391
            }
392
        };
393

    
394
        return array_reduce($this->getList(), $reducer, '');
395
    }
396

    
397
    public function getNamespace()
398
    {
399
        return $this->namespace;
400
    }
401

    
402
    public function getParentNamespace()
403
    {
404
        if (false !== $pos = strrpos($this->namespace, '\\')) {
405
            return substr($this->namespace, 0, $pos);
406
        }
407

    
408
        return '';
409
    }
410

    
411
    public function getFQN($className)
412
    {
413
        if (($nsSepFirst = strpos($className, '\\')) === false) {
414
            if (isset($this->aliases[$className])) {
415
                return $this->aliases[$className];
416
            }
417

    
418
            return (string)$this->getNamespace() . "\\$className";
419
        }
420

    
421
        if ($nsSepFirst != 0) {
422
            throw new InvalidArgumentException("Partially qualified names are not supported");
423
        }
424

    
425
        return $className;
426
    }
427

    
428
    public function count()
429
    {
430
        return count($this->use);
431
    }
432
}
433

    
434
class PhpClass
435
{
436
    const LICENSE_HEADER = <<<LICENSE
437
/*
438
 * This file is part of the Predis package.
439
 *
440
 * (c) Daniele Alessandri <suppakilla@gmail.com>
441
 *
442
 * For the full copyright and license information, please view the LICENSE
443
 * file that was distributed with this source code.
444
 */
445
LICENSE;
446

    
447
    private $namespace;
448
    private $file;
449
    private $body;
450
    private $implements;
451
    private $extends;
452
    private $name;
453

    
454
    public function __construct(PhpNamespace $namespace, SplFileInfo $classFile)
455
    {
456
        $this->namespace = $namespace;
457
        $this->file = $classFile;
458
        $this->implements = array();
459
        $this->extends = array();
460

    
461
        $this->extractData();
462
        $namespace->addClass($this);
463
    }
464

    
465
    public static function extractName($fqn)
466
    {
467
        $nsSepLast = strrpos($fqn, '\\');
468
        if ($nsSepLast === false) {
469
            return $fqn;
470
        }
471

    
472
        return substr($fqn, $nsSepLast + 1);
473
    }
474

    
475
    private function extractData()
476
    {
477
        $renames = array();
478
        $useDirectives = $this->getNamespace()->getUseDirectives();
479

    
480
        $useExtractor = function ($m) use ($useDirectives, &$renames) {
481
            array_shift($m);
482

    
483
            if (isset($m[1])) {
484
                $m[1] = str_replace(" as ", '', $m[1]);
485
            }
486

    
487
            if ($rename = call_user_func_array(array($useDirectives, 'add'), $m)) {
488
                $renames[] = $rename;
489
            }
490
        };
491

    
492
        $classBuffer = stream_get_contents(fopen($this->getFile()->getPathname(), 'r'));
493

    
494
        $classBuffer = str_replace(self::LICENSE_HEADER, '', $classBuffer);
495

    
496
        $classBuffer = preg_replace('/<\?php\s?\\n\s?/', '', $classBuffer);
497
        $classBuffer = preg_replace('/\s?\?>\n?/ms', '', $classBuffer);
498
        $classBuffer = preg_replace('/namespace\s+[\w\d_\\\\]+;\s?/', '', $classBuffer);
499
        $classBuffer = preg_replace_callback('/use\s+([\w\d_\\\\]+)(\s+as\s+.*)?;\s?\n?/', $useExtractor, $classBuffer);
500

    
501
        foreach ($renames as $rename) {
502
            $classBuffer = str_replace($rename->from, $rename->to, $classBuffer);
503
        }
504

    
505
        $this->body = trim($classBuffer);
506

    
507
        $this->extractHierarchy();
508
    }
509

    
510
    private function extractHierarchy()
511
    {
512
        $implements = array();
513
        $extends =  array();
514

    
515
        $extractor = function ($iterator, $callback) {
516
            $className = '';
517
            $iterator->seek($iterator->key() + 1);
518

    
519
            while ($iterator->valid()) {
520
                $token = $iterator->current();
521

    
522
                if (is_string($token)) {
523
                    if (preg_match('/\s?,\s?/', $token)) {
524
                        $callback(trim($className));
525
                        $className = '';
526
                    } else if ($token == '{') {
527
                        $callback(trim($className));
528
                        return;
529
                    }
530
                }
531

    
532
                switch ($token[0]) {
533
                    case T_NS_SEPARATOR:
534
                        $className .= '\\';
535
                        break;
536

    
537
                    case T_STRING:
538
                        $className .= $token[1];
539
                        break;
540

    
541
                    case T_IMPLEMENTS:
542
                    case T_EXTENDS:
543
                        $callback(trim($className));
544
                        $iterator->seek($iterator->key() - 1);
545
                        return;
546
                }
547

    
548
                $iterator->next();
549
            }
550
        };
551

    
552
        $tokens = token_get_all("<?php\n" . trim($this->getPhpCode()));
553
        $iterator = new ArrayIterator($tokens);
554

    
555
        while ($iterator->valid()) {
556
            $token = $iterator->current();
557
            if (is_string($token)) {
558
                $iterator->next();
559
                continue;
560
            }
561

    
562
            switch ($token[0]) {
563
                case T_CLASS:
564
                case T_INTERFACE:
565
                    $iterator->seek($iterator->key() + 2);
566
                    $tk = $iterator->current();
567
                    $this->name = $tk[1];
568
                    break;
569

    
570
                case T_IMPLEMENTS:
571
                    $extractor($iterator, function ($fqn) use (&$implements) {
572
                        $implements[] = $fqn;
573
                    });
574
                    break;
575

    
576
                case T_EXTENDS:
577
                    $extractor($iterator, function ($fqn) use (&$extends) {
578
                        $extends[] = $fqn;
579
                    });
580
                    break;
581
            }
582

    
583
            $iterator->next();
584
        }
585

    
586
        $this->implements = $this->guessFQN($implements);
587
        $this->extends = $this->guessFQN($extends);
588
    }
589

    
590
    public function guessFQN($classes)
591
    {
592
        $useDirectives = $this->getNamespace()->getUseDirectives();
593
        return array_map(array($useDirectives, 'getFQN'), $classes);
594
    }
595

    
596
    public function getImplementedInterfaces($all = false)
597
    {
598
        if ($all) {
599
            return $this->implements;
600
        }
601

    
602
        return array_filter(
603
            $this->implements,
604
            function ($cn) { return strpos($cn, 'Predis\\') === 0; }
605
        );
606
    }
607

    
608
    public function getExtendedClasses($all = false)
609
    {
610
        if ($all) {
611
            return $this->extemds;
612
        }
613

    
614
        return array_filter(
615
            $this->extends,
616
            function ($cn) { return strpos($cn, 'Predis\\') === 0; }
617
        );
618
    }
619

    
620
    public function getDependencies($all = false)
621
    {
622
        return array_merge(
623
            $this->getImplementedInterfaces($all),
624
            $this->getExtendedClasses($all)
625
        );
626
    }
627

    
628
    public function getNamespace()
629
    {
630
        return $this->namespace;
631
    }
632

    
633
    public function getFile()
634
    {
635
        return $this->file;
636
    }
637

    
638
    public function getName()
639
    {
640
        return $this->name;
641
    }
642

    
643
    public function getFQN()
644
    {
645
        return (string)$this->getNamespace() . '\\' . $this->name;
646
    }
647

    
648
    public function getPhpCode()
649
    {
650
        return $this->body;
651
    }
652

    
653
    public function __toString()
654
    {
655
        return "class " . $this->getName() . '{ ... }';
656
    }
657
}
658

    
659
/* -------------------------------------------------------------------------- */
660

    
661
$options = CommandLine::getOptions();
662
$predisFile = PredisFile::from($options['source'], $options['exclude']);
663
$predisFile->saveTo($options['output']);
(4-4/4)