diff --git a/.gitattributes b/.gitattributes index 00c615af..7f1a1cee 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,12 @@ /.github export-ignore /.gitignore export-ignore /.github export-ignore +/Makefile export-ignore +/phpstan-baseline.neon export-ignore +/phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore +/psalm-baseline.xml export-ignore +/psalm.xml export-ignore /README.md export-ignore /UPGRADING.md export-ignore +/vendor-bin export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 72084f84..590c9593 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,75 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email + address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a - professional setting + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at graham@alt-three.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +hello@gjcampbell.co.uk. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index baf77cc5..c1f7ada2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ We accept contributions via pull requests on Github. Please review these guideli ## Guidelines -* Please follow the [PSR-2 Coding Style Guide](https://www.php-fig.org/psr/psr-2/). +* Please follow the [PSR-12 Coding Style Guide](https://www.php-fig.org/psr/psr-12/). * Ensure that the current tests pass, and if you've added something new, add the tests where relevant. * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. * You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. @@ -18,13 +18,13 @@ We accept contributions via pull requests on Github. Please review these guideli First, install the dependencies using [Composer](https://getcomposer.org/): ```bash -$ composer install +$ make install ``` -Then run [PHPUnit](https://phpunit.de/): +Then run [PHPUnit](https://phpunit.de/) and the static analyzers: ```bash -$ vendor/bin/phpunit +$ make test ``` -The tests will be automatically run by [Travis CI](https://travis-ci.org/) and [GitHub Actions](https://github.com/features/actions) against pull requests. +These will also be automatically run by [GitHub Actions](https://github.com/features/actions) against pull requests. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index aeb6d6ef..44153fa3 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -3,12 +3,12 @@ ## Supported Versions After each new major release, the previous release will be supported for no -less than 2 years, unless explictly stated otherwise. This may mean that there +less than 2 years, unless explicitly stated otherwise. This may mean that there are multiple supported versions at any given time. ## Reporting a Vulnerability If you discover a security vulnerability within this package, please send an -email to Graham Campbell at graham@alt-three.com. All security vulnerabilities -will be promptly addressed. Please do not disclose security-related issues -publicly until a fix has been announced. +email to security@tidelift.com. All security vulnerabilities will be promptly +addressed. Please do not disclose security-related issues publicly until a fix +has been announced. diff --git a/.github/bin/composer.sh b/.github/bin/composer.sh deleted file mode 100755 index a8720060..00000000 --- a/.github/bin/composer.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" -php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" -if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then - >&2 echo 'ERROR: Invalid installer checksum' - rm composer-setup.php - exit 1 -else - php composer-setup.php --install-dir="/usr/bin" --filename=composer - RESULT=$? - rm composer-setup.php - composer config platform.php 5.6.50 - exit $RESULT -fi diff --git a/.github/bin/hhvm.sh b/.github/bin/hhvm.sh deleted file mode 100755 index a0448d6c..00000000 --- a/.github/bin/hhvm.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "deb https://dl.hhvm.com/ubuntu $(lsb_release -sc)-lts-$1 main" >> /etc/apt/sources.list -apt-get update -apt-get --allow-downgrades --reinstall install hhvm/$(lsb_release -sc)-lts-$1 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 00000000..702462a0 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,74 @@ +name: Static Analysis + +on: + push: + pull_request: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer:v2 + coverage: none + env: + update: true + + - name: Install Dependencies + uses: nick-invision/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --no-interaction --no-progress + + - name: Install PHPStan + uses: nick-invision/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer bin phpstan update --no-interaction --no-progress + + - name: Execute PHPStan + run: vendor/bin/phpstan analyze --no-progress + + psalm: + name: Psalm + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer:v2 + coverage: none + env: + update: true + + - name: Install Dependencies + uses: nick-invision/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --no-interaction --no-progress + + - name: Install Psalm + uses: nick-invision/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer bin psalm update --no-interaction --no-progress + + - name: Execute Psalm + run: vendor/bin/psalm.phar --no-progress --output-format=github diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57c72393..709203f8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,17 +5,17 @@ on: pull_request: jobs: - php: - name: PHP ${{ matrix.php }} - runs-on: ubuntu-20.04 + latest: + name: PHP ${{ matrix.php }} Latest + runs-on: ubuntu-24.04 strategy: matrix: - php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -27,8 +27,8 @@ jobs: - name: Setup Problem Matchers run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Install Dependencies - uses: nick-invision/retry@v1 + - name: Install Latest Dependencies + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -37,32 +37,34 @@ jobs: - name: Execute PHPUnit run: vendor/bin/phpunit - hhvm: - name: HHVM ${{ matrix.hhvm }} - runs-on: ubuntu-16.04 + lowest: + name: PHP ${{ matrix.php }} Lowest + runs-on: ubuntu-24.04 strategy: matrix: - hhvm: ["3.15", "3.18", "3.21", "3.24", "3.27", "3.30"] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Install HHVM - shell: bash - run: sudo .github/bin/hhvm.sh ${{ matrix.hhvm }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none - - name: Install Composer - shell: bash - run: sudo .github/bin/composer.sh + - name: Setup Problem Matchers + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Install Dependencies - uses: nick-invision/retry@v1 + - name: Install Lowest Dependencies + uses: nick-invision/retry@v3 with: timeout_minutes: 5 max_attempts: 5 - command: composer update --no-interaction --no-progress + command: composer update --prefer-lowest --prefer-stable --no-interaction --no-progress - name: Execute PHPUnit - run: hhvm vendor/bin/phpunit + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index f973ee0d..22d275d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .phpunit.result.cache composer.lock +phpstan.neon +phpstan.tests.neon phpunit.xml vendor diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b56bc428 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +install: + @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.4-base update + @docker run -it -w /data -v ${PWD}:/data:delegated -v ~/.composer:/root/.composer:delegated --entrypoint composer --rm registry.gitlab.com/grahamcampbell/php:8.4-base bin all update + +phpunit: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpunit --rm registry.gitlab.com/grahamcampbell/php:8.4-cli + +phpstan-analyze: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.4-cli analyze + +phpstan-baseline: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/phpstan --rm registry.gitlab.com/grahamcampbell/php:8.4-cli analyze --generate-baseline + +psalm-analyze: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli + +psalm-baseline: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli --set-baseline=psalm-baseline.xml + +psalm-show-info: + @docker run -it -w /data -v ${PWD}:/data:delegated --entrypoint vendor/bin/psalm.phar --rm registry.gitlab.com/grahamcampbell/php:8.4-cli --show-info=true + +test: phpunit phpstan-analyze psalm-analyze + +clean: + @rm -rf .phpunit.result.cache composer.lock vendor vendor-bin/*/composer.lock vendor-bin/*/vendor diff --git a/README.md b/README.md index ffa526d5..fe9f077e 100644 --- a/README.md +++ b/README.md @@ -11,24 +11,41 @@ Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` au Latest Version

- -Why .env? ---------- +
+ +**Special thanks to [our sponsors](https://github.com/sponsors/GrahamCampbell)** + +
+ +
+ Dotenvx +
+ Need to sync .env files across teams and environments? +
+ Dotenvx builds on the simplicity of phpdotenv with support for encryption, multiple environments, and team workflows. Use it alongside phpdotenv to modernize your secrets management. +
+
+ +
+
+ + +## Why .env? **You should never store sensitive credentials in your code**. Storing -[configuration in the environment](http://www.12factor.net/config) is one of -the tenets of a [twelve-factor app](http://www.12factor.net/). Anything that is -likely to change between deployment environments – such as database credentials -or credentials for 3rd party services – should be extracted from the code into -environment variables. +[configuration in the environment](https://www.12factor.net/config) is one of +the tenets of a [twelve-factor app](https://www.12factor.net/). Anything that +is likely to change between deployment environments – such as database +credentials or credentials for 3rd party services – should be extracted from +the code into environment variables. Basically, a `.env` file is an easy way to load custom configuration variables that your application needs without having to modify .htaccess files or Apache/nginx virtual hosts. This means you won't have to edit any files outside the project, and all the environment variables are always set no matter how you -run your project - Apache, Nginx, CLI, and even PHP 5.4's built-in webserver. -It's WAY easier than all the other ways you know of to set environment -variables, and you're going to love it! +run your project - Apache, Nginx, CLI, and even PHP's built-in webserver. It's +WAY easier than all the other ways you know of to set environment variables, +and you're going to love it! * NO editing virtual hosts in Apache or Nginx * NO adding `php_value` flags to .htaccess files @@ -39,8 +56,7 @@ PHP dotenv is a PHP version of the original [Ruby dotenv](https://github.com/bkeepers/dotenv). -Installation with Composer --------------------------- +## Installation Installation is super-easy via [Composer](https://getcomposer.org/): @@ -51,29 +67,14 @@ $ composer require vlucas/phpdotenv or add it by hand to your `composer.json` file. -UPGRADING FROM V2 ------------------ - -New in Version 3 is first-class support for multiline variables -([#301](https://github.com/vlucas/phpdotenv/pull/301)) and much more -flexibility in terms of which parts of the environment we try to read and -modify ([#300](https://github.com/vlucas/phpdotenv/pull/300)). Consequently, -you will need to replace any occurrences of `new Dotenv(...)` with -`Dotenv::create(...)`, since our new native constructor takes a `Loader` -instance now, so that it can be truly customized if required. Finally, one -should note that the loader will no longer be trimming values -([#302](https://github.com/vlucas/phpdotenv/pull/302)), moreover -`Loader::load()` and its callers now return an associative array of the -variables loaded with their values, rather than an array of raw lines from the -environment file ([#306](https://github.com/vlucas/phpdotenv/pull/306)). +## Upgrading -For more details, please see the -[release notes](https://github.com/vlucas/phpdotenv/releases/tag/v3.0.0) and -the [upgrading guide](UPGRADING.md). +We follow [semantic versioning](https://semver.org/), which means breaking +changes may occur between major releases. We have upgrading guides available +for V2 to V3, V3 to V4 and V4 to V5 available [here](UPGRADING.md). -Usage ------ +## Usage The `.env` file is generally kept out of version control since it can contain sensitive API keys and passwords. A separate `.env.example` file is created @@ -111,36 +112,50 @@ SECRET_KEY="abc123" You can then load `.env` in your application with: ```php -$dotenv = Dotenv\Dotenv::create(__DIR__); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv->load(); ``` -Optionally you can pass in a filename as the second parameter, if you would like to use something other than `.env` +To suppress the exception that is thrown when there is no `.env` file, you can: + +```php +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); +$dotenv->safeLoad(); +``` + +Optionally you can pass in a filename as the second parameter, if you would +like to use something other than `.env`: ```php -$dotenv = Dotenv\Dotenv::create(__DIR__, 'myconfig'); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'myconfig'); $dotenv->load(); ``` -All of the defined variables are now accessible with the `getenv` -method, and are available in the `$_ENV` and `$_SERVER` super-globals. +All of the defined variables are now available in the `$_ENV` and `$_SERVER` +super-globals. ```php -$s3_bucket = getenv('S3_BUCKET'); $s3_bucket = $_ENV['S3_BUCKET']; $s3_bucket = $_SERVER['S3_BUCKET']; ``` -You should also be able to access them using your framework's Request -class (if you are using a framework). + +### Putenv and Getenv + +Using `getenv()` and `putenv()` is strongly discouraged due to the fact that +these functions are not thread safe, however it is still possible to instruct +PHP dotenv to use these functions. Instead of calling +`Dotenv::createImmutable`, one can call `Dotenv::createUnsafeImmutable`, which +will add the `PutenvAdapter` behind the scenes. Your environment variables will +now be available using the `getenv` method, as well as the super-globals: ```php -$s3_bucket = $request->env('S3_BUCKET'); -$s3_bucket = $request->getEnv('S3_BUCKET'); -$s3_bucket = $request->server->get('S3_BUCKET'); -$s3_bucket = env('S3_BUCKET'); +$s3_bucket = getenv('S3_BUCKET'); +$s3_bucket = $_ENV['S3_BUCKET']; +$s3_bucket = $_SERVER['S3_BUCKET']; ``` + ### Nesting Variables It's possible to nest an environment variable within another, useful to cut @@ -154,40 +169,56 @@ CACHE_DIR="${BASE_DIR}/cache" TMP_DIR="${BASE_DIR}/tmp" ``` -### Immutability -By default, Dotenv will NOT overwrite existing environment variables that are -already set in the environment. +### Immutability and Repository Customization -If you want Dotenv to overwrite existing environment variables, use `overload` -instead of `load`: +Immutability refers to if Dotenv is allowed to overwrite existing environment +variables. If you want Dotenv to overwrite existing environment variables, +use `createMutable` instead of `createImmutable`: ```php -$dotenv = Dotenv\Dotenv::create(__DIR__); -$dotenv->overload(); +$dotenv = Dotenv\Dotenv::createMutable(__DIR__); +$dotenv->load(); ``` -### Loader Customization +Behind the scenes, this is instructing the "repository" to allow immutability +or not. By default, the repository is configured to allow overwriting existing +values by default, which is relevant if one is calling the "create" method +using the `RepositoryBuilder` to construct a more custom repository: + +```php +$repository = Dotenv\Repository\RepositoryBuilder::createWithNoAdapters() + ->addAdapter(Dotenv\Repository\Adapter\EnvConstAdapter::class) + ->addWriter(Dotenv\Repository\Adapter\PutenvAdapter::class) + ->immutable() + ->make(); + +$dotenv = Dotenv\Dotenv::create($repository, __DIR__); +$dotenv->load(); +``` -Need us to not set `$_ENV` but not `$_SERVER`, or have other custom requirements? No problem! Simply pass a custom implementation of `Dotenv\Environment\FactoryInterface` to `Dotenv\Loader` on construction. In practice, you may not even need a custom implementation, since our default implementation allows you provide an array of `Dotenv\Environment\Adapter\AdapterInterface` for proxing the underlying calls to. +The above example will write loaded values to `$_ENV` and `putenv`, but when +interpolating environment variables, we'll only read from `$_ENV`. Moreover, it +will never replace any variables already set before loading the file. -For example, if you want us to only ever fiddle with `$_ENV` and `putenv`, then you can setup Dotenv as follows: +By means of another example, one can also specify a set of variables to be +allow listed. That is, only the variables in the allow list will be loaded: ```php -$factory = new Dotenv\Environment\DotenvFactory([ - new Dotenv\Environment\Adapter\EnvConstAdapter(), - new Dotenv\Environment\Adapter\PutenvAdapter(), -]); +$repository = Dotenv\Repository\RepositoryBuilder::createWithDefaultAdapters() + ->allowList(['FOO', 'BAR']) + ->make(); -$dotenv = Dotenv\Dotenv::create(__DIR__, null, $factory); +$dotenv = Dotenv\Dotenv::create($repository, __DIR__); +$dotenv->load(); ``` -Requiring Variables to be Set ------------------------------ +### Requiring Variables to be Set -Using Dotenv, you can require specific ENV vars to be defined ($_ENV, $_SERVER or getenv()) - throws an exception otherwise. -Note: It does not check for existence of a variable in a '.env' file. This is particularly useful to let people know any explicit required variables that your app will not work without. +PHP dotenv has built in validation functionality, including for enforcing the +presence of an environment variable. This is particularly useful to let people +know any explicit required variables that your app will not work without. You can use a single string: @@ -207,6 +238,7 @@ If any ENV vars are missing, Dotenv will throw a `RuntimeException` like this: One or more environment variables failed assertions: DATABASE_DSN is missing ``` + ### Empty Variables Beyond simply requiring a variable to be set, you might also need to ensure the @@ -222,9 +254,11 @@ If the environment variable is empty, you'd get an Exception: One or more environment variables failed assertions: DATABASE_DSN is empty ``` + ### Integer Variables -You might also need to ensure that the variable is of an integer value. You may do the following: +You might also need to ensure that the variable is of an integer value. You may +do the following: ```php $dotenv->required('FOO')->isInteger(); @@ -233,12 +267,22 @@ $dotenv->required('FOO')->isInteger(); If the environment variable is not an integer, you'd get an Exception: ``` -One or more environment variables failed assertions: FOO is not an integer +One or more environment variables failed assertions: FOO is not an integer. +``` + +One may only want to enforce validation rules when a variable is set. We +support this too: + +```php +$dotenv->ifPresent('FOO')->isInteger(); ``` + ### Boolean Variables -You may need to ensure a variable is in the form of a boolean, accepting "true", "false", "On", "1", "Yes", "Off", "0" and "No". You may do the following: +You may need to ensure a variable is in the form of a boolean, accepting +"true", "false", "On", "1", "Yes", "Off", "0" and "No". You may do the +following: ```php $dotenv->required('FOO')->isBoolean(); @@ -247,9 +291,16 @@ $dotenv->required('FOO')->isBoolean(); If the environment variable is not a boolean, you'd get an Exception: ``` -One or more environment variables failed assertions: FOO is not a boolean +One or more environment variables failed assertions: FOO is not a boolean. +``` + +Similarly, one may write: + +```php +$dotenv->ifPresent('FOO')->isBoolean(); ``` + ### Allowed Values It is also possible to define a set of values that your environment variable @@ -264,8 +315,7 @@ If the environment variable wasn't in this list of allowed values, you'd get a similar Exception: ``` -One or more environment variables failed assertions: SESSION_STORE is not an -allowed value +One or more environment variables failed assertions: SESSION_STORE is not an allowed value. ``` It is also possible to define a regex that your environment variable should be. @@ -273,6 +323,7 @@ It is also possible to define a regex that your environment variable should be. $dotenv->required('FOO')->allowedRegexValues('([[:lower:]]{3})'); ``` + ### Comments You can comment your `.env` file using the `#` character. E.g. @@ -283,38 +334,47 @@ VAR="value" # comment VAR=value # comment ``` -Usage Notes ------------ - -When a new developer clones your codebase, they will have an additional -**one-time step** to manually copy the `.env.example` file to `.env` and fill-in -their own values (or get any sensitive values from a project co-worker). -### Command Line Scripts +### Parsing Without Loading -If you need to use environment variables that you have set in your `.env` file -in a command line script that doesn't use the Dotenv library, you can `source` -it into your local shell session: +Sometimes you just wanna parse the file and resolve the nested environment variables, by giving us a string, and have an array returned back to you. While this is already possible, it is a little fiddly, so we have provided a direct way to do this: +```php +// ['FOO' => 'Bar', 'BAZ' => 'Hello Bar'] +Dotenv\Dotenv::parse("FOO=Bar\nBAZ=\"Hello \${FOO}\""); ``` -source .env + +This is exactly the same as: + +```php +Dotenv\Dotenv::createArrayBacked(__DIR__)->load(); ``` +only, instead of providing the directory to find the file, you have directly provided the file contents. + + +### Usage Notes + +When a new developer clones your codebase, they will have an additional +one-time step to manually copy the `.env.example` file to `.env` and fill-in +their own values (or get any sensitive values from a project co-worker). + + +### Troubleshooting + +In certain server setups (most commonly found in shared hosting), PHP might deactivate superglobals like `$_ENV` or `$_SERVER`. If these variables are not set, review the `variables_order` in the `php.ini` file. See [php.net/manual/en/ini.core.php#ini.variables-order](https://www.php.net/manual/en/ini.core.php#ini.variables-order). -Security --------- +## Security -If you discover a security vulnerability within this package, please send an email to Graham Campbell at graham@alt-three.com. All security vulnerabilities will be promptly addressed. You may view our full security policy [here](https://github.com/vlucas/phpdotenv/security/policy). +If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. You may view our full security policy [here](https://github.com/vlucas/phpdotenv/security/policy). -License -------- +## License PHP dotenv is licensed under [The BSD 3-Clause License](LICENSE). -For Enterprise --------------- +## For Enterprise Available as part of the Tidelift Subscription diff --git a/UPGRADING.md b/UPGRADING.md index 2210ccb6..77d96db0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,7 +1,161 @@ # Upgrading Guide +## V5.5 to V5.6 + +Bumping the minimum required PHP version is not a breaking change, however it is notable. Since version 5.6.0, we now require PHP 7.2.5 or higher. Installation metrics show that for some time, PHP 7.1 has represented only around 0.1% of installs of V5. + +Release notes for 5.6.0 are available [here](https://github.com/vlucas/phpdotenv/releases/tag/v5.6.0). + +## V4 to V5 + +### Introduction + +Version 5 bumps to PHP 7.1+, and adds some additional parameter typing. There have been some internal changes and refactorings too, but nothing that changes the overall feel and usage of the package. The Dotenv class itself is largely unchanged from V4. + +Release notes for 5.0.0 are available [here](https://github.com/vlucas/phpdotenv/releases/tag/v5.0.0). + +### Details + +1. The `Dotenv\Dotenv::createImmutable` and `Dotenv\Dotenv::createMutable` methods no longer call will result in `getenv` and `putenv` being called. One should instead use `Dotenv\Dotenv::createUnsafeImmutable` and `Dotenv\Dotenv::createUnsafeMutable` methods, if one really needs these functions. +2. The `Dotenv\Dotenv` constructor has been modified to expect exactly 4 parameters: a store, a parser, a loader, and a repository. This likely won't affect many people, since it is more common to construct this class via the public static create methods. Those methods have not changed. +3. Scalar typehints have been added to the public interface. +4. The parser now returns a result type instead of raising an exception. This change is strictly internal, and most users won't notice a difference. The responsibility for raising an exception has simply been shifted up to the caller. +5. Adapters have been refactored again, with changes to the repositories. In particular, the repository builder has been tweaked. It now expects to be explicitly told if you want to use the default adapters or not, and expects individual readers and writers to be added, one by one. Similar changes have been applied to the store factory. Moreover, the `ApacheAdapter` has been changed so that it behaves much like the other adapters. The old behaviour can be simulated by composing it with the new `ReplacingWriter` (see below). We will no longer include this adapter in our default setup, so that people can enable exactly what they need. Finally, by default, we will no longer be using the `PutenvAdapter`. It can be added, as required. +6. Variable whitelisting has been replaced with allow listing, and the responsibility has moved from the loader to a new adapter `GuardedWriter`. +7. The parser has been moved to its own namespace and parses entire files now. This change is expected to have little impact when upgrading. The `Lines` class has also moved to the parser namespace. +8. The loader now only returns the variables that were actually loaded into the repository, and not all the variables from the file. Moreover, it now expects as input the result of running the new parser (an array of entries), rather than raw file content. + +The changes listed in (4) mean that instead of: + +```php +$repository = Dotenv\Repository\RepositoryBuilder::create() + ->withReaders([ + new Dotenv\Repository\Adapter\EnvConstAdapter(), + ]) + ->withWriters([ + new Dotenv\Repository\Adapter\EnvConstAdapter(), + new Dotenv\Repository\Adapter\PutenvAdapter(), + ]) + ->make(); +``` + +one would now write: + +```php +$repository = Dotenv\Repository\RepositoryBuilder::createWithNoAdapters() + ->addAdapter(Dotenv\Repository\Adapter\EnvConstAdapter::class) + ->addWriter(Dotenv\Repository\Adapter\PutenvAdapter::class) + ->make(); +``` + +Instead of passing class names, one can also pass actual adapter instances. Note that it is not possible to directly construct any of the adapters. One has to go via their static `create` method which returns an optional. This is to strictly encapsulate the fact that not all adapters are capable of running on all systems, and so those that cannot be run, cannot be created. For example, the apache adapter can only be run within an apache web server context. Passing the class names as in the above example will handle this for you, by adding the adapter only if it can be created (the optional has a value set). + +To add an apache environment variable writer that only writes to existing apache environment variables, as was the default in v4, one should do the following: + +```php +$builder = Dotenv\Repository\RepositoryBuilder::createWithDefaultAdapters(); + +Dotenv\Repository\Adapter\ApacheAdapter::create()->map(function ($adapter) { + return new Dotenv\Repository\Adapter\ReplacingWriter($adapter, $adapter); +})->map([$builder, 'addWriter'])->getOrElse($builder); + +$repository = $builder->make(); +``` + +The use of optionals handles the case where the apache environment functions are not available (such as in a CLI environment). + +## V4.0 to V4.1 + +### Introduction + +Version 4.1 is a minor release, and as such, there are no breaking changes. There is, however a deprecation to be noted. + +### Details + +The `Dotenv\Dotenv` constructor now expects either an array of file paths as the third parameter, or an instance of `Dotenv\Store\StoreInterface`. Passing an array is deprecated, and will be removed in V5. + +## V3 to V4 + +### Introduction + +Version 4 sees some refactoring, and support for escaping dollars in values (https://github.com/vlucas/phpdotenv/pull/380). It is no longer possible to change immutability on the fly, and the `Loader` no longer is responsible for tracking immutability. It is now the responsibility of "repositories" to track this. One must explicitly decide if they want (im)mutability when constructing an instance of `Dotenv\Dotenv`. + +Release notes for 4.0.0 are available [here](https://github.com/vlucas/phpdotenv/releases/tag/v4.0.0). + +### Details + +V4 has again changed the way you initialize the `Dotenv` class. If you want immutable loading of environment variables, then replace `Dotenv::create` with `Dotenv::createImmutable`, and if you want mutable loading, replace `Dotenv::create` with `Dotenv::createMutable` and `->overload()` with `->load()`. The `overload` method has been removed in favour of specifying mutability at object construction. + +The behaviour when parsing single quoted strings has now changed, to mimic the behaviour of bash. It is no longer possible to escape characters in single quoted strings, and everything is treated literally. As soon as the first single quote character is read, after the initial one, then the variable is treated as ending immediately at that point. When parsing unquoted or double quoted strings, it is now possible to escape dollar signs, to forcefully avoid variable interpolation. Escaping dollars is not mandated, in the sense that if a dollar is present, and not following by variable interpolation syntax, this is allowed, and the dollar will be treated as a literal dollar. Finally, interpolation of variables is now performed right to left, instead of left to right, so it is possible to nest interpolations to allow using the value of a variable as the name of another for further interpolation. + +The `getEnvironmentVariableNames` method is no longer available. This is because calls to `load()` (since v3.0.0) return an associative array of what was loaded, so `$dotenv->getEnvironmentVariableNames()` can be replaced with `array_keys($dotenv->load())`. + +There have been various internal refactorings. Apart from what has already been mentioned, the only other changes likely to affect developers is: + +1. The `Dotenv\Environment` namespace has been moved to `Dotenv\Repository`, the `Dotenv\Environment\Adapter\AdapterInterface` interface has been replaced by `Dotenv\Repository\Adapter\ReaderInterface` and `Dotenv\Repository\Adapter\WriterInterface`. +2. The `Dotenv\Environment\DotenvFactory` has been (roughly) replaced by `Dotenv\Repository\RepositoryBuilder`, and `Dotenv\Environment\FactoryInterface` has been deleted. +3. `Dotenv\Environment\AbstractVariables` has been replaced by `Dotenv\Repository\AbstractRepository`, `Dotenv\Environment\DotenvVariables` has been replaced by `Dotenv\Repository\AdapterRepository`, and `Dotenv\Environment\VariablesInterface` has been replaced by `Dotenv\Repository\RepositoryInterface`. +4. The `Dotenv\Loader` class has been moved to `Dotenv\Loader\Loader`, and now has a different public interface. It no longer expects any parameters at construction, and implements only the new interface `Dotenv\Loader\LoaderInterface`. Its responsibility has changed to purely taking raw env file content, and handing it off to the parser, dealing with variable interpolation, and sending off instructions to the repository to set variables. No longer can it be used as a way to read the environment by callers, and nor does it track immutability. +5. The `Dotenv\Parser` and `Dotenv\Lines` classes have moved to `Dotenv\Loader\Parser` and `Dotenv\Loader\Lines`, respectively. `Dotenv\Loader\Parser::parse` now return has either `null` or `Dotenv\Loader\Value` objects as values, instead of `string`s. This is to support the new variable interpolation and dollar escaping features. +6. The `Dotenv\Validator` constructor has changed from `__construct(array $variables, Loader $loader, $required = true)` to `__construct(RepositoryInterface $repository, array $variables, $required = true)`. + +The example at the bottom of the below upgrading guide, in V4 now looks like: + +```php +withReaders($adapters) + ->withWriters($adapters) + ->immutable() + ->make(); + +Dotenv::create($repository, $path, null)->load(); +``` + +Since v3.2.0, it was easily possible to read a file and process variable interpolations, without actually "loading" the variables. This is still possible in v4.0.0. Example code that does this is as follows: + +```php +withReaders($adapters) + ->withWriters($adapters) + ->make(); + +$variables = (new Loader())->load($repository, $content); +``` + +Notice, that compared to v3, the loader no longer expects file paths in the constructor. Reading of the files is now managed by the `Dotenv\Dotenv` class. The loader is genuinely just loading the content into the repository. + +Finally, we note that the minimum supported version of PHP has increased to 5.5.9, up from 5.4.0 in V3 and 5.3.9 in V2. + ## V2 to V3 +### Introduction + +New in Version 3 is first-class support for multiline variables ([#301](https://github.com/vlucas/phpdotenv/pull/301)) and much more flexibility in terms of which parts of the environment we try to read and modify ([#300](https://github.com/vlucas/phpdotenv/pull/300)). Consequently, you will need to replace any occurrences of `new Dotenv(...)` with `Dotenv::create(...)`, since our new native constructor takes a `Loader` instance now, so that it can be truly customized if required. Finally, one should note that the loader will no longer be trimming values ([#302](https://github.com/vlucas/phpdotenv/pull/302)), moreover `Loader::load()` and its callers now return an associative array of the variables loaded with their values, rather than an array of raw lines from the environment file ([#306](https://github.com/vlucas/phpdotenv/pull/306)). + +Release notes for 3.0.0 are available [here](https://github.com/vlucas/phpdotenv/releases/tag/v3.0.0). + +### Details + V3 has changed the way you initialize the `Dotenv` class. Consequently, you will need to replace any occurrences of new Dotenv(...) with Dotenv::create(...), since our new native constructor takes a `Loader` instance now. `Loader::load()` and its callers now return an associative array of the variables loaded with their values. @@ -24,17 +178,19 @@ Value parsing has been modified in the following ways: In double quoted strings, double quotes and backslashes need escaping with a backslash, and in single quoted strings, single quote and backslashes need escaping with a backslash. In v2.5.2, forgetting an escape can lead to odd results due to the regex running out of stack, but this was fixed in 2.6 and 3.3, with 2.6 allowing you to continue after an unescaped backslash, but 3.3 not. -Finally, it's possible to use phpdotenv V3 in a threaded environment, instructing it to not call any functions that are not tread-safe: +It's possible to use phpdotenv V3 in a threaded environment, instructing it to not call any functions that are not tread-safe: ```php load(); ``` + +Finally, we note that the minimum supported version of PHP has increased from 5.3.9 to 5.4.0. diff --git a/composer.json b/composer.json index 61587904..3636317f 100644 --- a/composer.json +++ b/composer.json @@ -16,30 +16,45 @@ } ], "require": { - "php": "^5.4 || ^7.0 || ^8.0", - "phpoption/phpoption": "^1.5.2", - "symfony/polyfill-ctype": "^1.17" + "php": "^7.2.5 || ^8.0", + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { "ext-filter": "*", - "ext-pcre": "*", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit":"^8.5.34 || ^9.6.13 || ^10.4.2" }, "autoload": { "psr-4": { "Dotenv\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Dotenv\\Tests\\": "tests/Dotenv/" + } + }, "suggest": { - "ext-filter": "Required to use the boolean validator.", - "ext-pcre": "Required to use most of the library." + "ext-filter": "Required to use the boolean validator." }, "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + }, "preferred-install": "dist" }, "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "3.6-dev" + "dev-master": "5.6-dev" } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..598d35d5 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,163 @@ +parameters: + ignoreErrors: + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Option\\.$#' + identifier: varTag.type + count: 1 + path: src/Parser/Entry.php + + - + message: '#^Anonymous function should return GrahamCampbell\\ResultType\\Result\ but returns GrahamCampbell\\ResultType\\Result\\.$#' + identifier: return.type + count: 1 + path: src/Parser/EntryParser.php + + - + message: '#^Method Dotenv\\Parser\\EntryParser\:\:parse\(\) should return GrahamCampbell\\ResultType\\Result\ but returns GrahamCampbell\\ResultType\\Result\\.$#' + identifier: return.type + count: 1 + path: src/Parser/EntryParser.php + + - + message: '#^PHPDoc tag @var with type GrahamCampbell\\ResultType\\Result\ is not subtype of type GrahamCampbell\\ResultType\\Result\\|GrahamCampbell\\ResultType\\Result\\.$#' + identifier: varTag.type + count: 1 + path: src/Parser/EntryParser.php + + - + message: '#^Parameter \#2 \$callback of function array_reduce expects callable\(GrahamCampbell\\ResultType\\Result\\|GrahamCampbell\\ResultType\\Result\, string\)\: \(GrahamCampbell\\ResultType\\Result\\|GrahamCampbell\\ResultType\\Result\\), Closure\(GrahamCampbell\\ResultType\\Result, string\)\: GrahamCampbell\\ResultType\\Result\ given\.$#' + identifier: argument.type + count: 1 + path: src/Parser/EntryParser.php + + - + message: '#^Only booleans are allowed in a negated boolean, int\|false given\.$#' + identifier: booleanNot.exprNotBoolean + count: 1 + path: src/Parser/Lexer.php + + - + message: '#^Parameter \#1 \$pattern of function preg_match expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Parser/Lexer.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Some\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/ApacheAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Option\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/ApacheAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Some\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/ArrayAdapter.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string + count: 1 + path: src/Repository/Adapter/EnvConstAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Some\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/EnvConstAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Some\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/PutenvAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Option\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/PutenvAdapter.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string + count: 1 + path: src/Repository/Adapter/ServerConstAdapter.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Some\\.$#' + identifier: varTag.type + count: 1 + path: src/Repository/Adapter/ServerConstAdapter.php + + - + message: '#^Parameter \#1 \$callable of method PhpOption\\Some\\:\:flatMap\(\) expects callable\(Dotenv\\Repository\\Adapter\\AdapterInterface\|string\)\: PhpOption\\Option\, Closure\(mixed\)\: mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Repository/RepositoryBuilder.php + + - + message: '#^Parameter \#1 \$callable of method PhpOption\\Some\\:\:flatMap\(\) expects callable\(Dotenv\\Repository\\Adapter\\ReaderInterface\|string\)\: PhpOption\\Option\, Closure\(mixed\)\: mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Repository/RepositoryBuilder.php + + - + message: '#^Parameter \#1 \$callable of method PhpOption\\Some\\:\:flatMap\(\) expects callable\(Dotenv\\Repository\\Adapter\\WriterInterface\|string\)\: PhpOption\\Option\, Closure\(mixed\)\: mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Repository/RepositoryBuilder.php + + - + message: '#^Parameter \#1 \$readers of class Dotenv\\Repository\\RepositoryBuilder constructor expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/Repository/RepositoryBuilder.php + + - + message: '#^Parameter \#2 \$writers of class Dotenv\\Repository\\RepositoryBuilder constructor expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/Repository/RepositoryBuilder.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Option\\.$#' + identifier: varTag.type + count: 1 + path: src/Store/File/Reader.php + + - + message: '#^Method Dotenv\\Util\\Regex\:\:occurrences\(\) should return GrahamCampbell\\ResultType\\Result\ but returns GrahamCampbell\\ResultType\\Result\, string\>\.$#' + identifier: return.type + count: 1 + path: src/Util/Regex.php + + - + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: src/Util/Str.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 1 + path: src/Util/Str.php + + - + message: '#^PHPDoc tag @var with type GrahamCampbell\\ResultType\\Result\ is not subtype of type GrahamCampbell\\ResultType\\Result\\.$#' + identifier: varTag.type + count: 2 + path: src/Util/Str.php + + - + message: '#^PHPDoc tag @var with type PhpOption\\Option\ is not subtype of type PhpOption\\Option\\|false\>\.$#' + identifier: varTag.type + count: 1 + path: src/Util/Str.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..6f8227f5 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 340f19a4..5c1fa16e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,13 @@ - - - - ./tests - - - - - ./src - - + + + + ./tests + + + + + ./src + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 00000000..7d9c6a94 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..06d35250 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/Dotenv.php b/src/Dotenv.php index 41a2d3d5..34ca8500 100644 --- a/src/Dotenv.php +++ b/src/Dotenv.php @@ -1,159 +1,267 @@ store = $store; + $this->parser = $parser; $this->loader = $loader; + $this->repository = $repository; } /** * Create a new dotenv instance. * - * @param string|string[] $paths - * @param string|null $file - * @param \Dotenv\Environment\FactoryInterface|null $envFactory + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ - public static function create($paths, $file = null, FactoryInterface $envFactory = null) + public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) { - $loader = new Loader( - self::getFilePaths((array) $paths, $file ?: '.env'), - $envFactory ?: new DotenvFactory(), - true - ); + $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + + foreach ((array) $paths as $path) { + $builder = $builder->addPath($path); + } + + foreach ((array) $names as $name) { + $builder = $builder->addName($name); + } + + if ($shortCircuit) { + $builder = $builder->shortCircuit(); + } - return new self($loader); + return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); } /** - * Returns the full paths to the files. + * Create a new mutable dotenv instance with default repository. * - * @param string[] $paths - * @param string $file + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * - * @return string[] + * @return \Dotenv\Dotenv */ - private static function getFilePaths(array $paths, $file) + public static function createMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) { - return array_map(function ($path) use ($file) { - return rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$file; - }, $paths); + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** - * Load environment file in given directory. + * Create a new mutable dotenv instance with default repository with the putenv adapter. * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * - * @return array + * @return \Dotenv\Dotenv */ - public function load() + public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) { - return $this->loadData(); + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** - * Load environment file in given directory, silently failing if it doesn't exist. + * Create a new immutable dotenv instance with default repository. * - * @throws \Dotenv\Exception\InvalidFileException + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * - * @return array + * @return \Dotenv\Dotenv */ - public function safeLoad() + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) { - try { - return $this->loadData(); - } catch (InvalidPathException $e) { - // suppressing exception - return []; - } + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** - * Load environment file in given directory. + * Create a new immutable dotenv instance with default repository with the putenv adapter. * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * - * @return array + * @return \Dotenv\Dotenv */ - public function overload() + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) { - return $this->loadData(true); + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** - * Actually load the data. + * Create a new dotenv instance with an array backed repository. * - * @param bool $overload + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException + * @return \Dotenv\Dotenv + */ + public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Parse the given content and resolve nested variables. + * + * This method behaves just like load(), only without mutating your actual + * environment. We do this by using an array backed repository. * - * @return array + * @param string $content + * + * @throws \Dotenv\Exception\InvalidFileException + * + * @return array */ - protected function loadData($overload = false) + public static function parse(string $content) { - return $this->loader->setImmutable(!$overload)->load(); + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); + + return $phpdotenv->load(); } /** - * Required ensures that the specified variables exist, and returns a new validator object. + * Read and load environment file(s). * - * @param string|string[] $variables + * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException * - * @return \Dotenv\Validator + * @return array */ - public function required($variables) + public function load() { - return new Validator((array) $variables, $this->loader); + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); } /** - * Returns a new validator object that won't check if the specified variables exist. + * Read and load environment file(s), silently failing if no files can be read. + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException + * + * @return array + */ + public function safeLoad() + { + try { + return $this->load(); + } catch (InvalidPathException $e) { + // suppressing exception + return []; + } + } + + /** + * Required ensures that the specified variables exist, and returns a new validator object. * * @param string|string[] $variables * * @return \Dotenv\Validator */ - public function ifPresent($variables) + public function required($variables) { - return new Validator((array) $variables, $this->loader, false); + return (new Validator($this->repository, (array) $variables))->required(); } /** - * Get the list of environment variables declared inside the 'env' file. + * Returns a new validator object that won't check if the specified variables exist. * - * @return string[] + * @param string|string[] $variables + * + * @return \Dotenv\Validator */ - public function getEnvironmentVariableNames() + public function ifPresent($variables) { - return $this->loader->getEnvironmentVariableNames(); + return new Validator($this->repository, (array) $variables); } } diff --git a/src/Environment/AbstractVariables.php b/src/Environment/AbstractVariables.php deleted file mode 100644 index 1485b628..00000000 --- a/src/Environment/AbstractVariables.php +++ /dev/null @@ -1,195 +0,0 @@ -immutable = $immutable; - $this->loaded = new ArrayAdapter(); - } - - /** - * Get an environment variable. - * - * @param string $name - * - * @throws \InvalidArgumentException - * - * @return string|null - */ - public function get($name) - { - if (!is_string($name)) { - throw new InvalidArgumentException('Expected name to be a string.'); - } - - return $this->getInternal($name); - } - - /** - * Get an environment variable. - * - * @param string $name - * - * @return string|null - */ - abstract protected function getInternal($name); - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @throws \InvalidArgumentException - * - * @return void - */ - public function set($name, $value = null) - { - if (!is_string($name)) { - throw new InvalidArgumentException('Expected name to be a string.'); - } - - // Don't overwrite existing environment variables if we're immutable - // Ruby's dotenv does this with `ENV[key] ||= value`. - if ($this->isImmutable() && $this->get($name) !== null && $this->loaded->get($name)->isEmpty()) { - return; - } - - $this->setInternal($name, $value); - $this->loaded->set($name, ''); - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - abstract protected function setInternal($name, $value = null); - - /** - * Clear an environment variable. - * - * @param string $name - * - * @throws \InvalidArgumentException - * - * @return void - */ - public function clear($name) - { - if (!is_string($name)) { - throw new InvalidArgumentException('Expected name to be a string.'); - } - - // Don't clear anything if we're immutable. - if ($this->isImmutable()) { - return; - } - - $this->clearInternal($name); - } - - /** - * Clear an environment variable. - * - * @param string $name - * - * @return void - */ - abstract protected function clearInternal($name); - - /** - * Determine if the environment is immutable. - * - * @return bool - */ - public function isImmutable() - { - return $this->immutable; - } - - /** - * Tells whether environment variable has been defined. - * - * @param string $name - * - * @return bool - */ - public function has($name) - { - return is_string($name) && $this->get($name) !== null; - } - - /** - * {@inheritdoc} - */ - #[ReturnTypeWillChange] - public function offsetExists($offset) - { - return $this->has($offset); - } - - /** - * {@inheritdoc} - */ - #[ReturnTypeWillChange] - public function offsetGet($offset) - { - return $this->get($offset); - } - - /** - * {@inheritdoc} - */ - #[ReturnTypeWillChange] - public function offsetSet($offset, $value) - { - $this->set($offset, $value); - } - - /** - * {@inheritdoc} - */ - #[ReturnTypeWillChange] - public function offsetUnset($offset) - { - $this->clear($offset); - } -} diff --git a/src/Environment/Adapter/AdapterInterface.php b/src/Environment/Adapter/AdapterInterface.php deleted file mode 100644 index 21ef29a1..00000000 --- a/src/Environment/Adapter/AdapterInterface.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - private $variables = []; - - /** - * Determines if the adapter is supported. - * - * @return bool - */ - public function isSupported() - { - return true; - } - - /** - * Get an environment variable, if it exists. - * - * @param string $name - * - * @return \PhpOption\Option - */ - public function get($name) - { - if (array_key_exists($name, $this->variables)) { - return Some::create($this->variables[$name]); - } - - return None::create(); - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - public function set($name, $value = null) - { - $this->variables[$name] = $value; - } - - /** - * Clear an environment variable. - * - * @param string $name - * - * @return void - */ - public function clear($name) - { - unset($this->variables[$name]); - } -} diff --git a/src/Environment/Adapter/EnvConstAdapter.php b/src/Environment/Adapter/EnvConstAdapter.php deleted file mode 100644 index 813d0e37..00000000 --- a/src/Environment/Adapter/EnvConstAdapter.php +++ /dev/null @@ -1,60 +0,0 @@ -adapters = array_filter($adapters === null ? [new ApacheAdapter(), new EnvConstAdapter(), new ServerConstAdapter(), new PutenvAdapter()] : $adapters, function (AdapterInterface $adapter) { - return $adapter->isSupported(); - }); - } - - /** - * Creates a new mutable environment variables instance. - * - * @return \Dotenv\Environment\VariablesInterface - */ - public function create() - { - return new DotenvVariables($this->adapters, false); - } - - /** - * Creates a new immutable environment variables instance. - * - * @return \Dotenv\Environment\VariablesInterface - */ - public function createImmutable() - { - return new DotenvVariables($this->adapters, true); - } -} diff --git a/src/Environment/DotenvVariables.php b/src/Environment/DotenvVariables.php deleted file mode 100644 index 486f0cdd..00000000 --- a/src/Environment/DotenvVariables.php +++ /dev/null @@ -1,78 +0,0 @@ -adapters = $adapters; - parent::__construct($immutable); - } - - /** - * Get an environment variable. - * - * We do this by querying our adapters sequentially. - * - * @param string $name - * - * @return string|null - */ - protected function getInternal($name) - { - foreach ($this->adapters as $adapter) { - $result = $adapter->get($name); - if ($result->isDefined()) { - return $result->get(); - } - } - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - protected function setInternal($name, $value = null) - { - foreach ($this->adapters as $adapter) { - $adapter->set($name, $value); - } - } - - /** - * Clear an environment variable. - * - * @param string $name - * - * @return void - */ - protected function clearInternal($name) - { - foreach ($this->adapters as $adapter) { - $adapter->clear($name); - } - } -} diff --git a/src/Environment/FactoryInterface.php b/src/Environment/FactoryInterface.php deleted file mode 100644 index 3d9f489b..00000000 --- a/src/Environment/FactoryInterface.php +++ /dev/null @@ -1,26 +0,0 @@ -filePaths = $filePaths; - $this->envFactory = $envFactory; - $this->setImmutable($immutable); - } - - /** - * Set immutable value. - * - * @param bool $immutable - * - * @return $this - */ - public function setImmutable($immutable = false) - { - $this->envVariables = $immutable - ? $this->envFactory->createImmutable() - : $this->envFactory->create(); - - return $this; - } - - /** - * Load the environment file from disk. - * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function load() - { - return $this->loadDirect( - self::findAndRead($this->filePaths) - ); - } - - /** - * Directly load the given string. - * - * @param string $content - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function loadDirect($content) - { - return $this->processEntries( - Lines::process(preg_split("/(\r\n|\n|\r)/", $content)) - ); - } - - /** - * Attempt to read the files in order. - * - * @param string[] $filePaths - * - * @throws \Dotenv\Exception\InvalidPathException - * - * @return string[] - */ - private static function findAndRead(array $filePaths) - { - if ($filePaths === []) { - throw new InvalidPathException('At least one environment file path must be provided.'); - } - - foreach ($filePaths as $filePath) { - $lines = self::readFromFile($filePath); - if ($lines->isDefined()) { - return $lines->get(); - } - } - - throw new InvalidPathException( - sprintf('Unable to read any of the environment file(s) at [%s].', implode(', ', $filePaths)) - ); - } - - /** - * Read the given file. - * - * @param string $filePath - * - * @return \PhpOption\Option - */ - private static function readFromFile($filePath) - { - $content = @file_get_contents($filePath); - - return Option::fromValue($content, false); - } - - /** - * Process the environment variable entries. - * - * We'll fill out any nested variables, and acually set the variable using - * the underlying environment variables instance. - * - * @param string[] $entries - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return array - */ - private function processEntries(array $entries) - { - $vars = []; - - foreach ($entries as $entry) { - list($name, $value) = Parser::parse($entry); - $vars[$name] = $this->resolveNestedVariables($value); - $this->setEnvironmentVariable($name, $vars[$name]); - } - - return $vars; - } - - /** - * Resolve the nested variables. - * - * Look for ${varname} patterns in the variable value and replace with an - * existing environment variable. - * - * @param string|null $value - * - * @return string|null - */ - private function resolveNestedVariables($value = null) - { - return Option::fromValue($value) - ->filter(function ($str) { - return strpos($str, '$') !== false; - }) - ->flatMap(function ($str) { - return Regex::replaceCallback( - '/\${([a-zA-Z0-9_.]+)}/', - function (array $matches) { - return Option::fromValue($this->getEnvironmentVariable($matches[1])) - ->getOrElse($matches[0]); - }, - $str - )->success(); - }) - ->getOrElse($value); - } - - /** - * Search the different places for environment variables and return first value found. - * - * @param string $name - * - * @return string|null - */ - public function getEnvironmentVariable($name) - { - return $this->envVariables->get($name); - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - public function setEnvironmentVariable($name, $value = null) - { - $this->variableNames[] = $name; - $this->envVariables->set($name, $value); - } - - /** - * Clear an environment variable. - * - * This method only expects names in normal form. - * - * @param string $name - * - * @return void - */ - public function clearEnvironmentVariable($name) - { - $this->envVariables->clear($name); - } - - /** - * Get the list of environment variables names. - * - * @return string[] - */ - public function getEnvironmentVariableNames() - { - return $this->variableNames; - } -} diff --git a/src/Loader/Loader.php b/src/Loader/Loader.php new file mode 100644 index 00000000..22a50af7 --- /dev/null +++ b/src/Loader/Loader.php @@ -0,0 +1,48 @@ + + */ + public function load(RepositoryInterface $repository, array $entries) + { + /** @var array */ + return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { + $name = $entry->getName(); + + $value = $entry->getValue()->map(static function (Value $value) use ($repository) { + return Resolver::resolve($repository, $value); + }); + + if ($value->isDefined()) { + $inner = $value->get(); + if ($repository->set($name, $inner)) { + return \array_merge($vars, [$name => $inner]); + } + } else { + if ($repository->clear($name)) { + return \array_merge($vars, [$name => null]); + } + } + + return $vars; + }, []); + } +} diff --git a/src/Loader/LoaderInterface.php b/src/Loader/LoaderInterface.php new file mode 100644 index 00000000..f40d6ad6 --- /dev/null +++ b/src/Loader/LoaderInterface.php @@ -0,0 +1,20 @@ + + */ + public function load(RepositoryInterface $repository, array $entries); +} diff --git a/src/Loader/Resolver.php b/src/Loader/Resolver.php new file mode 100644 index 00000000..ab5adf88 --- /dev/null +++ b/src/Loader/Resolver.php @@ -0,0 +1,65 @@ +getVars(), static function (string $s, int $i) use ($repository) { + return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); + }, $value->getChars()); + } + + /** + * Resolve a single nested variable. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string $str + * + * @return string + */ + private static function resolveVariable(RepositoryInterface $repository, string $str) + { + return Regex::replaceCallback( + '/\A\${([a-zA-Z0-9_.]+)}/', + static function (array $matches) use ($repository) { + /** @var string */ + return Option::fromValue($repository->get($matches[1]))->getOrElse($matches[0]); + }, + $str, + 1 + )->success()->getOrElse($str); + } +} diff --git a/src/Parser.php b/src/Parser.php deleted file mode 100644 index c8f91f72..00000000 --- a/src/Parser.php +++ /dev/null @@ -1,185 +0,0 @@ -success()->getOrElse(0) === 1; - } - - /** - * Strips quotes and comments from the environment variable value. - * - * @param string|null $value - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return string|null - */ - private static function parseValue($value) - { - if ($value === null || trim($value) === '') { - return $value; - } - - $result = array_reduce(str_split($value), function ($data, $char) use ($value) { - switch ($data[1]) { - case self::INITIAL_STATE: - if ($char === '"' || $char === '\'') { - return [$data[0], self::QUOTED_STATE]; - } elseif ($char === '#') { - return [$data[0], self::COMMENT_STATE]; - } else { - return [$data[0].$char, self::UNQUOTED_STATE]; - } - case self::UNQUOTED_STATE: - if ($char === '#') { - return [$data[0], self::COMMENT_STATE]; - } elseif (ctype_space($char)) { - return [$data[0], self::WHITESPACE_STATE]; - } else { - return [$data[0].$char, self::UNQUOTED_STATE]; - } - case self::QUOTED_STATE: - if ($char === $value[0]) { - return [$data[0], self::WHITESPACE_STATE]; - } elseif ($char === '\\') { - return [$data[0], self::ESCAPE_STATE]; - } else { - return [$data[0].$char, self::QUOTED_STATE]; - } - case self::ESCAPE_STATE: - if ($char === $value[0] || $char === '\\') { - return [$data[0].$char, self::QUOTED_STATE]; - } elseif (in_array($char, ['f', 'n', 'r', 't', 'v'], true)) { - return [$data[0].stripcslashes('\\'.$char), self::QUOTED_STATE]; - } else { - throw new InvalidFileException( - self::getErrorMessage('an unexpected escape sequence', $value) - ); - } - case self::WHITESPACE_STATE: - if ($char === '#') { - return [$data[0], self::COMMENT_STATE]; - } elseif (!ctype_space($char)) { - throw new InvalidFileException( - self::getErrorMessage('unexpected whitespace', $value) - ); - } else { - return [$data[0], self::WHITESPACE_STATE]; - } - case self::COMMENT_STATE: - return [$data[0], self::COMMENT_STATE]; - } - }, ['', self::INITIAL_STATE]); - - if ($result[1] === self::QUOTED_STATE || $result[1] === self::ESCAPE_STATE) { - throw new InvalidFileException( - self::getErrorMessage('a missing closing quote', $value) - ); - } - - return $result[0]; - } - - /** - * Generate a friendly error message. - * - * @param string $cause - * @param string $subject - * - * @return string - */ - private static function getErrorMessage($cause, $subject) - { - return sprintf( - 'Failed to parse dotenv file due to %s. Failed at [%s].', - $cause, - strtok($subject, "\n") - ); - } -} diff --git a/src/Parser/Entry.php b/src/Parser/Entry.php new file mode 100644 index 00000000..716f422e --- /dev/null +++ b/src/Parser/Entry.php @@ -0,0 +1,59 @@ +name = $name; + $this->value = $value; + } + + /** + * Get the entry name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the entry value. + * + * @return \PhpOption\Option<\Dotenv\Parser\Value> + */ + public function getValue() + { + /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ + return Option::fromValue($this->value); + } +} diff --git a/src/Parser/EntryParser.php b/src/Parser/EntryParser.php new file mode 100644 index 00000000..85e5fa3f --- /dev/null +++ b/src/Parser/EntryParser.php @@ -0,0 +1,300 @@ + + */ + public static function parse(string $entry) + { + return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { + [$name, $value] = $parts; + + return self::parseName($name)->flatMap(static function (string $name) use ($value) { + /** @var Result */ + $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); + + return $parsedValue->map(static function (?Value $value) use ($name) { + return new Entry($name, $value); + }); + }); + }); + } + + /** + * Split the compound string into parts. + * + * @param string $line + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function splitStringIntoParts(string $line) + { + /** @var array{string, string|null} */ + $result = Str::pos($line, '=')->map(static function () use ($line) { + return \array_map('trim', \explode('=', $line, 2)); + })->getOrElse([$line, null]); + + if ($result[0] === '') { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an unexpected equals', $line)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } + + /** + * Parse the given variable name. + * + * That is, strip the optional quotes and leading "export" from the + * variable name. We wrap the answer in a result type. + * + * @param string $name + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function parseName(string $name) + { + if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { + $name = \ltrim(Str::substr($name, 6)); + } + + if (self::isQuotedName($name)) { + $name = Str::substr($name, 1, -1); + } + + if (!self::isValidName($name)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an invalid name', $name)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($name); + } + + /** + * Is the given variable name quoted? + * + * @param string $name + * + * @return bool + */ + private static function isQuotedName(string $name) + { + if (Str::len($name) < 3) { + return false; + } + + $first = Str::substr($name, 0, 1); + $last = Str::substr($name, -1, 1); + + return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); + } + + /** + * Is the given variable name valid? + * + * @param string $name + * + * @return bool + */ + private static function isValidName(string $name) + { + return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false); + } + + /** + * Parse the given variable value. + * + * This has the effect of stripping quotes and comments, dealing with + * special characters, and locating nested variables, but not resolving + * them. Formally, we run a finite state automaton with an output tape: a + * transducer. We wrap the answer in a result type. + * + * @param string $value + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> + */ + private static function parseValue(string $value) + { + if (\trim($value) === '') { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Success::create(Value::blank()); + } + + return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { + return $data->flatMap(static function (array $data) use ($token) { + return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { + return [$data[0]->append($val[0], $val[1]), $val[2]]; + }); + }); + }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { + /** @psalm-suppress DocblockTypeContradiction */ + if (in_array($result[1], self::REJECT_STATES, true)) { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Error::create('a missing closing quote'); + } + + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Success::create($result[0]); + })->mapError(static function (string $err) use ($value) { + return self::getErrorMessage($err, $value); + }); + } + + /** + * Process the given token. + * + * @param int $state + * @param string $token + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function processToken(int $state, string $token) + { + switch ($state) { + case self::INITIAL_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::SINGLE_QUOTED_STATE]); + } elseif ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::UNQUOTED_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::SINGLE_QUOTED_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); + } + case self::DOUBLE_QUOTED_STATE: + if ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } + case self::ESCAPE_SEQUENCE_STATE: + if ($token === '"' || $token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } else { + $first = Str::substr($token, 0, 1); + if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('an unexpected escape sequence'); + } + } + case self::WHITESPACE_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (!\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('unexpected whitespace'); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } + case self::COMMENT_STATE: + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + default: + throw new \Error('Parser entered invalid state.'); + } + } + + /** + * Generate a friendly error message. + * + * @param string $cause + * @param string $subject + * + * @return string + */ + private static function getErrorMessage(string $cause, string $subject) + { + return \sprintf( + 'Encountered %s at [%s].', + $cause, + \strtok($subject, "\n") + ); + } +} diff --git a/src/Parser/Lexer.php b/src/Parser/Lexer.php new file mode 100644 index 00000000..981af24f --- /dev/null +++ b/src/Parser/Lexer.php @@ -0,0 +1,58 @@ + + */ + public static function lex(string $content) + { + static $regex; + + if ($regex === null) { + $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; + } + + $offset = 0; + + while (isset($content[$offset])) { + if (!\preg_match($regex, $content, $matches, 0, $offset)) { + throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); + } + + $offset += \strlen($matches[0]); + + yield $matches[0]; + } + } +} diff --git a/src/Lines.php b/src/Parser/Lines.php similarity index 50% rename from src/Lines.php rename to src/Parser/Lines.php index 05c534bb..b3af1605 100644 --- a/src/Lines.php +++ b/src/Parser/Lines.php @@ -1,13 +1,30 @@ map(static function () use ($line) { + return self::looksLikeMultilineStop($line, true) === false; + })->getOrElse(false); } /** @@ -83,35 +100,15 @@ private static function looksLikeMultilineStart($line) * * @return bool */ - private static function looksLikeMultilineStop($line, $started) + private static function looksLikeMultilineStop(string $line, bool $started) { if ($line === '"') { return true; } - $seen = $started ? 0 : 1; - - foreach (self::getCharPairs(str_replace('\\\\', '', $line)) as $pair) { - if ($pair[0] !== '\\' && $pair[1] === '"') { - $seen++; - } - } - - return $seen > 1; - } - - /** - * Get all pairs of adjacent characters within the line. - * - * @param string $line - * - * @return bool - */ - private static function getCharPairs($line) - { - $chars = str_split($line); - - return array_map(null, $chars, array_slice($chars, 1)); + return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { + return $started ? $count > 1 : $count >= 1; + })->success()->getOrElse(false); } /** @@ -121,14 +118,10 @@ private static function getCharPairs($line) * * @return bool */ - private static function isCommentOrWhitespace($line) + private static function isCommentOrWhitespace(string $line) { - if (trim($line) === '') { - return true; - } - - $line = ltrim($line); + $line = \trim($line); - return isset($line[0]) && $line[0] === '#'; + return $line === '' || (isset($line[0]) && $line[0] === '#'); } } diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php new file mode 100644 index 00000000..bca8ec5a --- /dev/null +++ b/src/Parser/Parser.php @@ -0,0 +1,53 @@ +mapError(static function () { + return 'Could not split into separate lines.'; + })->flatMap(static function (array $lines) { + return self::process(Lines::process($lines)); + })->mapError(static function (string $error) { + throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); + })->success()->get(); + } + + /** + * Convert the raw entries into proper entries. + * + * @param string[] $entries + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> + */ + private static function process(array $entries) + { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> */ + return \array_reduce($entries, static function (Result $result, string $raw) { + return $result->flatMap(static function (array $entries) use ($raw) { + return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { + /** @var \Dotenv\Parser\Entry[] */ + return \array_merge($entries, [$entry]); + }); + }); + }, Success::create([])); + } +} diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 00000000..17cc42ad --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,19 @@ +chars = $chars; + $this->vars = $vars; + } + + /** + * Create an empty value instance. + * + * @return \Dotenv\Parser\Value + */ + public static function blank() + { + return new self('', []); + } + + /** + * Create a new value instance, appending the characters. + * + * @param string $chars + * @param bool $var + * + * @return \Dotenv\Parser\Value + */ + public function append(string $chars, bool $var) + { + return new self( + $this->chars.$chars, + $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars + ); + } + + /** + * Get the string representation of the parsed value. + * + * @return string + */ + public function getChars() + { + return $this->chars; + } + + /** + * Get the locations of the variables in the value. + * + * @return int[] + */ + public function getVars() + { + $vars = $this->vars; + + \rsort($vars); + + return $vars; + } +} diff --git a/src/Regex/Error.php b/src/Regex/Error.php deleted file mode 100644 index 6ad7b5d0..00000000 --- a/src/Regex/Error.php +++ /dev/null @@ -1,82 +0,0 @@ -value = $value; - } - - /** - * Create a new error value. - * - * @param string $value - * - * @return \Dotenv\Regex\Result - */ - public static function create($value) - { - return new self($value); - } - - /** - * Get the success option value. - * - * @return \PhpOption\Option - */ - public function success() - { - return None::create(); - } - - /** - * Map over the success value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - public function mapSuccess(callable $f) - { - return self::create($this->value); - } - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - public function error() - { - return Some::create($this->value); - } - - /** - * Map over the error value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - public function mapError(callable $f) - { - return self::create($f($this->value)); - } -} diff --git a/src/Regex/Regex.php b/src/Regex/Regex.php deleted file mode 100644 index ead817fe..00000000 --- a/src/Regex/Regex.php +++ /dev/null @@ -1,101 +0,0 @@ -success()->get(); - } - - /** - * Map over the success value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - abstract public function mapSuccess(callable $f); - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - abstract public function error(); - - /** - * Get the error value, if possible. - * - * @return string - */ - public function getError() - { - return $this->error()->get(); - } - - /** - * Map over the error value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - abstract public function mapError(callable $f); -} diff --git a/src/Regex/Success.php b/src/Regex/Success.php deleted file mode 100644 index 3f06e968..00000000 --- a/src/Regex/Success.php +++ /dev/null @@ -1,82 +0,0 @@ -value = $value; - } - - /** - * Create a new success value. - * - * @param string|int $value - * - * @return \Dotenv\Regex\Result - */ - public static function create($value) - { - return new self($value); - } - - /** - * Get the success option value. - * - * @return \PhpOption\Option - */ - public function success() - { - return Some::create($this->value); - } - - /** - * Map over the success value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - public function mapSuccess(callable $f) - { - return self::create($f($this->value)); - } - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - public function error() - { - return None::create(); - } - - /** - * Map over the error value. - * - * @param callable $f - * - * @return \Dotenv\Regex\Result - */ - public function mapError(callable $f) - { - return self::create($this->value); - } -} diff --git a/src/Repository/Adapter/AdapterInterface.php b/src/Repository/Adapter/AdapterInterface.php new file mode 100644 index 00000000..5604398a --- /dev/null +++ b/src/Repository/Adapter/AdapterInterface.php @@ -0,0 +1,15 @@ + + */ + public static function create(); +} diff --git a/src/Repository/Adapter/ApacheAdapter.php b/src/Repository/Adapter/ApacheAdapter.php new file mode 100644 index 00000000..af0aae11 --- /dev/null +++ b/src/Repository/Adapter/ApacheAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * This happens if PHP is running as an Apache module. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('apache_getenv') && \function_exists('apache_setenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(apache_getenv($name))->filter(static function ($value) { + return \is_string($value) && $value !== ''; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + return apache_setenv($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + return apache_setenv($name, ''); + } +} diff --git a/src/Repository/Adapter/ArrayAdapter.php b/src/Repository/Adapter/ArrayAdapter.php new file mode 100644 index 00000000..7c3740d8 --- /dev/null +++ b/src/Repository/Adapter/ArrayAdapter.php @@ -0,0 +1,80 @@ + + */ + private $variables; + + /** + * Create a new array adapter instance. + * + * @return void + */ + private function __construct() + { + $this->variables = []; + } + + /** + * Create a new instance of the adapter, if it is available. + * + * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + return Option::fromArraysValue($this->variables, $name); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $this->variables[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($this->variables[$name]); + + return true; + } +} diff --git a/src/Repository/Adapter/EnvConstAdapter.php b/src/Repository/Adapter/EnvConstAdapter.php new file mode 100644 index 00000000..9eb19477 --- /dev/null +++ b/src/Repository/Adapter/EnvConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_ENV, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_ENV[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_ENV[$name]); + + return true; + } +} diff --git a/src/Repository/Adapter/GuardedWriter.php b/src/Repository/Adapter/GuardedWriter.php new file mode 100644 index 00000000..fed8b9ba --- /dev/null +++ b/src/Repository/Adapter/GuardedWriter.php @@ -0,0 +1,85 @@ +writer = $writer; + $this->allowList = $allowList; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't set non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->write($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->delete($name); + } + + /** + * Determine if the given variable is allowed. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isAllowed(string $name) + { + return \in_array($name, $this->allowList, true); + } +} diff --git a/src/Repository/Adapter/ImmutableWriter.php b/src/Repository/Adapter/ImmutableWriter.php new file mode 100644 index 00000000..3b279b89 --- /dev/null +++ b/src/Repository/Adapter/ImmutableWriter.php @@ -0,0 +1,110 @@ + + */ + private $loaded; + + /** + * Create a new immutable writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->loaded = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't overwrite existing environment variables + // Ruby's dotenv does this with `ENV[key] ||= value` + if ($this->isExternallyDefined($name)) { + return false; + } + + // Set the value on the inner writer + if (!$this->writer->write($name, $value)) { + return false; + } + + // Record that we have loaded the variable + $this->loaded[$name] = ''; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear existing environment variables + if ($this->isExternallyDefined($name)) { + return false; + } + + // Clear the value on the inner writer + if (!$this->writer->delete($name)) { + return false; + } + + // Leave the variable as fair game + unset($this->loaded[$name]); + + return true; + } + + /** + * Determine if the given variable is externally defined. + * + * That is, is it an "existing" variable. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isExternallyDefined(string $name) + { + return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); + } +} diff --git a/src/Repository/Adapter/MultiReader.php b/src/Repository/Adapter/MultiReader.php new file mode 100644 index 00000000..0cfda6f6 --- /dev/null +++ b/src/Repository/Adapter/MultiReader.php @@ -0,0 +1,48 @@ +readers = $readers; + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + foreach ($this->readers as $reader) { + $result = $reader->read($name); + if ($result->isDefined()) { + return $result; + } + } + + return None::create(); + } +} diff --git a/src/Repository/Adapter/MultiWriter.php b/src/Repository/Adapter/MultiWriter.php new file mode 100644 index 00000000..15a9d8fd --- /dev/null +++ b/src/Repository/Adapter/MultiWriter.php @@ -0,0 +1,64 @@ +writers = $writers; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + foreach ($this->writers as $writers) { + if (!$writers->write($name, $value)) { + return false; + } + } + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + foreach ($this->writers as $writers) { + if (!$writers->delete($name)) { + return false; + } + } + + return true; + } +} diff --git a/src/Repository/Adapter/PutenvAdapter.php b/src/Repository/Adapter/PutenvAdapter.php new file mode 100644 index 00000000..6d017cdb --- /dev/null +++ b/src/Repository/Adapter/PutenvAdapter.php @@ -0,0 +1,91 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('getenv') && \function_exists('putenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\getenv($name), false)->filter(static function ($value) { + return \is_string($value); + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + \putenv("$name=$value"); + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + \putenv($name); + + return true; + } +} diff --git a/src/Repository/Adapter/ReaderInterface.php b/src/Repository/Adapter/ReaderInterface.php new file mode 100644 index 00000000..306a63fc --- /dev/null +++ b/src/Repository/Adapter/ReaderInterface.php @@ -0,0 +1,17 @@ + + */ + public function read(string $name); +} diff --git a/src/Repository/Adapter/ReplacingWriter.php b/src/Repository/Adapter/ReplacingWriter.php new file mode 100644 index 00000000..4c92a4f2 --- /dev/null +++ b/src/Repository/Adapter/ReplacingWriter.php @@ -0,0 +1,104 @@ + + */ + private $seen; + + /** + * Create a new replacement writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->seen = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + if ($this->exists($name)) { + return $this->writer->write($name, $value); + } + + // succeed if nothing to do + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + if ($this->exists($name)) { + return $this->writer->delete($name); + } + + // succeed if nothing to do + return true; + } + + /** + * Does the given environment variable exist. + * + * Returns true if it currently exists, or existed at any point in the past + * that we are aware of. + * + * @param non-empty-string $name + * + * @return bool + */ + private function exists(string $name) + { + if (isset($this->seen[$name])) { + return true; + } + + if ($this->reader->read($name)->isDefined()) { + $this->seen[$name] = ''; + + return true; + } + + return false; + } +} diff --git a/src/Repository/Adapter/ServerConstAdapter.php b/src/Repository/Adapter/ServerConstAdapter.php new file mode 100644 index 00000000..f93b6e5e --- /dev/null +++ b/src/Repository/Adapter/ServerConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_SERVER, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_SERVER[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_SERVER[$name]); + + return true; + } +} diff --git a/src/Repository/Adapter/WriterInterface.php b/src/Repository/Adapter/WriterInterface.php new file mode 100644 index 00000000..4cb3d61f --- /dev/null +++ b/src/Repository/Adapter/WriterInterface.php @@ -0,0 +1,27 @@ +reader = $reader; + $this->writer = $writer; + } + + /** + * Determine if the given environment variable is defined. + * + * @param string $name + * + * @return bool + */ + public function has(string $name) + { + return '' !== $name && $this->reader->read($name)->isDefined(); + } + + /** + * Get an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return string|null + */ + public function get(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->reader->read($name)->getOrElse(null); + } + + /** + * Set an environment variable. + * + * @param string $name + * @param string $value + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function set(string $name, string $value) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->write($name, $value); + } + + /** + * Clear an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function clear(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->delete($name); + } +} diff --git a/src/Repository/RepositoryBuilder.php b/src/Repository/RepositoryBuilder.php new file mode 100644 index 00000000..76079244 --- /dev/null +++ b/src/Repository/RepositoryBuilder.php @@ -0,0 +1,272 @@ +readers = $readers; + $this->writers = $writers; + $this->immutable = $immutable; + $this->allowList = $allowList; + } + + /** + * Create a new repository builder instance with no adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithNoAdapters() + { + return new self(); + } + + /** + * Create a new repository builder instance with the default adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithDefaultAdapters() + { + $adapters = \iterator_to_array(self::defaultAdapters()); + + return new self($adapters, $adapters); + } + + /** + * Return the array of default adapters. + * + * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> + */ + private static function defaultAdapters() + { + foreach (self::DEFAULT_ADAPTERS as $adapter) { + $instance = $adapter::create(); + if ($instance->isDefined()) { + yield $instance->get(); + } + } + } + + /** + * Determine if the given name if of an adapterclass. + * + * @param string $name + * + * @return bool + */ + private static function isAnAdapterClass(string $name) + { + if (!\class_exists($name)) { + return false; + } + + return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); + } + + /** + * Creates a repository builder with the given reader added. + * + * Accepts either a reader instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addReader($reader) + { + if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + ReaderInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($reader)->flatMap(static function ($reader) { + return \is_string($reader) ? $reader::create() : Some::create($reader); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + + return new self($readers, $this->writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given writer added. + * + * Accepts either a writer instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addWriter($writer) + { + if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($writer)->flatMap(static function ($writer) { + return \is_string($writer) ? $writer::create() : Some::create($writer); + }); + + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($this->readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given adapter added. + * + * Accepts either an adapter instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. We will + * add the adapter as both a reader and a writer. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addAdapter($adapter) + { + if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($adapter)->flatMap(static function ($adapter) { + return \is_string($adapter) ? $adapter::create() : Some::create($adapter); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with mutability enabled. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function immutable() + { + return new self($this->readers, $this->writers, true, $this->allowList); + } + + /** + * Creates a repository builder with the given allow list. + * + * @param string[]|null $allowList + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function allowList(?array $allowList = null) + { + return new self($this->readers, $this->writers, $this->immutable, $allowList); + } + + /** + * Creates a new repository instance. + * + * @return \Dotenv\Repository\RepositoryInterface + */ + public function make() + { + $reader = new MultiReader($this->readers); + $writer = new MultiWriter($this->writers); + + if ($this->immutable) { + $writer = new ImmutableWriter($writer, $reader); + } + + if ($this->allowList !== null) { + $writer = new GuardedWriter($writer, $this->allowList); + } + + return new AdapterRepository($reader, $writer); + } +} diff --git a/src/Repository/RepositoryInterface.php b/src/Repository/RepositoryInterface.php new file mode 100644 index 00000000..d9b18a40 --- /dev/null +++ b/src/Repository/RepositoryInterface.php @@ -0,0 +1,51 @@ + + */ + public static function read(array $filePaths, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $output = []; + + foreach ($filePaths as $filePath) { + $content = self::readFromFile($filePath, $fileEncoding); + if ($content->isDefined()) { + $output[$filePath] = $content->get(); + if ($shortCircuit) { + break; + } + } + } + + return $output; + } + + /** + * Read the given file. + * + * @param string $path + * @param string|null $encoding + * + * @throws \Dotenv\Exception\InvalidEncodingException + * + * @return \PhpOption\Option + */ + private static function readFromFile(string $path, ?string $encoding = null) + { + /** @var Option */ + $content = Option::fromValue(@\file_get_contents($path), false); + + return $content->flatMap(static function (string $content) use ($encoding) { + return Str::utf8($content, $encoding)->mapError(static function (string $error) { + throw new InvalidEncodingException($error); + })->success(); + }); + } +} diff --git a/src/Store/FileStore.php b/src/Store/FileStore.php new file mode 100644 index 00000000..e7a4d3f1 --- /dev/null +++ b/src/Store/FileStore.php @@ -0,0 +1,72 @@ +filePaths = $filePaths; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Read the content of the environment file(s). + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException + * + * @return string + */ + public function read() + { + if ($this->filePaths === []) { + throw new InvalidPathException('At least one environment file path must be provided.'); + } + + $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); + + if (\count($contents) > 0) { + return \implode("\n", $contents); + } + + throw new InvalidPathException( + \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) + ); + } +} diff --git a/src/Store/StoreBuilder.php b/src/Store/StoreBuilder.php new file mode 100644 index 00000000..2ce08515 --- /dev/null +++ b/src/Store/StoreBuilder.php @@ -0,0 +1,141 @@ +paths = $paths; + $this->names = $names; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Create a new store builder instance with no names. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithNoNames() + { + return new self(); + } + + /** + * Create a new store builder instance with the default name. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithDefaultName() + { + return new self([], [self::DEFAULT_NAME]); + } + + /** + * Creates a store builder with the given path added. + * + * @param string $path + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addPath(string $path) + { + return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with the given name added. + * + * @param string $name + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addName(string $name) + { + return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with short circuit mode enabled. + * + * @return \Dotenv\Store\StoreBuilder + */ + public function shortCircuit() + { + return new self($this->paths, $this->names, true, $this->fileEncoding); + } + + /** + * Creates a store builder with the specified file encoding. + * + * @param string|null $fileEncoding + * + * @return \Dotenv\Store\StoreBuilder + */ + public function fileEncoding(?string $fileEncoding = null) + { + return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); + } + + /** + * Creates a new store instance. + * + * @return \Dotenv\Store\StoreInterface + */ + public function make() + { + return new FileStore( + Paths::filePaths($this->paths, $this->names), + $this->shortCircuit, + $this->fileEncoding + ); + } +} diff --git a/src/Store/StoreInterface.php b/src/Store/StoreInterface.php new file mode 100644 index 00000000..6f5b9862 --- /dev/null +++ b/src/Store/StoreInterface.php @@ -0,0 +1,17 @@ +content = $content; + } + + /** + * Read the content of the environment file(s). + * + * @return string + */ + public function read() + { + return $this->content; + } +} diff --git a/src/Util/Regex.php b/src/Util/Regex.php new file mode 100644 index 00000000..599f09cd --- /dev/null +++ b/src/Util/Regex.php @@ -0,0 +1,112 @@ + + */ + public static function matches(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return @\preg_match($pattern, $subject) === 1; + }, $subject); + } + + /** + * Perform a preg match all, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function occurrences(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return (int) @\preg_match_all($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg replace callback, wrapping up the result. + * + * @param string $pattern + * @param callable(string[]): string $callback + * @param string $subject + * @param int|null $limit + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function replaceCallback(string $pattern, callable $callback, string $subject, ?int $limit = null) + { + return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { + return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); + }, $subject); + } + + /** + * Perform a preg split, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function split(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + /** @var string[] */ + return (array) @\preg_split($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg operation, wrapping up the result. + * + * @template V + * + * @param callable(string): V $operation + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function pregAndWrap(callable $operation, string $subject) + { + $result = $operation($subject); + + if (\preg_last_error() !== \PREG_NO_ERROR) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(\preg_last_error_msg()); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } +} diff --git a/src/Util/Str.php b/src/Util/Str.php new file mode 100644 index 00000000..b113d78c --- /dev/null +++ b/src/Util/Str.php @@ -0,0 +1,108 @@ + + */ + public static function utf8(string $input, ?string $encoding = null) + { + if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create( + \sprintf('Illegal character encoding [%s] specified.', $encoding) + ); + } + + $converted = $encoding === null ? + @\mb_convert_encoding($input, 'UTF-8') : + @\mb_convert_encoding($input, 'UTF-8', $encoding); + + if (!is_string($converted)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create( + \sprintf('Conversion from encoding [%s] failed.', $encoding ?? 'NULL') + ); + } + + /** + * this is for support UTF-8 with BOM encoding + * @see https://en.wikipedia.org/wiki/Byte_order_mark + * @see https://github.com/vlucas/phpdotenv/issues/500 + */ + if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { + $converted = \substr($converted, 3); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($converted); + } + + /** + * Search for a given substring of the input. + * + * @param string $haystack + * @param string $needle + * + * @return \PhpOption\Option + */ + public static function pos(string $haystack, string $needle) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); + } + + /** + * Grab the specified substring of the input. + * + * @param string $input + * @param int $start + * @param int|null $length + * + * @return string + */ + public static function substr(string $input, int $start, ?int $length = null) + { + return \mb_substr($input, $start, $length, 'UTF-8'); + } + + /** + * Compute the length of the given string. + * + * @param string $input + * + * @return int + */ + public static function len(string $input) + { + return \mb_strlen($input, 'UTF-8'); + } +} diff --git a/src/Validator.php b/src/Validator.php index 39d2fe4d..d5580c7f 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -1,55 +1,59 @@ repository = $repository; $this->variables = $variables; - $this->loader = $loader; - - if ($required) { - $this->assertCallback( - function ($value) { - return $value !== null; - }, - 'is missing' - ); - } + } + + /** + * Assert that each variable is present. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function required() + { + return $this->assert( + static function (?string $value) { + return $value !== null; + }, + 'is missing' + ); } /** @@ -61,13 +65,9 @@ function ($value) { */ public function notEmpty() { - return $this->assertCallback( - function ($value) { - if ($value === null) { - return true; - } - - return strlen(trim($value)) > 0; + return $this->assertNullable( + static function (string $value) { + return Str::len(\trim($value)) > 0; }, 'is empty' ); @@ -82,13 +82,9 @@ function ($value) { */ public function isInteger() { - return $this->assertCallback( - function ($value) { - if ($value === null) { - return true; - } - - return ctype_digit($value); + return $this->assertNullable( + static function (string $value) { + return \ctype_digit($value); }, 'is not an integer' ); @@ -103,17 +99,13 @@ function ($value) { */ public function isBoolean() { - return $this->assertCallback( - function ($value) { - if ($value === null) { - return true; - } - + return $this->assertNullable( + static function (string $value) { if ($value === '') { return false; } - return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null; + return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; }, 'is not a boolean' ); @@ -130,15 +122,11 @@ function ($value) { */ public function allowedValues(array $choices) { - return $this->assertCallback( - function ($value) use ($choices) { - if ($value === null) { - return true; - } - - return in_array($value, $choices, true); + return $this->assertNullable( + static function (string $value) use ($choices) { + return \in_array($value, $choices, true); }, - sprintf('is not one of [%s]', implode(', ', $choices)) + \sprintf('is not one of [%s]', \implode(', ', $choices)) ); } @@ -151,47 +139,69 @@ function ($value) use ($choices) { * * @return \Dotenv\Validator */ - public function allowedRegexValues($regex) + public function allowedRegexValues(string $regex) { - return $this->assertCallback( - function ($value) use ($regex) { - if ($value === null) { - return true; - } - - return Regex::match($regex, $value)->success()->getOrElse(0) === 1; + return $this->assertNullable( + static function (string $value) use ($regex) { + return Regex::matches($regex, $value)->success()->getOrElse(false); }, - sprintf('does not match "%s"', $regex) + \sprintf('does not match "%s"', $regex) ); } /** * Assert that the callback returns true for each variable. * - * @param callable $callback - * @param string $message + * @param callable(?string):bool $callback + * @param string $message * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ - protected function assertCallback(callable $callback, $message = 'failed callback assertion') + public function assert(callable $callback, string $message) { $failing = []; foreach ($this->variables as $variable) { - if ($callback($this->loader->getEnvironmentVariable($variable)) === false) { - $failing[] = sprintf('%s %s', $variable, $message); + if ($callback($this->repository->get($variable)) === false) { + $failing[] = \sprintf('%s %s', $variable, $message); } } - if (count($failing) > 0) { - throw new ValidationException(sprintf( + if (\count($failing) > 0) { + throw new ValidationException(\sprintf( 'One or more environment variables failed assertions: %s.', - implode(', ', $failing) + \implode(', ', $failing) )); } return $this; } + + /** + * Assert that the callback returns true for each variable. + * + * Skip checking null variable values. + * + * @param callable(string):bool $callback + * @param string $message + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function assertNullable(callable $callback, string $message) + { + return $this->assert( + static function (?string $value) use ($callback) { + if ($value === null) { + return true; + } + + return $callback($value); + }, + $message + ); + } } diff --git a/tests/Dotenv/DotenvTest.php b/tests/Dotenv/DotenvTest.php index ce0a58f9..97ddfd73 100644 --- a/tests/Dotenv/DotenvTest.php +++ b/tests/Dotenv/DotenvTest.php @@ -1,165 +1,263 @@ fixturesFolder = dirname(__DIR__).'/fixtures/env'; + self::$folder = \dirname(__DIR__).'/fixtures/env'; } - /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage Unable to read any of the environment file(s) at - */ public function testDotenvThrowsExceptionIfUnableToLoadFile() { - $dotenv = Dotenv::create(__DIR__); + $dotenv = Dotenv::createMutable(__DIR__); + + $this->expectException(InvalidPathException::class); + $this->expectExceptionMessage('Unable to read any of the environment file(s) at'); + $dotenv->load(); } - /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage Unable to read any of the environment file(s) at - */ public function testDotenvThrowsExceptionIfUnableToLoadFiles() { - $dotenv = Dotenv::create([__DIR__, __DIR__.'/foo/bar']); + $dotenv = Dotenv::createMutable([__DIR__, __DIR__.'/foo/bar']); + + $this->expectException(InvalidPathException::class); + $this->expectExceptionMessage('Unable to read any of the environment file(s) at'); + + $dotenv->load(); + } + + public function testDotenvThrowsExceptionWhenNoFiles() + { + $dotenv = Dotenv::createMutable([]); + + $this->expectException(InvalidPathException::class); + $this->expectExceptionMessage('At least one environment file path must be provided.'); + $dotenv->load(); } public function testDotenvTriesPathsToLoad() { - $dotenv = Dotenv::create([__DIR__, $this->fixturesFolder]); - $this->assertCount(4, $dotenv->load()); + $dotenv = Dotenv::createMutable([__DIR__, self::$folder]); + self::assertCount(4, $dotenv->load()); + } + + public function testDotenvTriesPathsToLoadTwice() + { + $dotenv = Dotenv::createMutable([__DIR__, self::$folder]); + self::assertCount(4, $dotenv->load()); + + $dotenv = Dotenv::createImmutable([__DIR__, self::$folder]); + self::assertCount(0, $dotenv->load()); + } + + public function testDotenvTriesPathsToSafeLoad() + { + $dotenv = Dotenv::createMutable([__DIR__, self::$folder]); + self::assertCount(4, $dotenv->safeLoad()); } public function testDotenvSkipsLoadingIfFileIsMissing() { - $dotenv = Dotenv::create(__DIR__); - $this->assertSame([], $dotenv->safeLoad()); + $dotenv = Dotenv::createMutable(__DIR__); + self::assertSame([], $dotenv->safeLoad()); } public function testDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); - $this->assertSame( + $dotenv = Dotenv::createMutable(self::$folder); + self::assertSame( ['FOO' => 'bar', 'BAR' => 'baz', 'SPACED' => 'with spaces', 'NULL' => ''], $dotenv->load() ); - $this->assertSame('bar', getenv('FOO')); - $this->assertSame('baz', getenv('BAR')); - $this->assertSame('with spaces', getenv('SPACED')); - $this->assertEmpty(getenv('NULL')); + self::assertSame('bar', $_SERVER['FOO']); + self::assertSame('baz', $_SERVER['BAR']); + self::assertSame('with spaces', $_SERVER['SPACED']); + self::assertEmpty($_SERVER['NULL']); + } + + public function testDotenvLoadsEnvironmentVarsMultipleWithShortCircuitMode() + { + $dotenv = Dotenv::createMutable(self::$folder, ['.env', 'example.env']); + + self::assertSame( + ['FOO' => 'bar', 'BAR' => 'baz', 'SPACED' => 'with spaces', 'NULL' => ''], + $dotenv->load() + ); + } + + public function testDotenvLoadsEnvironmentVarsMultipleWithoutShortCircuitMode() + { + $dotenv = Dotenv::createMutable(self::$folder, ['.env', 'example.env'], false); + + self::assertSame( + ['FOO' => 'bar', 'BAR' => 'baz', 'SPACED' => 'with spaces', 'NULL' => '', 'EG' => 'example'], + $dotenv->load() + ); } public function testCommentedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'commented.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'commented.env'); $dotenv->load(); - $this->assertSame('bar', getenv('CFOO')); - $this->assertFalse(getenv('CBAR')); - $this->assertFalse(getenv('CZOO')); - $this->assertSame('with spaces', getenv('CSPACED')); - $this->assertSame('a value with a # character', getenv('CQUOTES')); - $this->assertSame('a value with a # character & a quote " character inside quotes', getenv('CQUOTESWITHQUOTE')); - $this->assertEmpty(getenv('CNULL')); - $this->assertEmpty(getenv('EMPTY')); - $this->assertEmpty(getenv('EMPTY2')); - $this->assertSame('foo', getenv('FOOO')); + self::assertSame('bar', $_SERVER['CFOO']); + self::assertFalse(isset($_SERVER['CBAR'])); + self::assertFalse(isset($_SERVER['CZOO'])); + self::assertSame('with spaces', $_SERVER['CSPACED']); + self::assertSame('a value with a # character', $_SERVER['CQUOTES']); + self::assertSame('a value with a # character & a quote " character inside quotes', $_SERVER['CQUOTESWITHQUOTE']); + self::assertEmpty($_SERVER['CNULL']); + self::assertEmpty($_SERVER['EMPTY']); + self::assertEmpty($_SERVER['EMPTY2']); + self::assertSame('foo', $_SERVER['FOOO']); } public function testQuotedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'quoted.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'quoted.env'); $dotenv->load(); - $this->assertSame('bar', getenv('QFOO')); - $this->assertSame('baz', getenv('QBAR')); - $this->assertSame('with spaces', getenv('QSPACED')); - $this->assertEmpty(getenv('QNULL')); + self::assertSame('bar', $_SERVER['QFOO']); + self::assertSame('baz', $_SERVER['QBAR']); + self::assertSame('with spaces', $_SERVER['QSPACED']); + self::assertEmpty(\getenv('QNULL')); - $this->assertSame('pgsql:host=localhost;dbname=test', getenv('QEQUALS')); - $this->assertSame('test some escaped characters like a quote (") or maybe a backslash (\\)', getenv('QESCAPED')); - $this->assertSame('iiiiviiiixiiiiviiii\\n', getenv('QSLASH')); - - $this->assertSame('test some escaped characters like a quote (\') or maybe a backslash (\\)', getenv('SQESCAPED')); - $this->assertSame('iiiiviiiixiiiiviiii\\n', getenv('SQSLASH')); + self::assertSame('pgsql:host=localhost;dbname=test', $_SERVER['QEQUALS']); + self::assertSame('test some escaped characters like a quote (") or maybe a backslash (\\)', $_SERVER['QESCAPED']); + self::assertSame('iiiiviiiixiiiiviiii\\n', $_SERVER['QSLASH']); + self::assertSame('iiiiviiiixiiiiviiii\\\\n', $_SERVER['SQSLASH']); } public function testLargeDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'large.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'large.env'); $dotenv->load(); - $this->assertNotEmpty(getenv('LARGE')); + self::assertSame(2730, \strlen($_SERVER['LARGE'])); + self::assertSame(8192, \strlen($_SERVER['HUGE'])); + } + + public function testDotenvLoadsMultibyteVars() + { + $dotenv = Dotenv::createMutable(self::$folder, 'multibyte.env'); + $dotenv->load(); + self::assertSame('Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě', $_SERVER['MB1']); + self::assertSame('行内支付', $_SERVER['MB2']); + self::assertSame('🚀', $_SERVER['APP_ENV']); + } + + public function testDotenvLoadsMultibyteUTF8Vars() + { + $dotenv = Dotenv::createMutable(self::$folder, 'multibyte.env', false, 'UTF-8'); + $dotenv->load(); + self::assertSame('Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě', $_SERVER['MB1']); + self::assertSame('行内支付', $_SERVER['MB2']); + self::assertSame('🚀', $_SERVER['APP_ENV']); + } + + public function testDotenvLoadWithInvalidEncoding() + { + $dotenv = Dotenv::createMutable(self::$folder, 'multibyte.env', false, 'UTF-88'); + + $this->expectException(InvalidEncodingException::class); + $this->expectExceptionMessage('Illegal character encoding [UTF-88] specified.'); + + $dotenv->load(); + } + + public function testDotenvLoadsMultibyteWindowsVars() + { + $dotenv = Dotenv::createMutable(self::$folder, 'windows.env', false, 'Windows-1252'); + $dotenv->load(); + self::assertSame('ñá', $_SERVER['MBW']); } public function testMultipleDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'multiple.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'multiple.env'); $dotenv->load(); - $this->assertSame('bar', getenv('MULTI1')); - $this->assertSame('foo', getenv('MULTI2')); + self::assertSame('bar', $_SERVER['MULTI1']); + self::assertSame('foo', $_SERVER['MULTI2']); } public function testExportedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'exported.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'exported.env'); $dotenv->load(); - $this->assertSame('bar', getenv('EFOO')); - $this->assertSame('baz', getenv('EBAR')); - $this->assertSame('with spaces', getenv('ESPACED')); - $this->assertEmpty(getenv('ENULL')); + self::assertSame('bar', $_SERVER['EFOO']); + self::assertSame('baz', $_SERVER['EBAR']); + self::assertSame('with spaces', $_SERVER['ESPACED']); + self::assertSame('123', $_SERVER['EDQUOTED']); + self::assertSame('456', $_SERVER['ESQUOTED']); + self::assertEmpty($_SERVER['ENULL']); } public function testDotenvLoadsEnvGlobals() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createMutable(self::$folder); $dotenv->load(); - $this->assertSame('bar', $_SERVER['FOO']); - $this->assertSame('baz', $_SERVER['BAR']); - $this->assertSame('with spaces', $_SERVER['SPACED']); - $this->assertEmpty($_SERVER['NULL']); + self::assertSame('bar', $_SERVER['FOO']); + self::assertSame('baz', $_SERVER['BAR']); + self::assertSame('with spaces', $_SERVER['SPACED']); + self::assertEmpty($_SERVER['NULL']); } public function testDotenvLoadsServerGlobals() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createMutable(self::$folder); $dotenv->load(); - $this->assertSame('bar', $_ENV['FOO']); - $this->assertSame('baz', $_ENV['BAR']); - $this->assertSame('with spaces', $_ENV['SPACED']); - $this->assertEmpty($_ENV['NULL']); + self::assertSame('bar', $_ENV['FOO']); + self::assertSame('baz', $_ENV['BAR']); + self::assertSame('with spaces', $_ENV['SPACED']); + self::assertEmpty($_ENV['NULL']); } public function testDotenvNestedEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'nested.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'nested.env'); $dotenv->load(); - $this->assertSame('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved - $this->assertSame('Hello World!', $_ENV['NVAR4']); - $this->assertSame('$NVAR1 {NVAR2}', $_ENV['NVAR5']); // not resolved - $this->assertSame('Special Value', $_ENV['N.VAR6']); // new '.' (dot) in var name - $this->assertSame('Special Value', $_ENV['NVAR7']); // nested '.' (dot) variable - $this->assertSame('', $_ENV['NVAR8']); - $this->assertSame('', $_ENV['NVAR9']); // nested variable is empty string - $this->assertSame('${NVAR888}', $_ENV['NVAR10']); // nested variable is not set + self::assertSame('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved + self::assertSame('Hellō World!', $_ENV['NVAR4']); + self::assertSame('$NVAR1 {NVAR2}', $_ENV['NVAR5']); // not resolved + self::assertSame('Special Value', $_ENV['N.VAR6']); // new '.' (dot) in var name + self::assertSame('Special Value', $_ENV['NVAR7']); // nested '.' (dot) variable + self::assertSame('', $_ENV['NVAR8']); // nested variable is empty string + self::assertSame('', $_ENV['NVAR9']); // nested variable is empty string + self::assertSame('${NVAR888}', $_ENV['NVAR10']); // nested variable is not set + self::assertSame('NVAR1', $_ENV['NVAR11']); + self::assertSame('Hellō', $_ENV['NVAR12']); + self::assertSame('${${NVAR11}}', $_ENV['NVAR13']); // single quotes + self::assertSame('${NVAR1} ${NVAR2}', $_ENV['NVAR14']); // single quotes + self::assertSame('${NVAR1} ${NVAR2}', $_ENV['NVAR15']); // escaped } public function testDotenvNullFileArgumentUsesDefault() { - $dotenv = Dotenv::create($this->fixturesFolder, null); + $dotenv = Dotenv::createMutable(self::$folder, null); $dotenv->load(); - $this->assertSame('bar', getenv('FOO')); + self::assertSame('bar', $_SERVER['FOO']); } /** @@ -169,86 +267,121 @@ public function testDotenvNullFileArgumentUsesDefault() */ public function testDotenvTrimmedKeys() { - $dotenv = Dotenv::create($this->fixturesFolder, 'quoted.env'); + $dotenv = Dotenv::createMutable(self::$folder, 'quoted.env'); $dotenv->load(); - $this->assertSame('no space', getenv('QWHITESPACE')); + self::assertSame('no space', $_SERVER['QWHITESPACE']); } public function testDotenvLoadDoesNotOverwriteEnv() { - putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); + \putenv('IMMUTABLE=true'); + $dotenv = Dotenv::createImmutable(self::$folder, 'immutable.env'); $dotenv->load(); - $this->assertSame('true', getenv('IMMUTABLE')); + self::assertSame('true', \getenv('IMMUTABLE')); } public function testDotenvLoadAfterOverload() { - putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); - $dotenv->overload(); - $this->assertSame('false', getenv('IMMUTABLE')); - - putenv('IMMUTABLE=true'); + \putenv('IMMUTABLE=true'); + $dotenv = Dotenv::createUnsafeMutable(self::$folder, 'immutable.env'); $dotenv->load(); - $this->assertSame('true', getenv('IMMUTABLE')); + self::assertSame('false', \getenv('IMMUTABLE')); } public function testDotenvOverloadAfterLoad() { - putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); + \putenv('IMMUTABLE=true'); + $dotenv = Dotenv::createUnsafeImmutable(self::$folder, 'immutable.env'); $dotenv->load(); - $this->assertSame('true', getenv('IMMUTABLE')); - - putenv('IMMUTABLE=true'); - $dotenv->overload(); - $this->assertSame('false', getenv('IMMUTABLE')); + self::assertSame('true', \getenv('IMMUTABLE')); } public function testDotenvOverloadDoesOverwriteEnv() { - $dotenv = Dotenv::create($this->fixturesFolder, 'mutable.env'); - $dotenv->overload(); - $this->assertSame('true', getenv('MUTABLE')); + $dotenv = Dotenv::createUnsafeMutable(self::$folder, 'mutable.env'); + $dotenv->load(); + self::assertSame('true', \getenv('MUTABLE')); } public function testDotenvAllowsSpecialCharacters() { - $dotenv = Dotenv::create($this->fixturesFolder, 'specialchars.env'); + $dotenv = Dotenv::createUnsafeMutable(self::$folder, 'specialchars.env'); $dotenv->load(); - $this->assertSame('$a6^C7k%zs+e^.jvjXk', getenv('SPVAR1')); - $this->assertSame('?BUty3koaV3%GA*hMAwH}B', getenv('SPVAR2')); - $this->assertSame('jdgEB4{QgEC]HL))&GcXxokB+wqoN+j>xkV7K?m$r', getenv('SPVAR3')); - $this->assertSame('22222:22#2^{', getenv('SPVAR4')); - $this->assertSame('test some escaped characters like a quote " or maybe a backslash \\', getenv('SPVAR5')); - $this->assertSame('secret!@', getenv('SPVAR6')); - $this->assertSame('secret!@#', getenv('SPVAR7')); - $this->assertSame('secret!@#', getenv('SPVAR8')); + self::assertSame('$a6^C7k%zs+e^.jvjXk', \getenv('SPVAR1')); + self::assertSame('?BUty3koaV3%GA*hMAwH}B', \getenv('SPVAR2')); + self::assertSame('jdgEB4{QgEC]HL))&GcXxokB+wqoN+j>xkV7K?m$r', \getenv('SPVAR3')); + self::assertSame('22222:22#2^{', \getenv('SPVAR4')); + self::assertSame('test some escaped characters like a quote " or maybe a backslash \\', \getenv('SPVAR5')); + self::assertSame('secret!@', \getenv('SPVAR6')); + self::assertSame('secret!@#', \getenv('SPVAR7')); + self::assertSame('secret!@#', \getenv('SPVAR8')); } - public function testMutlilineLoading() + public function testMultilineLoading() { - $dotenv = Dotenv::create($this->fixturesFolder, 'multiline.env'); + $dotenv = Dotenv::createUnsafeMutable(self::$folder, 'multiline.env'); $dotenv->load(); - $this->assertSame("test\n test\"test\"\n test", getenv('TEST')); - $this->assertSame("test\ntest", getenv('TEST_ND')); - $this->assertSame("test\ntest", getenv('TEST_NS')); + self::assertSame("test\n test\"test\"\n test", \getenv('TEST')); + self::assertSame("test\ntest", \getenv('TEST_ND')); + self::assertSame('test\\ntest', \getenv('TEST_NS')); - $this->assertSame('https://vision.googleapis.com/v1/images:annotate?key=', getenv('TEST_EQD')); - $this->assertSame('https://vision.googleapis.com/v1/images:annotate?key=', getenv('TEST_EQS')); + self::assertSame('https://vision.googleapis.com/v1/images:annotate?key=', \getenv('TEST_EQD')); + self::assertSame('https://vision.googleapis.com/v1/images:annotate?key=', \getenv('TEST_EQS')); } public function testEmptyLoading() { - $dotenv = Dotenv::create($this->fixturesFolder, 'empty.env'); - $this->assertSame(['EMPTY_VAR' => null], $dotenv->load()); + $dotenv = Dotenv::createImmutable(self::$folder, 'empty.env'); + self::assertSame(['EMPTY_VAR' => null], $dotenv->load()); } - public function testGetEnvironmentVariablesList() + public function testUnicodeVarNames() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable(self::$folder, 'unicodevarnames.env'); $dotenv->load(); - $this->assertSame(['FOO', 'BAR', 'SPACED', 'NULL'], $dotenv->getEnvironmentVariableNames()); + self::assertSame('Skybert', $_SERVER['AlbertÅberg']); + self::assertSame('2022-04-01T00:00', $_SERVER['ДатаЗакрытияРасчетногоПериода']); + } + + public function testDirectConstructor() + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); + $store = StoreBuilder::createWithDefaultName()->addPath(self::$folder)->make(); + + $dotenv = new Dotenv($store, new Parser(), new Loader(), $repository); + + self::assertSame([ + 'FOO' => 'bar', + 'BAR' => 'baz', + 'SPACED' => 'with spaces', + 'NULL' => '', + ], $dotenv->load()); + } + + public function testDotenvParseExample1() + { + $output = Dotenv::parse( + "BASE_DIR=\"/var/webroot/project-root\"\nCACHE_DIR=\"\${BASE_DIR}/cache\"\nTMP_DIR=\"\${BASE_DIR}/tmp\"\n" + ); + + self::assertSame($output, [ + 'BASE_DIR' => '/var/webroot/project-root', + 'CACHE_DIR' => '/var/webroot/project-root/cache', + 'TMP_DIR' => '/var/webroot/project-root/tmp', + ]); + } + + public function testDotenvParseExample2() + { + $output = Dotenv::parse("FOO=Bar\nBAZ=\"Hello \${FOO}\""); + + self::assertSame($output, ['FOO' => 'Bar', 'BAZ' => 'Hello Bar']); + } + + public function testDotenvParseEmptyCase() + { + $output = Dotenv::parse(''); + + self::assertSame($output, []); } } diff --git a/tests/Dotenv/EnvironmentVariablesTest.php b/tests/Dotenv/EnvironmentVariablesTest.php deleted file mode 100644 index cab198be..00000000 --- a/tests/Dotenv/EnvironmentVariablesTest.php +++ /dev/null @@ -1,153 +0,0 @@ -envFactory = new DotenvFactory(); - (new Loader([dirname(__DIR__).'/fixtures/env/.env'], $this->envFactory))->load(); - } - - public function testCheckingWhetherVariableExists() - { - $envVars = $this->envFactory->create(); - - $this->assertTrue($envVars->has('FOO')); - $this->assertFalse($envVars->has('NON_EXISTING_VARIABLE')); - } - - public function testCheckingHasWithBadType() - { - $envVars = $this->envFactory->create(); - - $this->assertFalse($envVars->has(123)); - $this->assertFalse($envVars->has(null)); - } - - public function testGettingVariableByName() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars->get('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testGettingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->get(null); - } - - public function testSettingVariable() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars->get('FOO')); - - $envVars->set('FOO', 'new'); - - $this->assertSame('new', $envVars->get('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testSettingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->set(null, 'foo'); - } - - public function testClearingVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->clear('FOO'); - - $this->assertFalse($envVars->has('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testClearingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->clear(null); - } - - public function testCannotSetVariableOnImmutableInstance() - { - $envVars = $this->envFactory->createImmutable(); - - $this->assertSame('bar', $envVars->get('FOO')); - - $envVars->set('FOO', 'new'); - - $this->assertSame('bar', $envVars->get('FOO')); - } - - public function testCannotClearVariableOnImmutableInstance() - { - $envVars = $this->envFactory->createImmutable(); - - $envVars->clear('FOO'); - - $this->assertTrue($envVars->has('FOO')); - } - - public function testCheckingWhetherVariableExistsUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertTrue(isset($envVars['FOO'])); - $this->assertFalse(isset($envVars['NON_EXISTING_VARIABLE'])); - } - - public function testGettingVariableByNameUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars['FOO']); - } - - public function testSettingVariableUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars['FOO']); - - $envVars['FOO'] = 'new'; - - $this->assertSame('new', $envVars['FOO']); - } - - public function testClearingVariableUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - unset($envVars['FOO']); - - $this->assertFalse(isset($envVars['FOO'])); - } -} diff --git a/tests/Dotenv/FactoryTest.php b/tests/Dotenv/FactoryTest.php deleted file mode 100644 index 44bac3bb..00000000 --- a/tests/Dotenv/FactoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -getProperty('adapters'); - - $prop->setAccessible(true); - - return $prop->getValue($obj); - } - - public function testDefaults() - { - $f = new DotenvFactory(); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(3, self::getAdapters($f->create())); - $this->assertCount(3, self::getAdapters($f->createImmutable())); - } - - public function testSingle() - { - $f = new DotenvFactory([new EnvConstAdapter()]); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(1, self::getAdapters($f->create())); - $this->assertCount(1, self::getAdapters($f->createImmutable())); - } - - public function testNone() - { - $f = new DotenvFactory([]); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(0, self::getAdapters($f->create())); - $this->assertCount(0, self::getAdapters($f->createImmutable())); - } -} diff --git a/tests/Dotenv/LinesTest.php b/tests/Dotenv/LinesTest.php deleted file mode 100644 index d7de1d5d..00000000 --- a/tests/Dotenv/LinesTest.php +++ /dev/null @@ -1,67 +0,0 @@ -assertSame($expected, Lines::process(preg_split("/(\r\n|\n|\r)/", $content))); - } - - public function testProcessQuotes() - { - $content = file_get_contents(dirname(__DIR__).'/fixtures/env/multiline.env'); - - $expected = [ - "TEST=\"test\n test\\\"test\\\"\n test\"", - 'TEST_ND="test\\ntest"', - 'TEST_NS=\'test\\ntest\'', - 'TEST_EQD="https://vision.googleapis.com/v1/images:annotate?key="', - 'TEST_EQS=\'https://vision.googleapis.com/v1/images:annotate?key=\'', - ]; - - $this->assertSame($expected, Lines::process(preg_split("/(\r\n|\n|\r)/", $content))); - } - - public function testProcessClosingSlash() - { - $lines = [ - 'SPVAR5="test some escaped characters like a quote \" or maybe a backslash \\" # not escaped', - ]; - - $expected = [ - 'SPVAR5="test some escaped characters like a quote \" or maybe a backslash \\" # not escaped', - ]; - - $this->assertSame($expected, $lines); - } - - public function testProcessBadQuotes() - { - $lines = [ - "TEST=\"erert\nTEST='erert\n", - ]; - - $expected = [ - "TEST=\"erert\nTEST='erert\n", - ]; - - $this->assertSame($expected, $lines); - } -} diff --git a/tests/Dotenv/Loader/LoaderTest.php b/tests/Dotenv/Loader/LoaderTest.php new file mode 100644 index 00000000..76ba8e9e --- /dev/null +++ b/tests/Dotenv/Loader/LoaderTest.php @@ -0,0 +1,86 @@ +addWriter(ArrayAdapter::class)->make(); + $loader = new Loader(); + + $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; + $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => '${NVAR1} ${NVAR2}']; + + self::assertSame($expected, $loader->load($repository, (new Parser())->parse($content))); + } + + public function testLoaderWithAllowList() + { + $adapter = ArrayAdapter::create()->get(); + $repository = RepositoryBuilder::createWithNoAdapters()->addReader($adapter)->addWriter($adapter)->allowList(['FOO'])->make(); + $loader = new Loader(); + + self::assertSame(['FOO' => 'Hello'], $loader->load($repository, (new Parser())->parse("FOO=\"Hello\"\nBAR=\"World!\"\n"))); + self::assertTrue($adapter->read('FOO')->isDefined()); + self::assertSame('Hello', $adapter->read('FOO')->get()); + self::assertFalse($adapter->read('BAR')->isDefined()); + } + + public function testLoaderWithGarbage() + { + $adapter = ArrayAdapter::create()->get(); + $repository = RepositoryBuilder::createWithNoAdapters()->make(); + $loader = new Loader(); + + $this->expectException(InvalidFileException::class); + $this->expectExceptionMessage('Failed to parse dotenv file. Encountered unexpected whitespace at ["""].'); + + $loader->load($repository, (new Parser())->parse('FOO="""')); + } + + /** + * @return array[] + */ + public static function providesAdapters() + { + return [ + [ArrayAdapter::create()->get()], + [EnvConstAdapter::class], + [ServerConstAdapter::class], + ]; + } + + /** + * @dataProvider providesAdapters + * + * @param \Dotenv\Repository\Adapter\AdapterInterface|string $adapter + */ + public function testLoaderWithSpecificAdapter($adapter) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addReader($adapter)->addWriter($adapter)->make(); + $loader = new Loader(); + + $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; + $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => 'Hello World!']; + + self::assertSame($expected, $loader->load($repository, (new Parser())->parse($content))); + } +} diff --git a/tests/Dotenv/LoaderTest.php b/tests/Dotenv/LoaderTest.php deleted file mode 100644 index e5491203..00000000 --- a/tests/Dotenv/LoaderTest.php +++ /dev/null @@ -1,151 +0,0 @@ -folder = dirname(__DIR__).'/fixtures/env'; - $this->keyVal(true); - } - - /** - * Generates a new key/value pair or returns the previous one. - * - * Since most of our functionality revolves around setting/retrieving keys - * and values, we have this utility function to help generate new, unique - * key/value pairs. - * - * @param bool $reset - * - * @return array - */ - protected function keyVal($reset = false) - { - if (!isset($this->keyVal) || $reset) { - $this->keyVal = [uniqid() => uniqid()]; - } - - return $this->keyVal; - } - - /** - * Returns the key from keyVal(), without reset. - * - * @return string - */ - protected function key() - { - $keyVal = $this->keyVal(); - - return key($keyVal); - } - - /** - * Returns the value from keyVal(), without reset. - * - * @return string - */ - protected function value() - { - $keyVal = $this->keyVal(); - - return reset($keyVal); - } - - public function testMutableLoaderClearsEnvironmentVars() - { - $loader = new Loader(["{$this->folder}/.env"], new DotenvFactory(), false); - - // Set an environment variable. - $loader->setEnvironmentVariable($this->key(), $this->value()); - - // Clear the set environment variable. - $loader->clearEnvironmentVariable($this->key()); - $this->assertSame(null, $loader->getEnvironmentVariable($this->key())); - $this->assertSame(false, getenv($this->key())); - $this->assertSame(false, isset($_ENV[$this->key()])); - $this->assertSame(false, isset($_SERVER[$this->key()])); - $this->assertSame([$this->key()], $loader->getEnvironmentVariableNames()); - } - - public function testImmutableLoaderCannotClearEnvironmentVars() - { - $loader = new Loader(["{$this->folder}/.env"], new DotenvFactory(), false); - - $loader->setImmutable(true); - - // Set an environment variable. - $loader->setEnvironmentVariable($this->key(), $this->value()); - - // Attempt to clear the environment variable, check that it fails. - $loader->clearEnvironmentVariable($this->key()); - $this->assertSame($this->value(), $loader->getEnvironmentVariable($this->key())); - $this->assertSame($this->value(), getenv($this->key())); - $this->assertSame(true, isset($_ENV[$this->key()])); - $this->assertSame(true, isset($_SERVER[$this->key()])); - $this->assertSame([$this->key()], $loader->getEnvironmentVariableNames()); - } - - /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage At least one environment file path must be provided. - */ - public function testLoaderWithNoPaths() - { - (new Loader([], new DotenvFactory(), false))->load(); - } - - /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage Unable to read any of the environment file(s) at - */ - public function testLoaderWithBadPaths() - { - (new Loader(["{$this->folder}/BAD1", "{$this->folder}/BAD2"], new DotenvFactory(), false))->load(); - } - - public function testLoaderWithOneGoodPath() - { - $loader = (new Loader(["{$this->folder}/BAD1", "{$this->folder}/.env"], new DotenvFactory(), false)); - - $this->assertCount(4, $loader->load()); - } - - public function testLoaderWithNoAdapters() - { - $loader = (new Loader([], new DotenvFactory([]))); - - $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; - $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => '${NVAR1} ${NVAR2}']; - - $this->assertSame($expected, $loader->loadDirect($content)); - } - - public function testLoaderWithArrayAdapter() - { - $loader = (new Loader([], new DotenvFactory([new ArrayAdapter()]))); - - $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; - $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => 'Hello World!']; - - $this->assertSame($expected, $loader->loadDirect($content)); - } -} diff --git a/tests/Dotenv/Parser/EntryParserTest.php b/tests/Dotenv/Parser/EntryParserTest.php new file mode 100644 index 00000000..8a5f5f6d --- /dev/null +++ b/tests/Dotenv/Parser/EntryParserTest.php @@ -0,0 +1,234 @@ +checkPositiveResult($result, 'FOO', 'BAR'); + } + + public function testNullParse() + { + $result = EntryParser::parse('FOO'); + $this->checkEmptyResult($result, 'FOO'); + } + + public function testUnicodeNameParse() + { + $result = EntryParser::parse('FOOƱ=BAZ'); + $this->checkPositiveResult($result, 'FOOƱ', 'BAZ'); + } + + public function testQuotesParse() + { + $result = EntryParser::parse("FOO=\"BAR \n\""); + $this->checkPositiveResult($result, 'FOO', "BAR \n"); + } + + public function testNewlineParse() + { + $result = EntryParser::parse('FOO="\n"'); + $this->checkPositiveResult($result, 'FOO', "\n"); + } + + public function testTabParseDouble() + { + $result = EntryParser::parse('FOO="\t"'); + $this->checkPositiveResult($result, 'FOO', "\t"); + } + + public function testTabParseSingle() + { + $result = EntryParser::parse('FOO=\'\t\''); + $this->checkPositiveResult($result, 'FOO', '\t'); + } + + public function testNonEscapeParse1() + { + $result = EntryParser::parse('FOO=\n\v'); + $this->checkPositiveResult($result, 'FOO', '\n\v'); + } + + public function testNonEscapeParse2() + { + $result = EntryParser::parse('FOO=\q'); + $this->checkPositiveResult($result, 'FOO', '\q'); + } + + public function testBadEscapeParse() + { + $result = EntryParser::parse('FOO="\q"'); + $this->checkErrorResult($result, 'Encountered an unexpected escape sequence at ["\q"].'); + } + + public function testInlineVariable() + { + $result = EntryParser::parse('FOO=$BAR'); + $this->checkPositiveResult($result, 'FOO', '$BAR', [0]); + } + + public function testInlineVariableOffset() + { + $result = EntryParser::parse('FOO=AAA$BAR'); + $this->checkPositiveResult($result, 'FOO', 'AAA$BAR', [3]); + } + + public function testInlineVariables() + { + $result = EntryParser::parse('FOO="TEST $BAR $$BAZ"'); + $this->checkPositiveResult($result, 'FOO', 'TEST $BAR $$BAZ', [11, 10, 5]); + } + + public function testNonInlineVariable() + { + $result = EntryParser::parse('FOO=\'TEST $BAR $$BAZ\''); + $this->checkPositiveResult($result, 'FOO', 'TEST $BAR $$BAZ'); + self::assertTrue($result->success()->isDefined()); + } + + public function testWhitespaceParse() + { + $result = EntryParser::parse("FOO=\"\n\""); + $this->checkPositiveResult($result, 'FOO', "\n"); + } + + public function testExportParse() + { + $result = EntryParser::parse('export FOO="bar baz"'); + $this->checkPositiveResult($result, 'FOO', 'bar baz'); + } + + public function testExportParseTab() + { + $result = EntryParser::parse("export\t\"FOO\"='bar baz'"); + $this->checkPositiveResult($result, 'FOO', 'bar baz'); + } + + public function testExportParseFail() + { + $result = EntryParser::parse('export "FOO="bar baz"'); + $this->checkErrorResult($result, 'Encountered an invalid name at ["FOO].'); + } + + public function testClosingSlashParse() + { + $result = EntryParser::parse('SPVAR5="test some escaped characters like a quote \\" or maybe a backslash \\\\" # not escaped'); + $this->checkPositiveResult($result, 'SPVAR5', 'test some escaped characters like a quote " or maybe a backslash \\'); + } + + public function testParseInvalidSpaces() + { + $result = EntryParser::parse('FOO=bar baz'); + $this->checkErrorResult($result, 'Encountered unexpected whitespace at [bar baz].'); + } + + public function testParseStrayEquals() + { + $result = EntryParser::parse('='); + $this->checkErrorResult($result, 'Encountered an unexpected equals at [=].'); + } + + public function testParseInvalidName() + { + $result = EntryParser::parse('FOO_ASD!=BAZ'); + $this->checkErrorResult($result, 'Encountered an invalid name at [FOO_ASD!].'); + } + + public function testParserEscapingDouble() + { + $result = EntryParser::parse('FOO_BAD="iiiiviiiixiiiiviiii\\a"'); + $this->checkErrorResult($result, 'Encountered an unexpected escape sequence at ["iiiiviiiixiiiiviiii\a"].'); + } + + public function testParserEscapingSingle() + { + $result = EntryParser::parse('FOO_BAD=\'iiiiviiiixiiiiviiii\\a\''); + $this->checkPositiveResult($result, 'FOO_BAD', 'iiiiviiiixiiiiviiii\\a'); + } + + public function testParserMissingClosingSingleQuote() + { + $result = EntryParser::parse('TEST=\'erert'); + $this->checkErrorResult($result, 'Encountered a missing closing quote at [\'erert].'); + } + + public function testParserMissingClosingDoubleQuote() + { + $result = EntryParser::parse('TEST="erert'); + $this->checkErrorResult($result, 'Encountered a missing closing quote at ["erert].'); + } + + public function testParserMissingClosingQuotes() + { + $result = EntryParser::parse("TEST=\"erert\nTEST='erert\n"); + $this->checkErrorResult($result, 'Encountered a missing closing quote at ["erert].'); + } + + public function testParserClosingQuoteWithEscape() + { + $result = EntryParser::parse('TEST="\\'); + $this->checkErrorResult($result, 'Encountered a missing closing quote at ["\\].'); + } + + /** + * @param \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string> $result + * @param string $name + * @param string $chars + * @param int[] $vars + * + * @return void + */ + private function checkPositiveResult(Result $result, string $name, string $chars, array $vars = []) + { + self::assertTrue($result->success()->isDefined()); + + $entry = $result->success()->get(); + self::assertInstanceOf(Entry::class, $entry); + self::assertSame($name, $entry->getName()); + self::assertTrue($entry->getValue()->isDefined()); + + $value = $entry->getValue()->get(); + self::assertInstanceOf(Value::class, $value); + self::assertSame($chars, $value->getChars()); + self::assertSame($vars, $value->getVars()); + } + + /** + * @param \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string> $result + * @param string $name + * + * @return void + */ + private function checkEmptyResult(Result $result, string $name) + { + self::assertTrue($result->success()->isDefined()); + + $entry = $result->success()->get(); + self::assertInstanceOf(Entry::class, $entry); + self::assertSame('FOO', $entry->getName()); + self::assertFalse($entry->getValue()->isDefined()); + } + + /** + * @param \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string> $result + * @param string $error + * + * @return void + */ + private function checkErrorResult(Result $result, string $error) + { + self::assertTrue($result->error()->isDefined()); + self::assertSame($error, $result->error()->get()); + } +} diff --git a/tests/Dotenv/Parser/LexerTest.php b/tests/Dotenv/Parser/LexerTest.php new file mode 100644 index 00000000..fabc36e0 --- /dev/null +++ b/tests/Dotenv/Parser/LexerTest.php @@ -0,0 +1,40 @@ +success()->isDefined()); + + $expected = [ + 'ASSERTVAR1=val1', + 'ASSERTVAR2=""', + 'ASSERTVAR3="val3 "', + 'ASSERTVAR4="0" # empty looking value', + 'ASSERTVAR5="#foo"', + "ASSERTVAR6=\"val1\nval2\"", + "ASSERTVAR7=\"\nval3\" #", + "ASSERTVAR8=\"val3\n\"", + "ASSERTVAR9=\"\n\n\"", + ]; + + self::assertSame($expected, Lines::process($result->success()->get())); + } + + public function testProcessQuotes() + { + $content = \file_get_contents(\dirname(\dirname(__DIR__)).'/fixtures/env/multiline.env'); + self::assertIsString($content); + $result = Regex::split("/(\r\n|\n|\r)/", $content); + self::assertTrue($result->success()->isDefined()); + + $expected = [ + "TEST=\"test\n test\\\"test\\\"\n test\"", + 'TEST_ND="test\\ntest"', + 'TEST_NS=\'test\\ntest\'', + 'TEST_EQD="https://vision.googleapis.com/v1/images:annotate?key="', + 'TEST_EQS=\'https://vision.googleapis.com/v1/images:annotate?key=\'', + "BASE64_ENCODED_MULTILINE=\"qS1zCzMVVUJWQShokv6YVYi+ruKSC/bHV7GmEiyVkLaBWJHNVHCHsgTksEBsy8wJ\nuwycAvR07ZyOJJed4XTRMKnKp1/v+6UATpWzkIjZXytK+pD+XlZimUHTx3uiDcmU\njhQX1wWSxHDqrSWxeIJiTD+BuUyId8FzmXQ3TcBydJ474tmOU2F492ubk3LAiZ18\nmhiRGoshXAOSbS/P3+RZi4bDeNE/No4=\"", + ]; + + self::assertSame($expected, Lines::process($result->success()->get())); + } +} diff --git a/tests/Dotenv/Parser/ParserTest.php b/tests/Dotenv/Parser/ParserTest.php new file mode 100644 index 00000000..ad46b00d --- /dev/null +++ b/tests/Dotenv/Parser/ParserTest.php @@ -0,0 +1,98 @@ +parse("FOO=BAR\nFOO\nFOO=\"BAR \n\"\nFOO=\"\\n\""); + + self::assertIsArray($result); + self::assertCount(4, $result); + + $this->checkPositiveEntry($result[0], 'FOO', 'BAR'); + $this->checkEmptyEntry($result[1], 'FOO'); + $this->checkPositiveEntry($result[2], 'FOO', "BAR \n"); + $this->checkPositiveEntry($result[3], 'FOO', "\n"); + } + + public function testBadEscapeParse() + { + $this->expectException(InvalidFileException::class); + $this->expectExceptionMessage('Failed to parse dotenv file. Encountered an unexpected escape sequence at ["\q"].'); + + (new Parser())->parse('FOO="\q"'); + } + + public function testParseInvalidSpaces() + { + $this->expectException(InvalidFileException::class); + $this->expectExceptionMessage('Failed to parse dotenv file. Encountered unexpected whitespace at [bar baz].'); + + (new Parser())->parse("FOO=bar baz\n"); + } + + public function testParseStrayEquals() + { + $this->expectException(InvalidFileException::class); + $this->expectExceptionMessage('Failed to parse dotenv file. Encountered an unexpected equals at [=].'); + + (new Parser())->parse("=\n"); + } + + public function testParseInvalidName() + { + $this->expectException(InvalidFileException::class); + $this->expectExceptionMessage('Failed to parse dotenv file. Encountered an invalid name at [FOO_ASD!].'); + + (new Parser())->parse('FOO_ASD!=BAZ'); + } + + /** + * @param \Dotenv\Parser\Entry $entry + * @param string $name + * @param string $chars + * @param int[] $vars + * + * @return void + */ + private function checkPositiveEntry(Entry $entry, string $name, string $chars, array $vars = []) + { + self::assertInstanceOf(Entry::class, $entry); + self::assertSame($name, $entry->getName()); + self::assertTrue($entry->getValue()->isDefined()); + + $value = $entry->getValue()->get(); + self::assertInstanceOf(Value::class, $value); + self::assertSame($chars, $value->getChars()); + self::assertSame($vars, $value->getVars()); + } + + /** + * @param \Dotenv\Parser\Entry $entry + * @param string $name + * + * @return void + */ + private function checkEmptyEntry(Entry $entry, string $name) + { + self::assertInstanceOf(Entry::class, $entry); + self::assertSame('FOO', $entry->getName()); + self::assertFalse($entry->getValue()->isDefined()); + } +} diff --git a/tests/Dotenv/ParserTest.php b/tests/Dotenv/ParserTest.php deleted file mode 100644 index a066f977..00000000 --- a/tests/Dotenv/ParserTest.php +++ /dev/null @@ -1,145 +0,0 @@ -assertSame(['FOO', 'BAR'], Parser::parse('FOO=BAR')); - } - - public function testQuotesParse() - { - $this->assertSame(['FOO', "BAR \n"], Parser::parse("FOO=\"BAR \n\"")); - } - - public function testNewlineParse() - { - $this->assertSame(['FOO', "\n"], Parser::parse('FOO="\n"')); - } - - public function testTabParse() - { - $this->assertSame(['FOO', "\t"], Parser::parse('FOO=\'\t\'')); - } - - public function testNonEscapeParse1() - { - $this->assertSame(['FOO', '\n\v'], Parser::parse('FOO=\n\v')); - } - - public function testNonEscapeParse2() - { - $this->assertSame(['FOO', '\q'], Parser::parse('FOO=\q')); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected escape sequence. Failed at ["\q"]. - */ - public function testBadEscapeParse() - { - Parser::parse('FOO="\q"'); - } - - public function testWhitespaceParse() - { - $this->assertSame(['FOO', "\n"], Parser::parse("FOO=\"\n\"")); - } - - public function testExportParse() - { - $this->assertSame(['FOO', 'bar baz'], Parser::parse('export FOO="bar baz"')); - } - - public function testClosingSlashParse() - { - $content = 'SPVAR5="test some escaped characters like a quote \\" or maybe a backslash \\\\" # not escaped'; - $expected = ['SPVAR5', 'test some escaped characters like a quote " or maybe a backslash \\']; - - $this->assertSame($expected, Parser::parse($content)); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to unexpected whitespace. Failed at [bar baz]. - */ - public function testParseInvalidSpaces() - { - Parser::parse('FOO=bar baz'); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected equals. Failed at [=]. - */ - public function testParseStrayEquals() - { - Parser::parse('='); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to an invalid name. Failed at [FOO_ASD!]. - */ - public function testParseInvalidName() - { - Parser::parse('FOO_ASD!=BAZ'); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected escape sequence. Failed at ["iiiiviiiixiiiiviiii\a"]. - */ - public function testParserEscapingDouble() - { - Parser::parse('FOO_BAD="iiiiviiiixiiiiviiii\\a"'); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected escape sequence. Failed at ['iiiiviiiixiiiiviiii\a']. - */ - public function testParserEscapingSingle() - { - Parser::parse('FOO_BAD=\'iiiiviiiixiiiiviiii\\a\''); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to a missing closing quote. Failed at ['erert]. - */ - public function testMissingClosingSingleQuote() - { - Parser::parse('TEST=\'erert'); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to a missing closing quote. Failed at ["erert]. - */ - public function testMissingClosingDoubleQuote() - { - Parser::parse('TEST="erert'); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to a missing closing quote. Failed at ["erert]. - */ - public function testMissingClosingQuotes() - { - Parser::parse("TEST=\"erert\nTEST='erert\n"); - } - - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Failed to parse dotenv file due to a missing closing quote. Failed at ["\]. - */ - public function testMissingClosingQuoteWithEscape() - { - Parser::parse('TEST="\\'); - } -} diff --git a/tests/Dotenv/Repository/Adapter/ArrayAdapterTest.php b/tests/Dotenv/Repository/Adapter/ArrayAdapterTest.php new file mode 100644 index 00000000..f79406a3 --- /dev/null +++ b/tests/Dotenv/Repository/Adapter/ArrayAdapterTest.php @@ -0,0 +1,57 @@ +write('CONST_TEST', 'foo bar baz'); + $value = $adapter->read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('foo bar baz', $value->get()); + } + + public function testUndefinedRead() + { + $adapter = self::createAdapter(); + unset($_ENV['CONST_TEST']); + $value = $adapter->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testGoodWrite() + { + $adapter = self::createAdapter(); + self::assertTrue($adapter->write('CONST_TEST', 'foo')); + self::assertSame('foo', $adapter->read('CONST_TEST')->get()); + } + + public function testEmptyWrite() + { + $adapter = self::createAdapter(); + self::assertTrue($adapter->write('CONST_TEST', '')); + self::assertSame('', $adapter->read('CONST_TEST')->get()); + } + + public function testGoodDelete() + { + $adapter = self::createAdapter(); + self::assertTrue($adapter->delete('CONST_TEST')); + self::assertFalse($adapter->read('CONST_TEST')->isDefined()); + } + + /** + * @return \Dotenv\Repository\Adapter\AdapterInterface + */ + private static function createAdapter() + { + return ArrayAdapter::create()->get(); + } +} diff --git a/tests/Dotenv/Repository/Adapter/EnvConstAdapterTest.php b/tests/Dotenv/Repository/Adapter/EnvConstAdapterTest.php new file mode 100644 index 00000000..8e10b711 --- /dev/null +++ b/tests/Dotenv/Repository/Adapter/EnvConstAdapterTest.php @@ -0,0 +1,75 @@ +read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('foo bar baz', $value->get()); + } + + public function testFalseRead() + { + $_ENV['CONST_TEST'] = false; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('false', $value->get()); + } + + public function testTrueRead() + { + $_ENV['CONST_TEST'] = true; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('true', $value->get()); + } + + public function testBadTypeRead() + { + $_ENV['CONST_TEST'] = [123]; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testUndefinedRead() + { + unset($_ENV['CONST_TEST']); + $value = self::createAdapter()->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testGoodWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', 'foo')); + self::assertSame('foo', $_ENV['CONST_TEST']); + } + + public function testEmptyWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', '')); + self::assertSame('', $_ENV['CONST_TEST']); + } + + public function testGoodDelete() + { + self::assertTrue(self::createAdapter()->delete('CONST_TEST')); + self::assertFalse(isset($_ENV['CONST_TEST'])); + } + + /** + * @return \Dotenv\Repository\Adapter\AdapterInterface + */ + private static function createAdapter() + { + return EnvConstAdapter::create()->get(); + } +} diff --git a/tests/Dotenv/Repository/Adapter/PutenvAdapterTest.php b/tests/Dotenv/Repository/Adapter/PutenvAdapterTest.php new file mode 100644 index 00000000..4861ec49 --- /dev/null +++ b/tests/Dotenv/Repository/Adapter/PutenvAdapterTest.php @@ -0,0 +1,52 @@ +read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('foo bar baz', $value->get()); + } + + public function testUndefinedRead() + { + \putenv('CONST_TEST'); + $value = self::createAdapter()->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testGoodWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', 'foo')); + self::assertSame('foo', \getenv('CONST_TEST')); + } + + public function testEmptyWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', '')); + self::assertSame('', \getenv('CONST_TEST')); + } + + public function testGoodDelete() + { + self::assertTrue(self::createAdapter()->delete('CONST_TEST')); + self::assertFalse(\getenv('CONST_TEST')); + } + + /** + * @return \Dotenv\Repository\Adapter\AdapterInterface + */ + private static function createAdapter() + { + return PutenvAdapter::create()->get(); + } +} diff --git a/tests/Dotenv/Repository/Adapter/ServerConstAdapterTest.php b/tests/Dotenv/Repository/Adapter/ServerConstAdapterTest.php new file mode 100644 index 00000000..d322f779 --- /dev/null +++ b/tests/Dotenv/Repository/Adapter/ServerConstAdapterTest.php @@ -0,0 +1,75 @@ +read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('foo bar baz', $value->get()); + } + + public function testFalseRead() + { + $_SERVER['CONST_TEST'] = false; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('false', $value->get()); + } + + public function testTrueRead() + { + $_SERVER['CONST_TEST'] = true; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertTrue($value->isDefined()); + self::assertSame('true', $value->get()); + } + + public function testBadTypeRead() + { + $_SERVER['CONST_TEST'] = [123]; + $value = self::createAdapter()->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testUndefinedRead() + { + unset($_SERVER['CONST_TEST']); + $value = self::createAdapter()->read('CONST_TEST'); + self::assertFalse($value->isDefined()); + } + + public function testGoodWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', 'foo')); + self::assertSame('foo', $_SERVER['CONST_TEST']); + } + + public function testEmptyWrite() + { + self::assertTrue(self::createAdapter()->write('CONST_TEST', '')); + self::assertSame('', $_SERVER['CONST_TEST']); + } + + public function testGoodDelete() + { + self::assertTrue(self::createAdapter()->delete('CONST_TEST')); + self::assertFalse(isset($_SERVER['CONST_TEST'])); + } + + /** + * @return \Dotenv\Repository\Adapter\AdapterInterface + */ + private static function createAdapter() + { + return ServerConstAdapter::create()->get(); + } +} diff --git a/tests/Dotenv/Repository/RepositoryTest.php b/tests/Dotenv/Repository/RepositoryTest.php new file mode 100644 index 00000000..8732dc70 --- /dev/null +++ b/tests/Dotenv/Repository/RepositoryTest.php @@ -0,0 +1,305 @@ +|null + */ + private $keyVal; + + /** + * @before + * + * @return void + */ + public function refreshKeyVal() + { + $this->keyVal(true); + } + + /** + * @return void + */ + private function load() + { + Dotenv::createMutable(\dirname(\dirname(__DIR__)).'/fixtures/env')->load(); + } + + /** + * Generates a new key/value pair or returns the previous one. + * + * Since most of our functionality revolves around setting/retrieving keys + * and values, we have this utility function to help generate new, unique + * key/value pairs. + * + * @param bool $reset + * + * @return array + */ + private function keyVal(bool $reset = false) + { + if (!isset($this->keyVal) || $reset) { + $this->keyVal = [\uniqid() => \uniqid()]; + } + + return $this->keyVal; + } + + /** + * Returns the key from keyVal(), without reset. + * + * @return string + */ + private function key() + { + $keyVal = $this->keyVal(); + + return (string) \key($keyVal); + } + + /** + * Returns the value from keyVal(), without reset. + * + * @return string + */ + private function value() + { + $keyVal = $this->keyVal(); + + /** @var string */ + return \reset($keyVal); + } + + public function testRepositoryInstanceOf() + { + self::assertInstanceOf(RepositoryInterface::class, RepositoryBuilder::createWithNoAdapters()->make()); + self::assertInstanceOf(RepositoryInterface::class, RepositoryBuilder::createWithDefaultAdapters()->make()); + } + + public function testMutableLoaderClearsEnvironmentVars() + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); + + // Set an environment variable. + $repository->set($this->key(), $this->value()); + + // Clear the set environment variable. + $repository->clear($this->key()); + self::assertNull($repository->get($this->key())); + self::assertFalse(\getenv($this->key())); + self::assertFalse(isset($_ENV[$this->key()])); + self::assertFalse(isset($_SERVER[$this->key()])); + } + + public function testImmutableLoaderCannotClearExistingEnvironmentVars() + { + $this->load(); + + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + // Pre-set an environment variable. + RepositoryBuilder::createWithDefaultAdapters()->make()->set($this->key(), $this->value()); + + // Attempt to clear the environment variable, check that it fails. + $repository->clear($this->key()); + self::assertSame($this->value(), $repository->get($this->key())); + self::assertTrue(isset($_ENV[$this->key()])); + self::assertTrue(isset($_SERVER[$this->key()])); + } + + public function testImmutableLoaderCanClearSetEnvironmentVars() + { + $this->load(); + + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + // Set an environment variable. + $repository->set($this->key(), $this->value()); + + // Attempt to clear the environment variable, check that it works. + $repository->clear($this->key()); + self::assertNull($repository->get($this->key())); + self::assertFalse(\getenv($this->key())); + self::assertFalse(isset($_ENV[$this->key()])); + self::assertFalse(isset($_SERVER[$this->key()])); + } + + public function testCheckingWhetherVariableExists() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + self::assertTrue($repo->has('FOO')); + self::assertFalse($repo->has('NON_EXISTING_VARIABLE')); + } + + public function testHasWithBadVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(TypeError::class); + + $repo->has(null); + } + + public function testGettingVariableByName() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + self::assertSame('bar', $repo->get('FOO')); + } + + public function testGettingNullVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(TypeError::class); + + $repo->get(null); + } + + public function testGettingEmptyVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected name to be a non-empty string.'); + + $repo->get(''); + } + + public function testSettingVariable() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + self::assertSame('bar', $repo->get('FOO')); + $repo->set('FOO', 'new'); + self::assertSame('new', $repo->get('FOO')); + } + + public function testSettingNullVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(TypeError::class); + + $repo->set(null, 'foo'); + } + + public function testSettingEmptyVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected name to be a non-empty string.'); + + $repo->set('', 'foo'); + } + + public function testClearingVariable() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + self::assertTrue($repo->has('FOO')); + $repo->clear('FOO'); + self::assertFalse($repo->has('FOO')); + } + + public function testClearingVariableWithArrayAdapter() + { + $adapter = ArrayAdapter::create()->get(); + $repo = RepositoryBuilder::createWithNoAdapters()->addReader($adapter)->addWriter($adapter)->make(); + + self::assertFalse($repo->has('FOO')); + $repo->set('FOO', 'BAR'); + self::assertTrue($repo->has('FOO')); + $repo->clear('FOO'); + self::assertFalse($repo->has('FOO')); + } + + public function testClearingNullVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(TypeError::class); + + $repo->clear(null); + } + + public function testClearingEmptyVariable() + { + $repo = RepositoryBuilder::createWithDefaultAdapters()->make(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected name to be a non-empty string.'); + + $repo->clear(''); + } + + public function testCannotSetVariableOnImmutableInstance() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + self::assertSame('bar', $repo->get('FOO')); + + $repo->set('FOO', 'new'); + + self::assertSame('bar', $repo->get('FOO')); + } + + public function testCannotClearVariableOnImmutableInstance() + { + $this->load(); + + $repo = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + $repo->clear('FOO'); + + self::assertTrue($repo->has('FOO')); + } + + public function testBuildWithBadReader() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected either an instance of '); + + RepositoryBuilder::createWithNoAdapters()->addReader('123'); + } + + public function testBuildWithBadWriter() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected either an instance of '); + + RepositoryBuilder::createWithNoAdapters()->addWriter('123'); + } + + public function testBuildWithBadAdapter() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected either an instance of '); + + RepositoryBuilder::createWithNoAdapters()->addAdapter(''); + } +} diff --git a/tests/Dotenv/ResultTest.php b/tests/Dotenv/ResultTest.php deleted file mode 100644 index 89f526d2..00000000 --- a/tests/Dotenv/ResultTest.php +++ /dev/null @@ -1,58 +0,0 @@ -assertTrue(Success::create('foo')->error()->isEmpty()); - $this->assertTrue(Success::create('foo')->success()->isDefined()); - $this->assertEquals('foo', Success::create('foo')->getSuccess()); - } - - public function testSuccessMapping() - { - $r = Success::create('foo') - ->mapSuccess('strtoupper') - ->mapError('ucfirst'); - - $this->assertEquals('FOO', $r->getSuccess()); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage None has no value. - */ - public function testSuccessFail() - { - Success::create('foo')->getError(); - } - - public function testErrorValue() - { - $this->assertTrue(Error::create('foo')->error()->isDefined()); - $this->assertTrue(Error::create('foo')->success()->isEmpty()); - $this->assertEquals('foo', Error::create('foo')->getError()); - } - - public function testErrorMapping() - { - $r = Error::create('foo') - ->mapSuccess('strtoupper') - ->mapError('ucfirst'); - - $this->assertEquals('Foo', $r->getError()); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage None has no value. - */ - public function testErrorFail() - { - Error::create('foo')->getSuccess(); - } -} diff --git a/tests/Dotenv/Store/StoreTest.php b/tests/Dotenv/Store/StoreTest.php new file mode 100644 index 00000000..6b4d9672 --- /dev/null +++ b/tests/Dotenv/Store/StoreTest.php @@ -0,0 +1,141 @@ + "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n", + ], + Reader::read( + Paths::filePaths([self::$folder], ['.env']) + ) + ); + } + + public function testBasicRead() + { + $builder = StoreBuilder::createWithDefaultName() + ->addPath(self::$folder); + + self::assertSame( + "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n", + $builder->make()->read() + ); + } + + public function testBasicReadWindowsEncoding() + { + $builder = StoreBuilder::createWithNoNames() + ->addPath(self::$folder) + ->addName('windows.env') + ->fileEncoding('Windows-1252'); + + self::assertSame( + "MBW=\"ñá\"\n", + $builder->make()->read() + ); + } + + public function testBasicReadBadEncoding() + { + $builder = StoreBuilder::createWithNoNames() + ->addPath(self::$folder) + ->addName('windows.env') + ->fileEncoding('Windowss-1252'); + + $this->expectException(InvalidEncodingException::class); + $this->expectExceptionMessage('Illegal character encoding [Windowss-1252] specified.'); + + $builder->make()->read(); + } + + public function testFileReadMultipleShortCircuitModeDirect() + { + self::assertSame( + [ + self::$folder.\DIRECTORY_SEPARATOR.'.env' => "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n", + ], + Reader::read( + Paths::filePaths([self::$folder], ['.env', 'example.env']) + ) + ); + } + + public function testFileReadMultipleShortCircuitMode() + { + $builder = StoreBuilder::createWithNoNames() + ->addPath(self::$folder) + ->addName('.env') + ->addName('example.env') + ->shortCircuit(); + + self::assertSame( + "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n", + $builder->make()->read() + ); + } + + public function testFileReadMultipleWithoutShortCircuitModeDirect() + { + self::assertSame( + [ + self::$folder.\DIRECTORY_SEPARATOR.'.env' => "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n", + self::$folder.\DIRECTORY_SEPARATOR.'example.env' => "EG=\"example\"\n", + ], + Reader::read( + Paths::filePaths([self::$folder], ['.env', 'example.env']), + false + ) + ); + } + + public function testFileReadMultipleWithoutShortCircuitMode() + { + $builder = StoreBuilder::createWithDefaultName() + ->addPath(self::$folder) + ->addName('example.env'); + + self::assertSame( + "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n\nNULL=\n\nEG=\"example\"\n", + $builder->make()->read() + ); + } + public function testFileReadWithUtf8WithBomEncoding() + { + self::assertSame( + [ + self::$folder.\DIRECTORY_SEPARATOR.'utf8-with-bom-encoding.env' => "FOO=bar\nBAR=baz\nSPACED=\"with spaces\"\n", + ], + Reader::read( + Paths::filePaths([self::$folder], ['utf8-with-bom-encoding.env']) + ) + ); + } +} diff --git a/tests/Dotenv/ValidatorTest.php b/tests/Dotenv/ValidatorTest.php index 7299b9a6..dd5bcf9b 100644 --- a/tests/Dotenv/ValidatorTest.php +++ b/tests/Dotenv/ValidatorTest.php @@ -1,110 +1,145 @@ fixturesFolder = dirname(__DIR__).'/fixtures/env'; + self::$folder = \dirname(__DIR__).'/fixtures/env'; } + /** + * @param string $name + * + * @return array{\Dotenv\Repository\RepositoryInterface,\Dotenv\Dotenv} + */ + public static function createArrayDotenv(string $name = '.env') + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + return [$repository, Dotenv::create($repository, self::$folder, $name)]; + } + + /** + * @doesNotPerformAssertions + */ public function testDotenvRequiredStringEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); $dotenv->required('FOO'); - $this->assertTrue(true); } + /** + * @doesNotPerformAssertions + */ public function testDotenvAllowedValues() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); $dotenv->required('FOO')->allowedValues(['bar', 'baz']); - $this->assertTrue(true); } + /** + * @doesNotPerformAssertions + */ public function testDotenvAllowedValuesIfPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); $dotenv->ifPresent('FOO')->allowedValues(['bar', 'baz']); - $this->assertTrue(true); } + /** + * @doesNotPerformAssertions + */ public function testDotenvAllowedValuesIfNotPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); $dotenv->ifPresent('FOOQWERTYOOOOOO')->allowedValues(['bar', 'baz']); - $this->assertTrue(true); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: FOO is not one of [buzz, buz]. - */ public function testDotenvProhibitedValues() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: FOO is not one of [buzz, buz].'); + $dotenv->required('FOO')->allowedValues(['buzz', 'buz']); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: FOO is not one of [buzz, buz]. - */ public function testDotenvProhibitedValuesIfPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: FOO is not one of [buzz, buz].'); + $dotenv->ifPresent('FOO')->allowedValues(['buzz', 'buz']); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: FOOX is missing, NOPE is missing. - */ public function testDotenvRequiredThrowsRuntimeException() { - $dotenv = Dotenv::create($this->fixturesFolder); + [$repo, $dotenv] = self::createArrayDotenv(); + $dotenv->load(); - $this->assertFalse(getenv('FOOX')); - $this->assertFalse(getenv('NOPE')); + + self::assertFalse($repo->has('FOOX')); + self::assertFalse($repo->has('NOPE')); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: FOOX is missing, NOPE is missing.'); + $dotenv->required(['FOOX', 'NOPE']); } + /** + * @doesNotPerformAssertions + */ public function testDotenvRequiredArrayEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = self::createArrayDotenv()[1]; $dotenv->load(); $dotenv->required(['FOO', 'BAR']); - $this->assertTrue(true); } public function testDotenvAssertions() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + [$repo, $dotenv] = self::createArrayDotenv('assertions.env'); + $dotenv->load(); - $this->assertSame('val1', getenv('ASSERTVAR1')); - $this->assertEmpty(getenv('ASSERTVAR2')); - $this->assertSame('val3 ', getenv('ASSERTVAR3')); - $this->assertSame('0', getenv('ASSERTVAR4')); - $this->assertSame('#foo', getenv('ASSERTVAR5')); - $this->assertSame("val1\nval2", getenv('ASSERTVAR6')); - $this->assertSame("\nval3", getenv('ASSERTVAR7')); - $this->assertSame("val3\n", getenv('ASSERTVAR8')); + + self::assertSame('val1', $repo->get('ASSERTVAR1')); + self::assertSame('', $repo->get('ASSERTVAR2')); + self::assertSame('val3 ', $repo->get('ASSERTVAR3')); + self::assertSame('0', $repo->get('ASSERTVAR4')); + self::assertSame('#foo', $repo->get('ASSERTVAR5')); + self::assertSame("val1\nval2", $repo->get('ASSERTVAR6')); + self::assertSame("\nval3", $repo->get('ASSERTVAR7')); + self::assertSame("val3\n", $repo->get('ASSERTVAR8')); $dotenv->required([ 'ASSERTVAR1', @@ -135,63 +170,44 @@ public function testDotenvAssertions() ])->notEmpty()->allowedValues(['0', 'val1', '#foo']); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: ASSERTVAR2 is empty. - */ public function testDotenvEmptyThrowsRuntimeException() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = self::createArrayDotenv('assertions.env')[1]; $dotenv->load(); - $this->assertEmpty(getenv('ASSERTVAR2')); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: ASSERTVAR2 is empty.'); $dotenv->required('ASSERTVAR2')->notEmpty(); } + /** + * @doesNotPerformAssertions + */ public function testDotenvEmptyWhenNotPresent() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = self::createArrayDotenv('assertions.env')[1]; $dotenv->load(); - $dotenv->ifPresent('ASSERTVAR2_NO_SUCH_VARIABLE')->notEmpty(); - $this->assertTrue(true); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: ASSERTVAR9 is empty. - */ public function testDotenvStringOfSpacesConsideredEmpty() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = self::createArrayDotenv('assertions.env')[1]; $dotenv->load(); - $dotenv->required('ASSERTVAR9')->notEmpty(); - } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: foo is missing. - */ - public function testDotenvValidateRequiredWithoutLoading() - { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); - $dotenv->required('foo'); - } + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: ASSERTVAR9 is empty.'); - public function testDotenvRequiredCanBeUsedWithoutLoadingFile() - { - putenv('REQUIRED_VAR=1'); - $dotenv = Dotenv::create($this->fixturesFolder); - $dotenv->required('REQUIRED_VAR')->notEmpty(); - $this->assertTrue(true); + $dotenv->required('ASSERTVAR9')->notEmpty(); } /** * List of valid boolean values in fixtures/env/booleans.env. * - * @return array + * @return string[][] */ - public function validBooleanValuesDataProvider() + public static function validBooleanValuesDataProvider() { return [ ['VALID_EXPLICIT_LOWERCASE_TRUE'], @@ -222,34 +238,32 @@ public function validBooleanValuesDataProvider() /** * @dataProvider validBooleanValuesDataProvider + * @doesNotPerformAssertions */ - public function testCanValidateBooleans($boolean) + public function testCanValidateBooleans(string $boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); - $dotenv->required($boolean)->isBoolean(); - $this->assertTrue(true); } /** * @dataProvider validBooleanValuesDataProvider + * @doesNotPerformAssertions */ - public function testCanValidateBooleansIfPresent($boolean) + public function testCanValidateBooleansIfPresent(string $boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); - $dotenv->ifPresent($boolean)->isBoolean(); - $this->assertTrue(true); } /** * List of non-boolean values in fixtures/env/booleans.env. * - * @return array + * @return string[][] */ - public function invalidBooleanValuesDataProvider() + public static function invalidBooleanValuesDataProvider() { return [ ['INVALID_SOMETHING'], @@ -266,63 +280,64 @@ public function invalidBooleanValuesDataProvider() /** * @dataProvider invalidBooleanValuesDataProvider - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: INVALID_ */ - public function testCanInvalidateNonBooleans($boolean) + public function testCanInvalidateNonBooleans(string $boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: INVALID_'); + $dotenv->required($boolean)->isBoolean(); } /** * @dataProvider invalidBooleanValuesDataProvider - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: INVALID_ */ - public function testCanInvalidateNonBooleansIfPresent($boolean) + public function testCanInvalidateNonBooleansIfPresent(string $boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: INVALID_'); + $dotenv->ifPresent($boolean)->isBoolean(); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: VAR_DOES_NOT_EXIST_234782462764 - */ public function testCanInvalidateBooleanNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: VAR_DOES_NOT_EXIST_234782462764'); + $dotenv->required(['VAR_DOES_NOT_EXIST_234782462764'])->isBoolean(); } + /** + * @doesNotPerformAssertions + */ public function testIfPresentBooleanNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'booleans.env'); $dotenv->load(); - $dotenv->ifPresent(['VAR_DOES_NOT_EXIST_234782462764'])->isBoolean(); - $this->assertTrue(true); } /** * List of valid integer values in fixtures/env/integers.env. * - * @return array + * @return string[][] */ - public function validIntegerValuesDataProvider() + public static function validIntegerValuesDataProvider() { return [ ['VALID_ZERO'], ['VALID_ONE'], ['VALID_TWO'], - ['VALID_LARGE'], ['VALID_HUGE'], ]; @@ -330,34 +345,32 @@ public function validIntegerValuesDataProvider() /** * @dataProvider validIntegerValuesDataProvider + * @doesNotPerformAssertions */ - public function testCanValidateIntegers($integer) + public function testCanValidateIntegers(string $integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); - $dotenv->required($integer)->isInteger(); - $this->assertTrue(true); } /** * @dataProvider validIntegerValuesDataProvider + * @doesNotPerformAssertions */ - public function testCanValidateIntegersIfPresent($integer) + public function testCanValidateIntegersIfPresent(string $integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); - $dotenv->ifPresent($integer)->isInteger(); - $this->assertTrue(true); } /** * List of non-integer values in fixtures/env/integers.env. * - * @return array + * @return string[][] */ - public function invalidIntegerValuesDataProvider() + public static function invalidIntegerValuesDataProvider() { return [ ['INVALID_SOMETHING'], @@ -375,86 +388,92 @@ public function invalidIntegerValuesDataProvider() /** * @dataProvider invalidIntegerValuesDataProvider - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: INVALID_ */ - public function testCanInvalidateNonIntegers($integer) + public function testCanInvalidateNonIntegers(string $integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: INVALID_'); + $dotenv->required($integer)->isInteger(); } /** * @dataProvider invalidIntegerValuesDataProvider - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: INVALID_ */ - public function testCanInvalidateNonIntegersIfExist($integer) + public function testCanInvalidateNonIntegersIfExist(string $integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: INVALID_'); + $dotenv->ifPresent($integer)->isInteger(); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: VAR_DOES_NOT_EXIST_234782462764 - */ public function testCanInvalidateIntegerNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: VAR_DOES_NOT_EXIST_234782462764'); + $dotenv->required(['VAR_DOES_NOT_EXIST_234782462764'])->isInteger(); } + /** + * @doesNotPerformAssertions + */ public function testIfPresentIntegerNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable(self::$folder, 'integers.env'); $dotenv->load(); - $dotenv->ifPresent(['VAR_DOES_NOT_EXIST_234782462764'])->isInteger(); - $this->assertTrue(true); } + /** + * @doesNotPerformAssertions + */ public function testDotenvRegexMatchPass() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable(self::$folder); $dotenv->load(); $dotenv->required('FOO')->allowedRegexValues('([[:lower:]]{3})'); - $this->assertTrue(true); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: FOO does not match "/^([[:lower:]]{1})$/". - */ public function testDotenvRegexMatchFail() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable(self::$folder); $dotenv->load(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: FOO does not match "/^([[:lower:]]{1})$/".'); + $dotenv->required('FOO')->allowedRegexValues('/^([[:lower:]]{1})$/'); } - /** - * @expectedException \Dotenv\Exception\ValidationException - * @expectedExceptionMessage One or more environment variables failed assertions: FOO does not match "/([[:lower:]{1{". - */ public function testDotenvRegexMatchError() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable(self::$folder); $dotenv->load(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('One or more environment variables failed assertions: FOO does not match "/([[:lower:]{1{".'); + $dotenv->required('FOO')->allowedRegexValues('/([[:lower:]{1{'); } + /** + * @doesNotPerformAssertions + */ public function testDotenvRegexMatchNotPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable(self::$folder); $dotenv->load(); $dotenv->ifPresent('FOOOOOOOOOOO')->allowedRegexValues('([[:lower:]]{3})'); - $this->assertTrue(true); } } diff --git a/tests/fixtures/.env b/tests/fixtures/.env deleted file mode 100644 index 82390349..00000000 --- a/tests/fixtures/.env +++ /dev/null @@ -1,6 +0,0 @@ -FOO=bar -BAR=baz -SPACED=with spaces - -NULL= - diff --git a/tests/fixtures/env/example.env b/tests/fixtures/env/example.env new file mode 100644 index 00000000..89c90230 --- /dev/null +++ b/tests/fixtures/env/example.env @@ -0,0 +1 @@ +EG="example" diff --git a/tests/fixtures/env/exported.env b/tests/fixtures/env/exported.env index fe286ae2..6eb325fb 100644 --- a/tests/fixtures/env/exported.env +++ b/tests/fixtures/env/exported.env @@ -1,5 +1,7 @@ -export EFOO="bar" -export EBAR="baz" +export EFOO="bar" +export EBAR = "baz" export ESPACED="with spaces" +export "EDQUOTED" = 123 +export 'ESQUOTED' = 456 -export ENULL="" +export ENULL="" diff --git a/tests/fixtures/env/large.env b/tests/fixtures/env/large.env index 56506317..d2bd6cac 100644 --- a/tests/fixtures/env/large.env +++ b/tests/fixtures/env/large.env @@ -1 +1,2 @@ LARGE='111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' +HUGE='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' diff --git a/tests/fixtures/env/multibyte.env b/tests/fixtures/env/multibyte.env new file mode 100644 index 00000000..5c264f3f --- /dev/null +++ b/tests/fixtures/env/multibyte.env @@ -0,0 +1,3 @@ +MB1="Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě" +MB2=行内支付 +APP_ENV=🚀 diff --git a/tests/fixtures/env/multiline.env b/tests/fixtures/env/multiline.env index 5ea54b09..ae1db4b5 100644 --- a/tests/fixtures/env/multiline.env +++ b/tests/fixtures/env/multiline.env @@ -7,3 +7,8 @@ TEST_NS='test\ntest' TEST_EQD="https://vision.googleapis.com/v1/images:annotate?key=" TEST_EQS='https://vision.googleapis.com/v1/images:annotate?key=' + +BASE64_ENCODED_MULTILINE="qS1zCzMVVUJWQShokv6YVYi+ruKSC/bHV7GmEiyVkLaBWJHNVHCHsgTksEBsy8wJ +uwycAvR07ZyOJJed4XTRMKnKp1/v+6UATpWzkIjZXytK+pD+XlZimUHTx3uiDcmU +jhQX1wWSxHDqrSWxeIJiTD+BuUyId8FzmXQ3TcBydJ474tmOU2F492ubk3LAiZ18 +mhiRGoshXAOSbS/P3+RZi4bDeNE/No4=" diff --git a/tests/fixtures/env/nested.env b/tests/fixtures/env/nested.env index 063f06b3..002cf89e 100644 --- a/tests/fixtures/env/nested.env +++ b/tests/fixtures/env/nested.env @@ -1,4 +1,4 @@ -NVAR1="Hello" +NVAR1="Hellō" NVAR2="World!" NVAR3="{$NVAR1} {$NVAR2}" NVAR4="${NVAR1} ${NVAR2}" @@ -8,3 +8,8 @@ NVAR7="${N.VAR6}" NVAR8="" NVAR9="${NVAR8}" NVAR10="${NVAR888}" +NVAR11="NVAR1" +NVAR12="${${NVAR11}}" +NVAR13='${${NVAR11}}' +NVAR14='${NVAR1} ${NVAR2}' +NVAR15="\${NVAR1} \${NVAR2}" diff --git a/tests/fixtures/env/quoted.env b/tests/fixtures/env/quoted.env index 20ddc246..bce0aa3e 100644 --- a/tests/fixtures/env/quoted.env +++ b/tests/fixtures/env/quoted.env @@ -8,6 +8,4 @@ QWHITESPACE = "no space" QESCAPED="test some escaped characters like a quote (\") or maybe a backslash (\\)" QSLASH="iiiiviiiixiiiiviiii\\n" - -SQESCAPED='test some escaped characters like a quote (\') or maybe a backslash (\\)' SQSLASH='iiiiviiiixiiiiviiii\\n' diff --git a/tests/fixtures/env/unicodevarnames.env b/tests/fixtures/env/unicodevarnames.env new file mode 100644 index 00000000..e86f24ef --- /dev/null +++ b/tests/fixtures/env/unicodevarnames.env @@ -0,0 +1,2 @@ +AlbertÅberg=Skybert +ДатаЗакрытияРасчетногоПериода='2022-04-01T00:00' diff --git a/tests/fixtures/env/utf8-with-bom-encoding.env b/tests/fixtures/env/utf8-with-bom-encoding.env new file mode 100644 index 00000000..29c07697 --- /dev/null +++ b/tests/fixtures/env/utf8-with-bom-encoding.env @@ -0,0 +1,3 @@ +FOO=bar +BAR=baz +SPACED="with spaces" diff --git a/tests/fixtures/env/windows.env b/tests/fixtures/env/windows.env new file mode 100644 index 00000000..e89299da --- /dev/null +++ b/tests/fixtures/env/windows.env @@ -0,0 +1 @@ +MBW="" diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json new file mode 100644 index 00000000..b9bd8f9b --- /dev/null +++ b/vendor-bin/phpstan/composer.json @@ -0,0 +1,15 @@ +{ + "require": { + "php": "^8.4", + "phpstan/phpstan": "2.1.11", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan-deprecation-rules": "2.0.1", + "phpstan/phpstan-strict-rules": "2.0.4" + }, + "config": { + "preferred-install": "dist", + "allow-plugins": { + "phpstan/extension-installer": true + } + } +} diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json new file mode 100644 index 00000000..1295a2fd --- /dev/null +++ b/vendor-bin/psalm/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "php": "^8.4", + "psalm/phar": "6.10.0" + }, + "config": { + "preferred-install": "dist" + } +}