GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

FullProjectBuildLargeTest   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 880
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 479
dl 0
loc 880
rs 4.5599
c 2
b 0
f 0
wmc 58

23 Methods

Rating   Name   Duplication   Size   Complexity  
A generateEmbeddable() 0 8 1
A execBash() 0 33 3
A setField() 0 11 1
A testRunTests() 0 33 2
A generateEntity() 0 9 1
A getFieldFqns() 0 11 3
A generateField() 0 21 3
A generateRelations() 0 4 2
B initComposerAndInstall() 0 80 5
A initRebuildFile() 0 55 2
A assertWeCheckAllPossibleRelationTypes() 0 20 4
A setRelation() 0 12 1
A setEmbeddable() 0 8 1
A setFields() 0 5 3
B setup() 0 96 6
A assertNoUncommitedChanges() 0 11 3
A removeUnusedRelations() 0 8 1
A setTheDuplicateNamedFields() 0 5 3
A setupGeneratedDb() 0 39 2
A generateFields() 0 12 4
A execDoctrine() 0 17 3
A addToRebuildFile() 0 12 2
A generateEntities() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like FullProjectBuildLargeTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\Tests\Large\A;
6
7
use Doctrine\Common\Inflector\Inflector;
8
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Command\GenerateFieldCommand;
9
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\AbstractGenerator;
10
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
11
use EdmondsCommerce\DoctrineStaticMeta\Config;
12
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
13
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\Financial\MoneyEmbeddable;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\Geo\AddressEmbeddable;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\Identity\FullNameEmbeddable;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Traits\Financial\HasMoneyEmbeddableTrait;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Traits\Geo\HasAddressEmbeddableTrait;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Traits\Identity\HasFullNameEmbeddableTrait;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Traits\String\BusinessIdentifierCodeFieldTrait;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Traits\String\NullableStringFieldTrait;
21
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
22
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
23
use EdmondsCommerce\DoctrineStaticMeta\Tests\Assets\AbstractLargeTest;
24
use EdmondsCommerce\DoctrineStaticMeta\Tests\Assets\AbstractTest;
25
use Exception;
26
use Psr\Container\ContainerExceptionInterface;
27
use Psr\Container\NotFoundExceptionInterface;
28
use RuntimeException;
29
30
use function array_diff;
31
use function array_keys;
32
use function ksort;
33
34
/**
35
 * Class GeneratedCodeTest
36
 *
37
 * @package EdmondsCommerce\DoctrineStaticMeta\GeneratedCode
38
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
39
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
40
 * @coversNothing
41
 */
42
class FullProjectBuildLargeTest extends AbstractLargeTest
43
{
44
    public const TEST_ENTITY_NAMESPACE_BASE = self::TEST_PROJECT_ROOT_NAMESPACE
45
                                              . '\\' . AbstractGenerator::ENTITIES_FOLDER_NAME;
46
47
    public const TEST_FIELD_TRAIT_NAMESPACE = self::TEST_FIELD_NAMESPACE_BASE . '\\Traits\\';
48
49
    public const TEST_ENTITY_PERSON                      = self::TEST_ENTITY_NAMESPACE_BASE . '\\Person';
50
    public const TEST_ENTITY_ADDRESS                     = self::TEST_ENTITY_NAMESPACE_BASE . '\\Attributes\\Address';
51
    public const TEST_ENTITY_EMAIL                       = self::TEST_ENTITY_NAMESPACE_BASE . '\\Attributes\\Email';
52
    public const TEST_ENTITY_COMPANY                     = self::TEST_ENTITY_NAMESPACE_BASE . '\\Company';
53
    public const TEST_ENTITY_DIRECTOR                    = self::TEST_ENTITY_NAMESPACE_BASE . '\\Company\\Director';
54
    public const TEST_ENTITY_ORDER                       = self::TEST_ENTITY_NAMESPACE_BASE . '\\Order';
55
    public const TEST_ENTITY_ORDER_ADDRESS               = self::TEST_ENTITY_NAMESPACE_BASE . '\\Order\\Address';
56
    public const TEST_ENTITY_NAME_SPACING_SOME_CLIENT    = self::TEST_ENTITY_NAMESPACE_BASE . '\\Some\\Client';
57
    public const TEST_ENTITY_NAME_SPACING_ANOTHER_CLIENT = self::TEST_ENTITY_NAMESPACE_BASE
58
                                                           . '\\Another\\Deeply\\Nested\\Client';
59
    public const TEST_ENTITY_PRODUCT                     = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product';
60
    public const TEST_ENTITY_PRODUCT_STOCK               = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product\\Stock';
61
    public const TEST_ENTITY_PRODUCT_REVIEW              = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product\\Review';
62
    public const TEST_ENTITY_PRODUCT_DATA                = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product\\Data';
63
    public const TEST_ENTITY_PRODUCT_DATA_ITEM           = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product\\Data\\Item';
64
    public const TEST_ENTITY_PRODUCT_DATA_ITEM_THING     = self::TEST_ENTITY_NAMESPACE_BASE .
65
                                                           '\\Product\\Data\\Item\\Thing';
66
    public const TEST_ENTITY_PRODUCT_DATA_ITEM_FOO       = self::TEST_ENTITY_NAMESPACE_BASE .
67
                                                           '\\Product\\Data\\Item\\Foo';
68
    public const TEST_ENTITY_PRODUCT_EXTRA               = self::TEST_ENTITY_NAMESPACE_BASE . '\\Product\\Extra';
69
70
    public const TEST_ENTITIES = [
71
        self::TEST_ENTITY_PERSON,
72
        self::TEST_ENTITY_ADDRESS,
73
        self::TEST_ENTITY_EMAIL,
74
        self::TEST_ENTITY_COMPANY,
75
        self::TEST_ENTITY_DIRECTOR,
76
        self::TEST_ENTITY_ORDER,
77
        self::TEST_ENTITY_ORDER_ADDRESS,
78
        self::TEST_ENTITY_NAME_SPACING_SOME_CLIENT,
79
        self::TEST_ENTITY_NAME_SPACING_ANOTHER_CLIENT,
80
        self::TEST_ENTITY_PRODUCT,
81
        self::TEST_ENTITY_PRODUCT_STOCK,
82
        self::TEST_ENTITY_PRODUCT_REVIEW,
83
        self::TEST_ENTITY_PRODUCT_DATA,
84
        self::TEST_ENTITY_PRODUCT_DATA_ITEM,
85
        self::TEST_ENTITY_PRODUCT_DATA_ITEM_THING,
86
        self::TEST_ENTITY_PRODUCT_DATA_ITEM_FOO,
87
        self::TEST_ENTITY_PRODUCT_EXTRA,
88
    ];
89
90
    public const TEST_RELATIONS = [
91
        [
92
            self::TEST_ENTITY_PERSON,
93
            RelationsGenerator::HAS_UNIDIRECTIONAL_MANY_TO_ONE,
94
            self::TEST_ENTITY_ADDRESS,
95
            true,
96
        ],
97
        [
98
            self::TEST_ENTITY_PERSON,
99
            RelationsGenerator::HAS_ONE_TO_MANY,
100
            self::TEST_ENTITY_EMAIL,
101
            true,
102
        ],
103
        [
104
            self::TEST_ENTITY_COMPANY,
105
            RelationsGenerator::HAS_MANY_TO_MANY,
106
            self::TEST_ENTITY_DIRECTOR,
107
            false,
108
        ],
109
        [
110
            self::TEST_ENTITY_COMPANY,
111
            RelationsGenerator::HAS_ONE_TO_MANY,
112
            self::TEST_ENTITY_ADDRESS,
113
            false,
114
        ],
115
        [
116
            self::TEST_ENTITY_COMPANY,
117
            RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
118
            self::TEST_ENTITY_EMAIL,
119
            true,
120
        ],
121
        [
122
            self::TEST_ENTITY_DIRECTOR,
123
            RelationsGenerator::HAS_ONE_TO_ONE,
124
            self::TEST_ENTITY_PERSON,
125
            false,
126
        ],
127
        [
128
            self::TEST_ENTITY_ORDER,
129
            RelationsGenerator::HAS_MANY_TO_ONE,
130
            self::TEST_ENTITY_PERSON,
131
            true,
132
        ],
133
        [
134
            self::TEST_ENTITY_ORDER,
135
            RelationsGenerator::HAS_ONE_TO_MANY,
136
            self::TEST_ENTITY_ORDER_ADDRESS,
137
            false,
138
        ],
139
        [
140
            self::TEST_ENTITY_ORDER_ADDRESS,
141
            RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_ONE,
142
            self::TEST_ENTITY_ADDRESS,
143
            true,
144
        ],
145
        [
146
            self::TEST_ENTITY_COMPANY,
147
            RelationsGenerator::HAS_ONE_TO_ONE,
148
            self::TEST_ENTITY_NAME_SPACING_SOME_CLIENT,
149
            false,
150
        ],
151
        [
152
            self::TEST_ENTITY_COMPANY,
153
            RelationsGenerator::HAS_ONE_TO_ONE,
154
            self::TEST_ENTITY_NAME_SPACING_ANOTHER_CLIENT,
155
            true,
156
        ],
157
        [
158
            self::TEST_ENTITY_PRODUCT,
159
            RelationsGenerator::HAS_REQUIRED_MANY_TO_MANY,
160
            self::TEST_ENTITY_PRODUCT_STOCK,
161
            true,
162
        ],
163
        [
164
            self::TEST_ENTITY_PRODUCT_REVIEW,
165
            RelationsGenerator::HAS_REQUIRED_MANY_TO_ONE,
166
            self::TEST_ENTITY_PRODUCT,
167
            false,
168
        ],
169
        [
170
            self::TEST_ENTITY_PRODUCT,
171
            RelationsGenerator::HAS_REQUIRED_ONE_TO_ONE,
172
            self::TEST_ENTITY_PRODUCT_EXTRA,
173
            true,
174
        ],
175
        [
176
            self::TEST_ENTITY_PRODUCT,
177
            RelationsGenerator::HAS_ONE_TO_ONE,
178
            self::TEST_ENTITY_PRODUCT_DATA,
179
            true,
180
        ],
181
        [
182
            self::TEST_ENTITY_PRODUCT_DATA_ITEM,
183
            RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE,
184
            self::TEST_ENTITY_PRODUCT_DATA,
185
            false,
186
        ],
187
        [
188
            self::TEST_ENTITY_PRODUCT_DATA_ITEM_THING,
189
            RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE,
190
            self::TEST_ENTITY_PRODUCT_DATA,
191
            false,
192
        ],
193
        [
194
            self::TEST_ENTITY_PRODUCT_DATA_ITEM_FOO,
195
            RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY,
196
            self::TEST_ENTITY_PRODUCT_DATA,
197
            false,
198
        ],
199
        [
200
            self::TEST_ENTITY_PRODUCT_DATA_ITEM_FOO,
201
            RelationsGenerator::HAS_REQUIRED_ONE_TO_MANY,
202
            self::TEST_ENTITY_PERSON,
203
            false,
204
        ],
205
    ];
206
207
    public const TEST_FIELD_NAMESPACE_BASE = self::TEST_PROJECT_ROOT_NAMESPACE . '\\Entity\\Fields';
208
209
    public const UNIQUEABLE_FIELD_TYPES = [
210
        MappingHelper::TYPE_INTEGER,
211
        MappingHelper::TYPE_STRING,
212
    ];
213
214
    public const DUPLICATE_SHORT_NAME_FIELDS = [
215
        [self::TEST_FIELD_NAMESPACE_BASE . '\\Traits\\Something\\FooFieldTrait', NullableStringFieldTrait::class],
216
        [
217
            self::TEST_FIELD_NAMESPACE_BASE . '\\Traits\\Otherthing\\FooFieldTrait',
218
            BusinessIdentifierCodeFieldTrait::class,
219
        ],
220
    ];
221
222
    public const EMBEDDABLE_TRAIT_BASE = self::TEST_PROJECT_ROOT_NAMESPACE . '\\Entity\\Embeddable\\Traits';
223
224
    public const TEST_EMBEDDABLES          = [
225
        [
226
            MoneyEmbeddable::class,
227
            self::EMBEDDABLE_TRAIT_BASE . '\\Financial\\HasPriceEmbeddableTrait',
228
            'PriceEmbeddable',
229
        ],
230
        [
231
            AddressEmbeddable::class,
232
            self::EMBEDDABLE_TRAIT_BASE . '\\Geo\\HasHeadOfficeEmbeddableTrait',
233
            'HeadOfficeEmbeddable',
234
        ],
235
        [
236
            FullNameEmbeddable::class,
237
            self::EMBEDDABLE_TRAIT_BASE . '\\Identity\\HasPersonEmbeddableTrait',
238
            'PersonEmbeddable',
239
        ],
240
    ];
241
    public const BASH_PHPNOXDEBUG_FUNCTION = <<<'BASH'
242
function phpNoXdebug {
243
    debugMode="off"
244
    if [[ "$-" == *x* ]]
245
    then
246
        debugMode='on'
247
    fi
248
    if [[ "$debugMode" == "on" ]]
249
    then
250
        set +x
251
    fi
252
    local returnCode;
253
    local temporaryPath="$(mktemp -t php.XXXX).ini"
254
    # Using awk to ensure that files ending without newlines do not lead to configuration error
255
    /usr/bin/php -i | grep "\.ini" | grep -o -e '\(/[a-z0-9._-]\+\)\+\.ini' | grep -v xdebug \
256
        | xargs awk 'FNR==1{print ""}1' > "$temporaryPath"
257
    #Run PHP with temp config with no xdebug, display errors on stderr
258
    set +e
259
    /usr/bin/php -n -c "$temporaryPath" "$@"    
260
    returnCode=$?
261
    set -e
262
    rm -f "$temporaryPath"
263
    if [[ "$debugMode" == "on" ]]
264
    then
265
        set -x
266
    fi
267
    return ${returnCode};    
268
}
269
270
BASH;
271
    /**
272
     * @var string
273
     */
274
    private $workDir;
275
276
    /**
277
     * @throws Exception
278
     * @throws ContainerExceptionInterface
279
     * @throws NotFoundExceptionInterface
280
     * @SuppressWarnings(PHPMD.Superglobals)
281
     * @SuppressWarnings(PHPMD.StaticAccess)
282
     */
283
    public function setup()
284
    {
285
        if ($this->isQuickTests()) {
286
            return;
287
        }
288
        $this->assertNoUncommitedChanges();
289
        $this->assertWeCheckAllPossibleRelationTypes();
290
        $this->workDir      = $this->isTravis() ?
291
            AbstractTest::VAR_PATH . '/GeneratedCodeTest'
292
            : sys_get_temp_dir() . '/dsm/test-project';
293
        $this->entitiesPath = $this->workDir . '/src/Entities';
294
        $this->getFileSystem()->mkdir($this->workDir);
295
        $this->emptyDirectory($this->workDir . '');
296
        $this->getFileSystem()->mkdir($this->entitiesPath);
297
        $this->setupContainer($this->entitiesPath);
298
        $this->initRebuildFile();
299
        $this->setupGeneratedDb();
300
        $this->initComposerAndInstall();
301
        $fileSystem = $this->getFileSystem();
302
        $fileSystem->mkdir(
303
            [
304
                $this->workDir . '/tests/',
305
                $this->workDir . '/cache/Proxies',
306
                $this->workDir . '/cache/qa',
307
                $this->workDir . '/qaConfig',
308
                $this->workDir . '/var/qa/phpunit_logs/',
309
            ]
310
        );
311
        file_put_contents(
312
            $this->workDir . '/qaConfig/phpunit.xml',
313
            <<<XML
314
<phpunit
315
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
316
        xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/3.7/phpunit.xsd"
317
        cacheTokens="false"
318
        colors="true"
319
        verbose="true"
320
        bootstrap="../tests/bootstrap.php"
321
        printerClass="\EdmondsCommerce\PHPQA\PHPUnit\TestDox\CliTestDoxPrinter"
322
        cacheResult="true"
323
        cacheResultFile="../var/qa/.phpunit.result.cache"
324
        executionOrder="depends,defects"
325
>
326
    <testsuites>
327
        <testsuite name="tests">
328
            <directory suffix="Test.php">../tests/</directory>
329
        </testsuite>
330
    </testsuites>
331
</phpunit>
332
XML
333
        );
334
        $fileSystem->symlink($this->workDir . '/qaConfig/phpunit.xml', $this->workDir . '/phpunit.xml');
335
336
        $fileSystem->mirror(
337
            __DIR__ . '/../../../qaConfig/codingStandard',
338
            $this->workDir . '/qaConfig/codingStandard'
339
        );
340
        $fileSystem->copy(
341
            __DIR__ . '/../../../qaConfig/qaConfig.inc.bash',
342
            $this->workDir . '/qaConfig/qaConfig.inc.bash'
343
        );
344
        $fileSystem->copy(__DIR__ . '/../../../cli-config.php', $this->workDir . '/cli-config.php');
345
        file_put_contents($this->workDir . '/README.md', '#Generated Code');
346
347
        $entities = $this->generateEntities();
348
        $this->generateRelations();
349
        $this->generateFields();
350
        $this->setFields(
351
            $entities,
352
            $this->getFieldFqns()
353
        );
354
        $this->setTheDuplicateNamedFields($entities);
355
        foreach ($entities as $entityFqn) {
356
            foreach (
357
                [
358
                         HasMoneyEmbeddableTrait::class,
359
                         HasFullNameEmbeddableTrait::class,
360
                         HasAddressEmbeddableTrait::class,
361
                     ] as $embeddableTraitFqn
362
            ) {
363
                $this->setEmbeddable($entityFqn, $embeddableTraitFqn);
364
            }
365
            foreach (self::TEST_EMBEDDABLES as $embeddable) {
366
                [$archetypeObject, $traitFqn, $className] = $embeddable;
367
                $this->generateEmbeddable(
368
                    '\\' . $archetypeObject,
369
                    $className
370
                );
371
                $this->setEmbeddable($entityFqn, $traitFqn);
372
            }
373
        }
374
        $this->removeUnusedRelations();
375
        $this->execDoctrine('dsm:finalise:build');
376
        $this->execDoctrine('orm:clear-cache:metadata');
377
        $this->execDoctrine('orm:schema-tool:update --force');
378
        $this->execDoctrine('orm:validate-schema');
379
    }
380
381
    /**
382
     * We need to check for uncommited changes in the main project. If there are, then the generated code tests will
383
     * not get them as it works by cloning this repo via the filesystem
384
     */
385
    protected function assertNoUncommitedChanges(): void
386
    {
387
        if ($this->isTravis()) {
388
            return;
389
        }
390
        exec("git status | grep -E 'nothing to commit, working .*? clean' ", $output, $exitCode);
391
        if (0 !== $exitCode) {
392
            $this->markTestSkipped(
393
                'uncommitted changes detected in this project, '
394
                . 'there is no point running the generated code test as it will not have your uncommitted changes.'
395
                . "\n\n" . implode("\n", $output)
396
            );
397
        }
398
    }
399
400
    protected function assertWeCheckAllPossibleRelationTypes(): void
401
    {
402
        $included = $toTest = [];
403
        foreach (RelationsGenerator::HAS_TYPES as $hasType) {
404
            if (false !== \ts\stringContains($hasType, RelationsGenerator::PREFIX_INVERSE)) {
405
                continue;
406
            }
407
            $toTest[$hasType] = true;
408
        }
409
        ksort($toTest);
410
        foreach (self::TEST_RELATIONS as $relation) {
411
            $included[$relation[1]] = true;
412
        }
413
        ksort($included);
414
        $missing = array_diff(array_keys($toTest), array_keys($included));
415
        self::assertEmpty(
416
            $missing,
417
            'We are not testing all relation types - '
418
            . 'these ones have not been included: '
419
            . print_r($missing, true)
420
        );
421
    }
422
423
    protected function initRebuildFile(): void
424
    {
425
        $bash =
426
            <<<'BASH'
427
#!/usr/bin/env bash
428
readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )";
429
cd "$DIR";
430
set -e
431
set -u
432
set -o pipefail
433
standardIFS="$IFS"
434
IFS=$'\n\t'
435
echo "
436
===========================================
437
$(hostname) $0 $@
438
===========================================
439
"
440
# Error Handling
441
backTraceExit () {
442
    local err=$?
443
    set +o xtrace
444
    local code="${1:-1}"
445
    printf "\n\nError in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}. '${BASH_COMMAND}'\n\n exited with status: \n\n$err\n\n"
446
    # Print out the stack trace described by $function_stack
447
    if [ ${#FUNCNAME[@]} -gt 2 ]
448
    then
449
        echo "Call tree:"
450
        for ((i=1;i<${#FUNCNAME[@]}-1;i++))
451
        do
452
            echo " $i: ${BASH_SOURCE[$i+1]}:${BASH_LINENO[$i]} ${FUNCNAME[$i]}(...)"
453
        done
454
    fi
455
    echo "Exiting with status ${code}"
456
    exit "${code}"
457
}
458
trap 'backTraceExit' ERR
459
set -o errtrace
460
# Error Handling Ends
461
462
echo "clearing out generated code and cache"
463
rm -rf src/* tests/* cache/*
464
465
echo "preparing empty Entities directory"
466
mkdir src/Entities
467
468
echo "making sure we have the latest version of code"
469
(cd vendor/edmondscommerce/doctrine-static-meta && git pull)
470
471
BASH;
472
        if (!$this->isTravis()) {
473
            $bash .= self::BASH_PHPNOXDEBUG_FUNCTION;
474
        }
475
        file_put_contents(
476
            $this->workDir . '/rebuild.bash',
477
            "\n\n" . $bash
478
        );
479
    }
480
481
    /**
482
     * @return string Generated Database Name
483
     * @throws Exception
484
     * @throws ContainerExceptionInterface
485
     * @throws NotFoundExceptionInterface
486
     */
487
    protected function setupGeneratedDb(): string
488
    {
489
        $dbHost = $this->container->get(Config::class)->get(ConfigInterface::PARAM_DB_HOST);
490
        $dbUser = $this->container->get(Config::class)->get(ConfigInterface::PARAM_DB_USER);
491
        $dbPass = $this->container->get(Config::class)->get(ConfigInterface::PARAM_DB_PASS);
492
        $dbName = $this->container->get(Config::class)->get(ConfigInterface::PARAM_DB_NAME);
493
        $link   = mysqli_connect($dbHost, $dbUser, $dbPass);
494
        if (!$link) {
495
            throw new DoctrineStaticMetaException('Failed getting connection in ' . __METHOD__);
496
        }
497
        $generatedDbName = $dbName . '_generated';
498
        mysqli_query($link, "DROP DATABASE IF EXISTS $generatedDbName");
499
        mysqli_query(
500
            $link,
501
            "CREATE DATABASE $generatedDbName 
502
        CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci"
503
        );
504
        mysqli_close($link);
505
506
        $rebuildBash = <<<BASH
507
echo "Dropping and creating the DB $generatedDbName"        
508
mysql -u $dbUser -p$dbPass -h $dbHost -e "DROP DATABASE IF EXISTS $generatedDbName";
509
mysql -u $dbUser -p$dbPass -h $dbHost -e "CREATE DATABASE $generatedDbName 
510
CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci";
511
BASH;
512
        $this->addToRebuildFile($rebuildBash);
513
        file_put_contents(
514
            $this->workDir . '/.env',
515
            <<<EOF
516
export dbUser="{$dbUser}"
517
export dbPass="{$dbPass}"
518
export dbHost="{$dbHost}"
519
export dbName="$generatedDbName"
520
export devMode=0
521
export dbDebug=0
522
EOF
523
        );
524
525
        return $generatedDbName;
526
    }
527
528
    /**
529
     * @param string $bash
530
     *
531
     * @return bool
532
     * @throws Exception
533
     */
534
    protected function addToRebuildFile(string $bash): bool
535
    {
536
        $result = file_put_contents(
537
            $this->workDir . '/rebuild.bash',
538
            "\n\n" . $bash . "\n\n",
539
            FILE_APPEND
540
        );
541
        if (!$result) {
542
            throw new RuntimeException('Failed writing to rebuild file');
543
        }
544
545
        return true;
546
    }
547
548
    protected function initComposerAndInstall(): void
549
    {
550
        $relativePath = __DIR__ . '/../../../../doctrine-static-meta/';
551
        $vcsPath      = realpath($relativePath);
552
        if (false === $vcsPath) {
553
            throw new RuntimeException('Failed getting realpath to main project at ' . $relativePath);
554
        }
555
        $namespace    = str_replace('\\', '\\\\', self::TEST_PROJECT_ROOT_NAMESPACE);
556
        $composerJson = <<<JSON
557
{
558
  "require": {
559
    "edmondscommerce/doctrine-static-meta": "dev-%s",
560
    "php": ">=7.2"
561
  },
562
  "repositories": [
563
    {
564
      "type": "vcs",
565
      "url": "%s"
566
    },
567
    {
568
      "type": "vcs",
569
      "url": "https://github.com/edmondscommerce/Faker.git"
570
    }
571
  ],
572
  "minimum-stability": "stable",
573
  "require-dev": {
574
    "fzaninotto/faker": "dev-dsm-patches@dev",
575
    "edmondscommerce/phpqa": "~1",
576
    "gossi/php-code-generator": "^0.5.0"
577
  },
578
  "autoload": {
579
    "psr-4": {
580
      "$namespace\\\\": [
581
        "src/"
582
      ]
583
    }
584
  },
585
  "autoload-dev": {
586
    "psr-4": {
587
      "$namespace\\\\": [
588
        "tests/"
589
      ]
590
    }
591
  },
592
  "config": {
593
    "bin-dir": "bin",
594
    "preferred-install": {
595
      "edmondscommerce/*": "source",
596
      "fzaninotto/faker": "source",
597
      "*": "dist"
598
    },
599
    "optimize-autoloader": true
600
  }
601
}
602
JSON;
603
604
        $gitCurrentBranchName = trim(shell_exec("git branch | grep '*' | cut -d ' ' -f 2-"));
605
        echo __LINE__ . ": \$gitCurrentBranchName $gitCurrentBranchName";
606
        if (\ts\stringContains($gitCurrentBranchName, 'HEAD detached at')) {
607
            $gitCurrentBranchName = trim(str_replace('HEAD detached at', '', $gitCurrentBranchName), " \t\n\r\0\x0B()");
608
            echo __LINE__ . ": \$gitCurrentBranchName $gitCurrentBranchName";
609
        } elseif (\ts\stringContains($gitCurrentBranchName, 'detached from')) {
610
            $gitCurrentBranchName = trim(str_replace('detached from', '', $gitCurrentBranchName), " \t\n\r\0\x0B()");
611
            echo __LINE__ . ": \$gitCurrentBranchName $gitCurrentBranchName";
612
        }
613
        file_put_contents(
614
            $this->workDir . '/composer.json',
615
            sprintf($composerJson, $gitCurrentBranchName, $vcsPath)
616
        );
617
618
        $phpCmd   = $this->isTravis() ? 'php' : 'phpNoXdebug';
619
        $bashCmds = <<<BASH
620
           
621
$phpCmd $(which composer) install \
622
    --prefer-dist
623
624
$phpCmd $(which composer) dump-autoload --optimize
625
626
BASH;
627
        $this->execBash($bashCmds);
628
    }
629
630
    /**
631
     * Runs bash with strict error handling and verbose logging
632
     *
633
     * Will ensure the phpNoXdebugFunction is available and will CD into the correct directory before running commands
634
     *
635
     * Asserts that the command returns with an exit code of 0
636
     *
637
     * Appends to the rebuild file allowing easy rerunning of the commmands in the test project
638
     *
639
     * @param string $bashCmds
640
     *
641
     * @throws Exception
642
     */
643
    protected function execBash(string $bashCmds): void
644
    {
645
        fwrite(STDERR, "\n\t# Executing:\n\t$bashCmds");
646
        $startTime = microtime(true);
647
648
        $fullCmds = '';
649
        if (!$this->isTravis()) {
650
            $fullCmds .= "\n" . self::BASH_PHPNOXDEBUG_FUNCTION . "\n\n";
651
        }
652
        $fullCmds .= "set -e;\n";
653
        $fullCmds .= "cd {$this->workDir};\n";
654
        #$fullCmds .= "exec 2>&1;\n";
655
        $fullCmds .= "$bashCmds\n";
656
657
        $output   = [];
658
        $exitCode = 0;
659
        exec($fullCmds, $output, $exitCode);
660
661
        if (0 !== $exitCode) {
662
            throw new RuntimeException(
663
                "Error running bash commands:\n\nOutput:\n----------\n\n"
664
                . implode("\n", $output)
665
                . "\n\nCommands:\n----------\n"
666
                . str_replace(
667
                    "\n",
668
                    "\n\t",
669
                    "\n$bashCmds"
670
                ) . "\n\n"
671
            );
672
        }
673
674
        $seconds = round(microtime(true) - $startTime, 2);
675
        fwrite(STDERR, "\n\t\t#Completed in $seconds seconds\n");
676
    }
677
678
    /**
679
     * Generate all test entities
680
     *
681
     * @return array
682
     */
683
    protected function generateEntities(): array
684
    {
685
        foreach (self::TEST_ENTITIES as $entityFqn) {
686
            $this->generateEntity($entityFqn);
687
        }
688
689
        return self::TEST_ENTITIES;
690
    }
691
692
    protected function generateEntity(string $entityFqn): void
693
    {
694
        $namespace   = self::TEST_PROJECT_ROOT_NAMESPACE;
695
        $doctrineCmd = <<<DOCTRINE
696
 dsm:generate:entity \
697
    --project-root-namespace="{$namespace}" \
698
    --entity-fully-qualified-name="{$entityFqn}"
699
DOCTRINE;
700
        $this->execDoctrine($doctrineCmd);
701
    }
702
703
    protected function execDoctrine(string $doctrineCmd): void
704
    {
705
        $phpCmd  = $this->isTravis() ? 'php' : 'phpNoXdebug';
706
        $bash    = <<<BASH
707
$phpCmd bin/doctrine $doctrineCmd    
708
BASH;
709
        $error   = false;
710
        $message = '';
711
        try {
712
            $this->execBash($bash);
713
        } catch (RuntimeException $e) {
714
            $this->addToRebuildFile("\n\n#The command below failed...\n\n");
715
            $error   = true;
716
            $message = $e->getMessage();
717
        }
718
        $this->addToRebuildFile($bash);
719
        self::assertFalse($error, $message);
720
    }
721
722
    /**
723
     * Generate all test relations
724
     *
725
     * @return void
726
     */
727
    protected function generateRelations(): void
728
    {
729
        foreach (self::TEST_RELATIONS as $relation) {
730
            $this->setRelation(...$relation);
0 ignored issues
show
Bug introduced by
The call to EdmondsCommerce\Doctrine...argeTest::setRelation() has too few arguments starting with type. ( Ignorable by Annotation )

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

730
            $this->/** @scrutinizer ignore-call */ 
731
                   setRelation(...$relation);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
731
        }
732
    }
733
734
    protected function setRelation(string $entity1, string $type, string $entity2, bool $required): void
735
    {
736
        $namespace = self::TEST_PROJECT_ROOT_NAMESPACE;
737
        $this->execDoctrine(
738
            <<<DOCTRINE
739
dsm:set:relation \
740
    --project-root-path="{$this->workDir}" \
741
    --project-root-namespace="{$namespace}" \
742
    --entity1="{$entity1}" \
743
    --hasType="{$type}" \
744
    --entity2="{$entity2}" \
745
    --required-relation="{$required}"
746
DOCTRINE
747
        );
748
    }
749
750
    /**
751
     * Generate one field per common type
752
     *
753
     * @return void
754
     */
755
    protected function generateFields(): void
756
    {
757
        foreach (MappingHelper::COMMON_TYPES as $type) {
758
            $fieldFqn = self::TEST_FIELD_TRAIT_NAMESPACE . '\\' . ucfirst($type);
759
            $this->generateField($fieldFqn, $type);
760
        }
761
        foreach (self::UNIQUEABLE_FIELD_TYPES as $uniqueableType) {
762
            $fieldFqn = self::TEST_FIELD_TRAIT_NAMESPACE . '\\Unique' . ucwords($uniqueableType);
763
            $this->generateField($fieldFqn, $uniqueableType, null, true);
764
        }
765
        foreach (self::DUPLICATE_SHORT_NAME_FIELDS as $duplicateShortName) {
766
            $this->generateField(...$duplicateShortName);
0 ignored issues
show
Bug introduced by
The call to EdmondsCommerce\Doctrine...geTest::generateField() has too few arguments starting with type. ( Ignorable by Annotation )

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

766
            $this->/** @scrutinizer ignore-call */ 
767
                   generateField(...$duplicateShortName);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
767
        }
768
    }
769
770
    /**
771
     * @param string     $propertyName
772
     * @param string     $type
773
     * @param mixed|null $default
774
     * @param bool       $isUnique
775
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
776
     */
777
    protected function generateField(
778
        string $propertyName,
779
        string $type,
780
        $default = null,
781
        bool $isUnique = false
782
    ): void {
783
        $namespace   = self::TEST_PROJECT_ROOT_NAMESPACE;
784
        $doctrineCmd = <<<DOCTRINE
785
 dsm:generate:field \
786
    --project-root-path="{$this->workDir}" \
787
    --project-root-namespace="{$namespace}" \
788
    --field-fully-qualified-name="{$propertyName}" \
789
    --field-property-doctrine-type="{$type}"
790
DOCTRINE;
791
        if (null !== $default) {
792
            $doctrineCmd .= ' --' . GenerateFieldCommand::OPT_DEFAULT_VALUE . '="' . $default . '"' . "\\\n";
793
        }
794
        if (true === $isUnique) {
795
            $doctrineCmd .= ' --' . GenerateFieldCommand::OPT_IS_UNIQUE . "\\\n";
796
        }
797
        $this->execDoctrine($doctrineCmd);
798
    }
799
800
    /**
801
     * Set each field type on each entity type
802
     *
803
     * @param array $entities
804
     * @param array $fields
805
     *
806
     * @return void
807
     */
808
    protected function setFields(array $entities, array $fields): void
809
    {
810
        foreach ($entities as $entityFqn) {
811
            foreach ($fields as $fieldFqn) {
812
                $this->setField($entityFqn, $fieldFqn);
813
            }
814
        }
815
    }
816
817
    protected function setField(string $entityFqn, string $fieldFqn): void
818
    {
819
        $namespace   = self::TEST_PROJECT_ROOT_NAMESPACE;
820
        $doctrineCmd = <<<DOCTRINE
821
 dsm:set:field \
822
    --project-root-path="{$this->workDir}" \
823
    --project-root-namespace="{$namespace}" \
824
    --entity="{$entityFqn}" \
825
    --field="{$fieldFqn}"
826
DOCTRINE;
827
        $this->execDoctrine($doctrineCmd);
828
    }
829
830
    /**
831
     * @return array
832
     * @SuppressWarnings(PHPMD.StaticAccess)
833
     */
834
    protected function getFieldFqns(): array
835
    {
836
        $fieldFqns = [];
837
        foreach (MappingHelper::COMMON_TYPES as $type) {
838
            $fieldFqns[] = self::TEST_FIELD_TRAIT_NAMESPACE . Inflector::classify($type) . 'FieldTrait';
839
        }
840
        foreach (self::UNIQUEABLE_FIELD_TYPES as $type) {
841
            $fieldFqns[] = self::TEST_FIELD_TRAIT_NAMESPACE . Inflector::classify('unique_' . $type) . 'FieldTrait';
842
        }
843
844
        return $fieldFqns;
845
    }
846
847
    protected function setTheDuplicateNamedFields(array $entities): void
848
    {
849
        foreach ($entities as $k => $entityFqn) {
850
            $fieldKey = ($k % 2 === 0) ? 0 : 1;
851
            $this->setField($entityFqn, self::DUPLICATE_SHORT_NAME_FIELDS[$fieldKey][0]);
852
        }
853
    }
854
855
    protected function setEmbeddable(string $entityFqn, string $embeddableTraitFqn): void
856
    {
857
        $doctrineCmd = <<<DOCTRINE
858
 dsm:set:embeddable \
859
    --entity="{$entityFqn}" \
860
    --embeddable="{$embeddableTraitFqn}"
861
DOCTRINE;
862
        $this->execDoctrine($doctrineCmd);
863
    }
864
865
    protected function generateEmbeddable(string $archetypeFqn, string $newClassName): void
866
    {
867
        $doctrineCmd = <<<DOCTRINE
868
dsm:generate:embeddable \
869
    --classname="{$newClassName}" \
870
    --archetype="{$archetypeFqn}"
871
DOCTRINE;
872
        $this->execDoctrine($doctrineCmd);
873
    }
874
875
    protected function removeUnusedRelations(): void
876
    {
877
        $namespace   = self::TEST_PROJECT_ROOT_NAMESPACE;
878
        $doctrineCmd = <<<DOCTRINE
879
 dsm:remove:unusedRelations \
880
    --project-root-namespace="{$namespace}"
881
DOCTRINE;
882
        $this->execDoctrine($doctrineCmd);
883
    }
884
885
    /**
886
     * @SuppressWarnings(PHPMD.Superglobals)
887
     * @throws Exception
888
     */
889
    public function testRunTests(): void
890
    {
891
        if ($this->isQuickTests()) {
892
            $this->markTestSkipped('Quick tests is enabled');
893
        }
894
        /** @lang bash */
895
        $bashCmds = <<<BASH
896
897
set +x
898
899
echo "
900
901
--------------------------------------------------
902
STARTS Running Tests In {$this->workDir}
903
--------------------------------------------------
904
905
"
906
907
#Prevent the retry tool dialogue etc
908
export CI=true
909
910
bash bin/qa
911
912
echo "
913
914
--------------------------------------------------
915
DONE Running Tests In {$this->workDir}
916
--------------------------------------------------
917
918
"
919
set -x
920
BASH;
921
        $this->execBash($bashCmds);
922
    }
923
}
924