diff --git a/.editorconfig b/.editorconfig
index d7ec37b..b88269d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -15,3 +15,6 @@ end_of_line = crlf
[*.yml]
indent_size = 2
+
+[*.neon]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
index 59e7ce9..33d633c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -11,9 +11,11 @@
# Ignore files for distribution archives, generated using `git archive`
.editorconfig export-ignore
-.git export-ignore
+.github export-ignore
.gitattributes export-ignore
.gitignore export-ignore
phpcs.xml export-ignore
phpunit.xml.dist export-ignore
/CakePHP/Tests export-ignore
+phpstan.neon export-ignore
+.phive export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4a98bd5..c099c7a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,11 +11,11 @@ on:
jobs:
testsuite:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
- php-version: ['8.1', '8.2', '8.3']
+ php-version: ['8.1', '8.2', '8.3', '8.4']
dependencies: ['highest']
include:
- php-version: '8.1'
@@ -40,20 +40,26 @@ jobs:
cs-stan:
name: Coding Standard & Static Analysis
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- tools: cs2pr
+ tools: phive, cs2pr
coverage: none
- name: Composer install
uses: ramsey/composer-install@v3
+ - name: Install PHP tools with phive.
+ run: "phive install --trust-gpg-keys 'CF1A108D0E7AE720,51C67305FFC2E5C0,12CE0F1D262429A5'"
+
- name: Run PHP CodeSniffer
run: vendor/bin/phpcs --report=checkstyle | cs2pr
+
+ - name: Run PHPStan
+ run: tools/phpstan analyse --error-format=github
diff --git a/.phive/phars.xml b/.phive/phars.xml
new file mode 100644
index 0000000..65df421
--- /dev/null
+++ b/.phive/phars.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php
index 52ea811..03d4ec1 100644
--- a/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php
+++ b/CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php
@@ -67,7 +67,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->addError(
'Chaining methods (@return $this) should not have any return-type-hint.',
$startIndex,
- 'InvalidSelf'
+ 'InvalidSelf',
);
return;
@@ -76,7 +76,7 @@ public function process(File $phpcsFile, $stackPtr)
$fix = $phpcsFile->addFixableError(
'Chaining methods (@return $this) should not have any return-type-hint (Remove "self").',
$startIndex,
- 'InvalidSelf'
+ 'InvalidSelf',
);
if (!$fix) {
return;
@@ -175,7 +175,7 @@ protected function assertNotThisOrStatic(File $phpCsFile, int $stackPointer): vo
$phpCsFile->addError(
'Class name repeated, expected `self` or `$this`.',
$classNameIndex,
- 'InvalidClass'
+ 'InvalidClass',
);
}
}
@@ -228,9 +228,14 @@ protected function getClassNameWithNamespace(File $phpCsFile): ?string
return null;
}
+ $classPointer = $phpCsFile->findPrevious(TokenHelper::$typeKeywordTokenCodes, $lastToken);
+ if (!$classPointer) {
+ return null;
+ }
+
return ClassHelper::getFullyQualifiedName(
$phpCsFile,
- $phpCsFile->findPrevious(TokenHelper::$typeKeywordTokenCodes, $lastToken)
+ $classPointer,
);
}
}
diff --git a/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php b/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php
index 41e90c1..c04c154 100644
--- a/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php
+++ b/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php
@@ -61,7 +61,7 @@ public function process(File $phpcsFile, $stackPtr)
$commentBorder = $phpcsFile->findNext(
[T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG],
$searchToken,
- $commentClose + 1
+ $commentClose + 1,
);
if ($commentBorder !== false) {
$tokensToIndent[$commentBorder] = $codeIndentation + 1;
diff --git a/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php b/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php
index 236d344..c6a3fa2 100644
--- a/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php
+++ b/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php
@@ -59,14 +59,14 @@ public function process(File $phpcsFile, $stackPtr)
$docCommentEnd = $phpcsFile->findPrevious(
[T_DOC_COMMENT_CLOSE_TAG, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET],
$stackPtr - 1,
- null
+ null,
);
if ($docCommentEnd === false || $tokens[$docCommentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) {
$phpcsFile->addError(
'Missing doc comment for function %s()',
$stackPtr,
'Missing',
- [$phpcsFile->getDeclarationName($stackPtr)]
+ [$phpcsFile->getDeclarationName($stackPtr)],
);
return;
@@ -77,14 +77,14 @@ public function process(File $phpcsFile, $stackPtr)
$attribute = $phpcsFile->findNext(
[T_ATTRIBUTE],
$lastEndToken + 1,
- $stackPtr
+ $stackPtr,
);
if ($attribute !== false) {
if ($tokens[$lastEndToken]['line'] !== $tokens[$attribute]['line'] - 1) {
$phpcsFile->addError(
'There must be no blank lines after the function comment or attribute',
$lastEndToken,
- 'SpacingAfter'
+ 'SpacingAfter',
);
return;
@@ -98,7 +98,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->addError(
'There must be no blank lines after the function comment or attribute',
$lastEndToken,
- 'SpacingAfter'
+ 'SpacingAfter',
);
}
@@ -152,7 +152,7 @@ protected function processThrows(File $phpcsFile, int $stackPtr, int $commentSta
if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
$matches = [];
preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[$tag + 2]['content'], $matches);
- $exception = $matches[1];
+ $exception = $matches[1] ?? null;
}
if ($exception === null) {
diff --git a/CakePHP/Sniffs/Commenting/TypeHintSniff.php b/CakePHP/Sniffs/Commenting/TypeHintSniff.php
index affdc84..da82ecf 100644
--- a/CakePHP/Sniffs/Commenting/TypeHintSniff.php
+++ b/CakePHP/Sniffs/Commenting/TypeHintSniff.php
@@ -32,6 +32,7 @@
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
+use PHPStan\PhpDocParser\ParserConfig;
/**
* Verifies order of types in type hints
@@ -128,7 +129,7 @@ public function process(File $phpcsFile, $stackPtr)
'%s type hint is not formatted properly, expected "%s"',
$tag,
'IncorrectFormat',
- [$tokens[$tag]['content'], $sortedTypeHint]
+ [$tokens[$tag]['content'], $sortedTypeHint],
);
if (!$fix) {
continue;
@@ -140,7 +141,7 @@ public function process(File $phpcsFile, $stackPtr)
'%s %s %s',
$sortedTypeHint,
$valueNode->variableName,
- $valueNode->description
+ $valueNode->description,
));
if ($tagComment[-1] === ' ') {
// tags above variables in code have a trailing space
@@ -152,13 +153,13 @@ public function process(File $phpcsFile, $stackPtr)
$sortedTypeHint,
$valueNode->isVariadic ? '...' : '',
$valueNode->parameterName,
- $valueNode->description
+ $valueNode->description,
));
} elseif ($valueNode instanceof ReturnTagValueNode) {
$newComment = trim(sprintf(
'%s %s',
$sortedTypeHint,
- $valueNode->description
+ $valueNode->description,
));
}
@@ -278,10 +279,10 @@ protected function getSortedTypeHint(array $types): string
protected function renderUnionTypes(array $typeNodes): string
{
// Remove parenthesis added by phpstan around union and intersection types
- return preg_replace(
+ return (string)preg_replace(
['/ ([\|&]) /', '/<\(/', '/\)>/', '/\), /', '/, \(/'],
['${1}', '<', '>', ', ', ', '],
- implode('|', $typeNodes)
+ implode('|', $typeNodes),
);
}
@@ -294,13 +295,16 @@ protected static function getValueNode(string $tagName, string $tagComment): Php
{
static $phpDocParser;
if (!$phpDocParser) {
- $constExprParser = new ConstExprParser();
- $phpDocParser = new PhpDocParser(new TypeParser($constExprParser), $constExprParser);
+ $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]);
+
+ $constExprParser = new ConstExprParser($config);
+ $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constExprParser), $constExprParser);
}
static $phpDocLexer;
if (!$phpDocLexer) {
- $phpDocLexer = new Lexer();
+ $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]);
+ $phpDocLexer = new Lexer($config);
}
return $phpDocParser->parseTagValue(new TokenIterator($phpDocLexer->tokenize($tagComment)), $tagName);
diff --git a/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php b/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php
index 7aa7bfd..916da55 100644
--- a/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php
+++ b/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php
@@ -72,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr)
$fix = $phpcsFile->addFixableError(
'Missing blank line before return statement',
$stackPtr,
- 'BlankLineBeforeReturn'
+ 'BlankLineBeforeReturn',
);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
diff --git a/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php b/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php
deleted file mode 100644
index e9dafce..0000000
--- a/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php
+++ /dev/null
@@ -1,52 +0,0 @@
-getTokens();
- $spaces = 0;
-
- if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) {
- $spaces = strlen($tokens[$stackPtr + 1]['content']);
- }
-
- if ($spaces !== 1) {
- $keyword = $tokens[$stackPtr]['code'] === T_FN ? 'fn' : 'function';
- $error = "Expected 1 space after closure's $keyword keyword; %s found";
- $data = [$spaces];
- $phpcsFile->addError($error, $stackPtr, 'SpaceAfterFunction', $data);
- }
- }
-}
diff --git a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php
index 17329df..c7dd3c0 100644
--- a/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php
+++ b/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php
@@ -63,6 +63,10 @@ public function __construct()
protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
{
$methodName = $phpcsFile->getDeclarationName($stackPtr);
+ if ($methodName === null) {
+ return;
+ }
+
$className = $phpcsFile->getDeclarationName($currScope);
$errorData = [$className . '::' . $methodName];
diff --git a/CakePHP/Sniffs/PHP/SingleQuoteSniff.php b/CakePHP/Sniffs/PHP/SingleQuoteSniff.php
index de97077..a07660b 100644
--- a/CakePHP/Sniffs/PHP/SingleQuoteSniff.php
+++ b/CakePHP/Sniffs/PHP/SingleQuoteSniff.php
@@ -56,7 +56,7 @@ public function process(File $phpcsFile, $stackPtr)
$fix = $phpcsFile->addFixableError(
'Use single instead of double quotes for simple strings.',
$stackPtr,
- 'UseSingleQuote'
+ 'UseSingleQuote',
);
if ($fix) {
$content = substr($content, 1, -1);
diff --git a/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php b/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php
index c488f28..064f04b 100644
--- a/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php
+++ b/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php
@@ -53,6 +53,9 @@ public function process(File $phpCsFile, $stackPointer)
$closingParenthesisIndex = $tokens[$openingParenthesisIndex]['parenthesis_closer'];
$semicolonIndex = $phpCsFile->findNext(T_SEMICOLON, $closingParenthesisIndex + 1);
+ if (!$semicolonIndex) {
+ return;
+ }
$nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $semicolonIndex + 1, null, true);
@@ -65,7 +68,7 @@ public function process(File $phpCsFile, $stackPointer)
$fix = $phpCsFile->addFixableError(
'Every function/method needs a newline afterwards',
$closingParenthesisIndex,
- 'Abstract'
+ 'Abstract',
);
if ($fix) {
$phpCsFile->fixer->addNewline($semicolonIndex);
@@ -84,6 +87,9 @@ public function process(File $phpCsFile, $stackPointer)
}
$nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $closingBraceIndex + 1, null, true);
+ if (!$nextContentIndex) {
+ return;
+ }
// Do not mess with the end of the class
if ($tokens[$nextContentIndex]['code'] === T_CLOSE_CURLY_BRACKET) {
@@ -108,7 +114,7 @@ protected function assertNewLineAtTheEnd(File $phpCsFile, int $closingBraceIndex
$fix = $phpCsFile->addFixableError(
'Every function/method needs a newline afterwards',
$closingBraceIndex,
- 'Concrete'
+ 'Concrete',
);
if ($fix) {
$phpCsFile->fixer->addNewline($closingBraceIndex);
@@ -147,6 +153,9 @@ protected function assertNewLineAtTheBeginning(File $phpCsFile, int $stackPointe
}
$prevContentIndex = $phpCsFile->findPrevious(T_WHITESPACE, $firstTokenInLineIndex - 1, null, true);
+ if (!$prevContentIndex) {
+ return;
+ }
// Do not mess with the start of the class
if ($tokens[$prevContentIndex]['code'] === T_OPEN_CURLY_BRACKET) {
@@ -160,7 +169,7 @@ protected function assertNewLineAtTheBeginning(File $phpCsFile, int $stackPointe
$fix = $phpCsFile->addFixableError(
'Every function/method needs a newline before',
$firstTokenInLineIndex,
- 'Concrete'
+ 'Concrete',
);
if ($fix) {
$phpCsFile->fixer->addNewline($prevContentIndex);
diff --git a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc b/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc
deleted file mode 100644
index 836703f..0000000
--- a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc
+++ /dev/null
@@ -1,39 +0,0 @@
- echo 'It Fails';
- $visitor = fn ($expression) => echo 'It works';
- }
-}
-
-$foo = 'bar';
-$bar = 'foo';
-
-$zum = function()use ($foo, $bar) {
- return $foo;
-};
-
-$zum = function ()use ($foo, $bar) {
- return $foo;
-};
diff --git a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php b/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php
deleted file mode 100644
index abe1c68..0000000
--- a/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 1,
- 25 => 1,
- 33 => 1,
- ];
- }
-
- /**
- * @inheritDoc
- */
- public function getWarningList()
- {
- return [];
- }
-}
diff --git a/CakePHP/ruleset.xml b/CakePHP/ruleset.xml
index c37562e..234e573 100644
--- a/CakePHP/ruleset.xml
+++ b/CakePHP/ruleset.xml
@@ -96,6 +96,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -115,17 +133,17 @@
-
-
-
-
-
+
+
+
+
+
-
+
@@ -141,6 +159,7 @@
*/tests/*
+
@@ -200,10 +219,11 @@
*/tests/*
-
+
-
+
+
@@ -214,12 +234,19 @@
-
+
+
+
+
+
+
+
-
+
+
diff --git a/README.md b/README.md
index 996d158..ee0b0d9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# CakePHP Code Sniffer
-
+
[](https://packagist.org/packages/cakephp/cakephp-codesniffer)
[](https://packagist.org/packages/cakephp/cakephp-codesniffer)
[](LICENSE)
diff --git a/composer.json b/composer.json
index 1347fc2..e7f83ff 100644
--- a/composer.json
+++ b/composer.json
@@ -19,8 +19,8 @@
},
"require": {
"php": ">=8.1.0",
- "phpstan/phpdoc-parser": "^1.4.5",
- "slevomat/coding-standard": "^8.15",
+ "phpstan/phpdoc-parser": "^2.1.0",
+ "slevomat/coding-standard": "^8.16",
"squizlabs/php_codesniffer": "^3.9"
},
"require-dev": {
@@ -45,6 +45,7 @@
],
"cs-check": "phpcs --colors --parallel=16 -p -s CakePHP/",
"cs-fix": "phpcbf --colors --parallel=16 -p CakePHP/",
+ "stan": "tools/phpstan analyse",
"docs": "php docs/generate.php",
"explain": "phpcs -e --standard=CakePHP"
}
diff --git a/docs/README.md b/docs/README.md
index 01afebf..c1384cc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,8 +1,8 @@
# CakePHP ruleset
-The CakePHP standard contains 147 sniffs
+The CakePHP standard contains 155 sniffs
-CakePHP (20 sniffs)
+CakePHP (19 sniffs)
-------------------
- CakePHP.Classes.ReturnTypeHint
- CakePHP.Commenting.DocBlockAlignment
@@ -13,7 +13,6 @@ CakePHP (20 sniffs)
- CakePHP.ControlStructures.ElseIfDeclaration
- CakePHP.ControlStructures.WhileStructures
- CakePHP.Formatting.BlankLineBeforeReturn
-- CakePHP.Functions.ClosureDeclaration
- CakePHP.NamingConventions.ValidFunctionName
- CakePHP.NamingConventions.ValidTraitName
- CakePHP.PHP.DisallowShortOpenTag
@@ -95,11 +94,12 @@ PSR12 (17 sniffs)
- PSR12.Properties.ConstantVisibility
- PSR12.Traits.UseDeclaration
-SlevomatCodingStandard (43 sniffs)
+SlevomatCodingStandard (52 sniffs)
----------------------------------
- SlevomatCodingStandard.Arrays.TrailingArrayComma
- SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing
- SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment
+- SlevomatCodingStandard.Classes.BackedEnumTypeSpacing
- SlevomatCodingStandard.Classes.ClassConstantVisibility
- SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces
- SlevomatCodingStandard.Classes.ModernClassNameReference
@@ -114,8 +114,16 @@ SlevomatCodingStandard (43 sniffs)
- SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses
- SlevomatCodingStandard.ControlStructures.NewWithParentheses
- SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator
+- SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator
- SlevomatCodingStandard.Exceptions.DeadCatch
- SlevomatCodingStandard.Functions.ArrowFunctionDeclaration
+- SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall
+- SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse
+- SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration
+- SlevomatCodingStandard.Functions.NamedArgumentSpacing
+- SlevomatCodingStandard.Functions.RequireTrailingCommaInCall
+- SlevomatCodingStandard.Functions.RequireTrailingCommaInClosureUse
+- SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration
- SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses
- SlevomatCodingStandard.Namespaces.DisallowGroupUse
- SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation
@@ -130,6 +138,7 @@ SlevomatCodingStandard (43 sniffs)
- SlevomatCodingStandard.PHP.UselessParentheses
- SlevomatCodingStandard.PHP.UselessSemicolon
- SlevomatCodingStandard.TypeHints.DeclareStrictTypes
+- SlevomatCodingStandard.TypeHints.DNFTypeHintFormat
- SlevomatCodingStandard.TypeHints.LongTypeHints
- SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue
- SlevomatCodingStandard.TypeHints.ParameterTypeHint
@@ -137,7 +146,6 @@ SlevomatCodingStandard (43 sniffs)
- SlevomatCodingStandard.TypeHints.PropertyTypeHint
- SlevomatCodingStandard.TypeHints.ReturnTypeHint
- SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing
-- SlevomatCodingStandard.TypeHints.UnionTypeHintFormat
- SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable
- SlevomatCodingStandard.Variables.UnusedVariable
@@ -174,4 +182,4 @@ Squiz (28 sniffs)
Zend (1 sniff)
--------------
-- Zend.NamingConventions.ValidVariableName
\ No newline at end of file
+- Zend.NamingConventions.ValidVariableName
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..f4fa105
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,8 @@
+parameters:
+ level: 8
+ paths:
+ - CakePHP/Sniffs/
+ bootstrapFiles:
+ - tests/phpstan_bootstrap.php
+ ignoreErrors:
+ - identifier: missingType.iterableValue
diff --git a/tests/phpstan_bootstrap.php b/tests/phpstan_bootstrap.php
new file mode 100644
index 0000000..3cc86c5
--- /dev/null
+++ b/tests/phpstan_bootstrap.php
@@ -0,0 +1,8 @@
+