PSR-1 includes recommendation 2.3. Side Effects:
A file SHOULD declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it SHOULD execute logic with side effects, but SHOULD NOT do both.
Consider this example (my own) inside of a config.php file:
/**
* Parsing the database URL.
* DATABASE_URL is in the form:
* postgres://user:password#hostname:port/database
* e.g.:
* postgres://u123:pabc#ec2.eu-west-1.compute.amazonaws.com:5432/dxyz
*/
$url = parse_url(getenv('DATABASE_URL'));
define('DB_HOST', $url['host']);
define('DB_NAME', substr($url['path'], 1)); // get rid of initial slash
define('DB_USER', $url['user']);
define('DB_PASSWORD', $url['pass']);
If I do this, I'm effectively not respecting the recommendation. phpcs will, rightfully, complain about it, because of the variable:
FILE: config.php
-----------------------------------------------------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
-----------------------------------------------------------------------------------------------------------------
1 | WARNING | A file should declare new symbols (classes, functions, constants, etc.) and cause no other side
| | effects, or it should execute logic with side effects, but should not do both. The first symbol
| | is defined on line 17 and the first side effect is on line 162.
-----------------------------------------------------------------------------------------------------------------
An alternative would be this:
define('DB_HOST', parse_url(getenv('DATABASE_URL'))['host']);
define('DB_NAME', substr(parse_url(getenv('DATABASE_URL'))['path'], 1));
define('DB_USER', parse_url(getenv('DATABASE_URL'))['user']);
define('DB_PASSWORD', parse_url(getenv('DATABASE_URL'))['pass']);
No variable, no problem. But this is WET and hard to read.
I understand the recommendation is just that, and that it says "SHOULD", not "MUST". But this still bugs me… For one thing, anytime I check the file phpcs will complain about it, but report it just once per line, leaving the door open to adding more "side effects" which have no place in a config file.
I'm still new to this whole PSR thing.
Did I miss any clever way to get rid of the variable, while keeping things readable?
A corollary would be: how do serious projects, that insist on following recommendations to the letter, handle this?
1. It's fine, don't sweat it
You already mention it in your question, but this recommendation is a SHOULD and not a MUST.
If this is the only PSR-1 issue in your entire project: good job!
But your question was: how do other projects go about this?
2. Move away from defines for configuration
Global constants, when used incorrectly, are dependency magnets. They introduce coupling and make your code harder to digest. This Q&A is a very good read on why you should move away from them.
Use dependency injection instead (yes, scalar configuration constants are also dependencies).
3. Case study: Symfony
Symfony-based projects use:
either YAML (recommended) or XML configuration files to configure the dependency injection container, along with
environment variables, to set the configuration options specific to each environment in which the application should run. These env vars are defined in environment-specific .env files.
For example, to configure a Database service in a Symfony project you'd create a YAML file that contains:
services:
My\Database\Factory: # <-- the class we are configuring
arguments:
$url: '%env(DATABASE_URL)' # <-- configure the $url constructor argument
Symfony compiles this into PHP code, injecting the DATABASE_URL environment variable into the class that requires it.
You would then parse DATABASE_URL in the constructor of the My\Database\Factory class, and use the result to construct your database class.
Pros:
Configuration is separated from code
Configuration is easy to change
Configuration is easy to read
Cons:
Dependency injection and using a DI container has a learning curve and requires a change in the way you think about constructing objects.
As stated in the twelve-factors app methodology:
Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.
The twelve-factor app stores config in environment variables. Env vars are easy to change between deploys without changing any code
You're on the right track about best practices, you just need to correct some mistakes.
1. Use variables for environment variables
You want to use constants for things that are not. The value of the database name can vary according to the environment. It's NOT a constant, it's a (environment) variable, you should use $dbName = getenv('DB_NAME').
In contrast, the number π is a constant, it will never change and can be hardcoded.
You can have a look at the source code of open-source projects like Composer or the Symfony components, you'll see getenv() used to populate variables only.
2. Use directly the elements expected in the configuration
In your case you shouldn't use the full database URL as a single configuration item. You should instead separate each element in environment variables like DB_HOST, DB_NAME, DB_PORT, as expected by the configuration.
Related
I'm trying to get a TYPO3 v8 system updated to TYPO3 v9, but when it comes to unit-testing, I got some errors. I was able to fix some of them on my own but this one here's a very difficult one for me, because unit-testing is somewhat new to me in general.
I already searched the web, the TYPO3 documentation (which seems like the important parts are missing?), asked some friends and tried some things on my own, but nothing helped.
$this->environmentMock = $this->createMock(Environment::class);
$this->environmentMock->expects($this->once())
->method("::isCli")
->will($this->returnValue(TRUE));
I'm expecting to manually override the static function ::isCli() that comes with the Environment class. If that's not possible, is there any other "workaround", like setting a protected variable or something like that?
Currently this is my error message:
Trying to configure method "::isCli" which cannot be configured because it does not exist, has not been specified, is final, or is static
Thanks in advance!
Update 1:
After using #susis tip, I get the following error when appending the code:
TypeError: Return value of TYPO3\CMS\Core\Core\Environment::getContext() must be an instance of TYPO3\CMS\Core\Core\ApplicationContext, null returned
Additional information: My project is just an extension folder with TYPO3 v9 sources required in its own composer.json. No web, no htdocs, just the extension folder.
Update 2:
Here's a full gist of my test file.
Update 3:
Even the debugger isn't helping me in this case, see attached screenshot:
xdebug phpstorm applicationcontext environment screenshot
Update 4:
I updated the gist, added the environment vars to the phpunit.xml file and added parent::setUp() to the top of the setUp() method but the error is still the same:
TypeError : Return value of TYPO3\CMS\Core\Core\Environment::getContext() must be an instance of TYPO3\CMS\Core\Core\ApplicationContext, null returned
/Users/xyz/my_redirect/public/typo3/sysext/core/Classes/Core/Environment.php:97
/Users/xyz/my_redirect/Tests/Unit/Hooks/RequestHandlerHookTest.php:41
Update 5:
I updated the gist and removed the environment settings from the phpunit.xml due to what I've seen that they didn't work either. At this moment, the test is working but I'm still not sure if it's done the right way. Thanks for your help!
You can initialize the Environment you want in your tests, for example with:
Environment::initialize(
Environment::getContext(),
true,
false,
Environment::getProjectPath(),
Environment::getPublicPath(),
Environment::getVarPath(),
Environment::getConfigPath(),
Environment::getBackendPath() . '/index.php',
Environment::isWindows() ? 'WINDOWS' : 'UNIX'
);
This is the same way as it is done in TYPO3 Core tests and allows you to customize the complete environment. If you are using the TYPO3 testing framework / UnitTestCase base classes, you can use the property protected $backupEnvironment = true; to make sure the environment is reset after your test.
For an example, you can have a look at the ResourceCompressorIntegrationTest
I'm having trouble getting Drupal 8 to load my custom module's classes from the same namespace. Here's what I've got:
<?php
namespace Drupal\sign_up\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
// ... snip ...
class StandardForm extends CLESignUpForm
{
Which all works perfectly well on my local environment. When deployed the server says: PHP Fatal error: Class 'Drupal\sign_up\Form\CLESignUpForm' not found in /path/to/drupal/modules/sign_up/src/Form/StandardForm.php on line 21
If I add the line:
include 'CLESignupForm.php';
It works just fine. But that defeats the purpose of auto-loading classes, doesn't it? get_declared_classes() does not show my base class .. or even the currently loaded class for that matter.
Any thoughts on how to make this particular environment comply? I've already loaded the db and config directly from my local setup where it's working. Could it be a php / apache setting somehow?
Turns out I'm a knucklehead. CLESignUpForm is the incorrect capitalization of the type. The correct capitalization is CLESignupForm. Still not clear why it works in every other environment and not this one, but it makes me feel like there's a bit more sanity at least.
Are there any methods to catch the php commands instead of executing them?
There's a backdoor which is encoded in my server, I was able to get declared classes and methods names, of the backdoor, but I need to find out the real code.
I can think of 2 things.
1. Use a profiler tool.
2. In your own code define a class with the same name. Then when the backdoor class is loaded, it will give a fatal error ('class name already defined') and tell you where the other class resides. Do enable stacktraces in case the backdoor is obfuscated/eval'd/remote included.
I'm using dotenv for PHP to manage the environment settings (not lavarel but I tagged it because lavarel also uses dotenv)
I have excluded the .env from the code base and I have added the .env.example for all other collaborators
On the github page of dotenv:
phpdotenv is made for development environments, and generally should not be used in production. In production, the actual environment variables should be set so that there is no overhead of loading the .env file on each request. This can be achieved via an automated deployment process with tools like Vagrant, chef, or Puppet, or can be set manually with cloud hosts like Pagodabox and Heroku.
The thing that I don't understand is that I get the following exception:
PHP Fatal error: Uncaught exception 'InvalidArgumentException' with message 'Dotenv: Environment file .env not found or not readable.
This contradicts with what documentation says "the actual environment variables should be set so that there is no overhead of loading the .env file on each request."
So the question is if there's any reason why dotenv throws that exception and/or am I missing something? First of all the behavior is different compared to other dotenv libraries (ruby)
I can easily work around this, the not so nice solution:
if(getenv('APPLICATION_ENV') !== 'production') { /* or staging */
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();
}
Nicest solution in my opinion, but I think dotenv should handle this.
$dotenv = new Dotenv\Dotenv(__DIR__);
//Check if file exists the same way as dotenv does it
//See classes DotEnv\DotEnv and DotEnv\Loader
//$filePath = $dotenv->getFilePath(__DIR__);
//This method is protected so extract code from method (see below)
$filePath = rtrim(__DIR__, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR . '.env';
//both calls are cached so (almost) no performance loss
if(is_file($filePath) && is_readable($filePath)) {
$dotenv->load();
}
Dotenv was built around an idea, that it will be used in development environments only. Thus, it always expects .env file to be present.
The solution you didn't like is a recommended way to use Dotenv. And it seems, that it won't change in near future. Related discussion in project's issue tracker: https://github.com/vlucas/phpdotenv/issues/63#issuecomment-74561880
Note, that Mark offers there a good approach for production/staging environments, which skips file loading, but not validation
$dotenv = new Dotenv\Dotenv();
if(getenv('APP_ENV') === 'development') {
$dotenv->load(__DIR__);
}
$dotenv->required('OTHER_VAR');
If you have problem to create an APP_ENV variable, this code is easier :
$dotenv = new Dotenv\Dotenv(__DIR__);
if(file_exists(".env")) {
$dotenv->load();
}
Also looked into this, my current solution is to use Lumen's way (as of 6 June 2016) which was suggested in a discussion:
try {
(new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
//
}
You can still do some additional exception handling if needed (e.g. fall to default values or do some validation.
I'm trying to (in a parallel fashion, that's it, as we're still developing under PHP 5.2.x for our main applications) adapt our current applications to PHP 5.4.x (using Apache 2.4.2 with PHP 5.4.1 in our testing server) but I'm finding some warnings in our current applications when we run them in the testing server.
For example... we're using constant definition in our current applications with a function that detects the language and loads a language file containing the definitions like idiomas/lang.php, being lang a 2-digit language representation (es, us, de, and so on).
Inside each language file there's some definitions like this one:
define("IDIOMA_AF", "Afrikaans");
define("IDIOMA_AR", "العربية");
define("IDIOMA_BG", "български език");
So, when we want to output a given translated text, we do this:
<?php echo IDIOMA_AF; ?>
The message PHP 5.4.1 outputs when the application is thrown at it is the following:
Notice: Constant IDIOMA_AF already defined in D:\apache\htdocs\aplicacion\modulos\base\idiomas\es.php on line 34
Does anyone have the same problem? I would like to know a bit about your experiences, as it would be useful to know how to solve these problems. We're thinking on implementing a better system using gettext (which I think is more organized and easier to handle in the long run, but still...), although we would like to run our current applications for a while before upgrading them.
It means you are trying to define the constant twice (which is not allowed).
You could try:
if (!defined('IDIOMA_AF')) define('IDIOMA_AF', 'Afrikaans');
Or trace, and fix, why you are defining a constant twice anyway.
The function get_defined_constants() is also a useful debugging tool for issues like these.
Or, just ignore those errors: error_reporting(E_ALL & ~E_NOTICE);