I have a custom CMS that I am writing from scratch in Laravel and want to set env values i.e. database details, mailer details, general configuration, etc from controller once the user sets up and want to give user the flexibility to change them on the go using the GUI that I am making.
So my question is how do I write the values received from user to the .env file as an when I need from the controller.
And is it a good idea to build the .env file on the go or is there any other way around it?
Since Laravel uses config files to access and store .env data, you can set this data on the fly with config() method:
config(['database.connections.mysql.host' => '127.0.0.1']);
To get this data use config():
config('database.connections.mysql.host')
To set configuration values at runtime, pass an array to the config helper
https://laravel.com/docs/5.3/configuration#accessing-configuration-values
Watch out! Not all variables in the laravel .env are stored in the config environment.
To overwrite real .env content use simply:
putenv ("CUSTOM_VARIABLE=hero");
To read as usual, env('CUSTOM_VARIABLE') or env('CUSTOM_VARIABLE', 'devault')
NOTE: Depending on which part of your app uses the env setting, you may need to set the variable early by placing it into your index.php or bootstrap.php file. Setting it in your app service provider may be too late for some packages/uses of the env settings.
More simplified:
public function putPermanentEnv($key, $value)
{
$path = app()->environmentFilePath();
$escaped = preg_quote('='.env($key), '/');
file_put_contents($path, preg_replace(
"/^{$key}{$escaped}/m",
"{$key}={$value}",
file_get_contents($path)
));
}
or as helper:
if ( ! function_exists('put_permanent_env'))
{
function put_permanent_env($key, $value)
{
$path = app()->environmentFilePath();
$escaped = preg_quote('='.env($key), '/');
file_put_contents($path, preg_replace(
"/^{$key}{$escaped}/m",
"{$key}={$value}",
file_get_contents($path)
));
}
}
Based on josh's answer. I needed a way to replace the value of a key inside the .env file.
But unlike josh's answer, I did not want to depend on knowing the current value or the current value being accessible in a config file at all.
Since my goal is to replace values that are used by Laravel Envoy which doesn't use a config file at all but instead uses the .env file directly.
Here's my take on it:
public function setEnvironmentValue($envKey, $envValue)
{
$envFile = app()->environmentFilePath();
$str = file_get_contents($envFile);
$oldValue = strtok($str, "{$envKey}=");
$str = str_replace("{$envKey}={$oldValue}", "{$envKey}={$envValue}\n", $str);
$fp = fopen($envFile, 'w');
fwrite($fp, $str);
fclose($fp);
}
Usage:
$this->setEnvironmentValue('DEPLOY_SERVER', 'forge#122.11.244.10');
Based on totymedli's answer.
Where it is required to change multiple enviroment variable values at once, you could pass an array (key->value). Any key not present previously will be added and a bool is returned so you can test for success.
public function setEnvironmentValue(array $values)
{
$envFile = app()->environmentFilePath();
$str = file_get_contents($envFile);
if (count($values) > 0) {
foreach ($values as $envKey => $envValue) {
$str .= "\n"; // In case the searched variable is in the last line without \n
$keyPosition = strpos($str, "{$envKey}=");
$endOfLinePosition = strpos($str, "\n", $keyPosition);
$oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition);
// If key does not exist, add it
if (!$keyPosition || !$endOfLinePosition || !$oldLine) {
$str .= "{$envKey}={$envValue}\n";
} else {
$str = str_replace($oldLine, "{$envKey}={$envValue}", $str);
}
}
}
$str = substr($str, 0, -1);
if (!file_put_contents($envFile, $str)) return false;
return true;
}
#more simple way to cover .env you can do like this
$_ENV['key'] = 'value';
tl;dr
Based on vesperknight's answer I created a solution that doesn't use strtok or env().
private function setEnvironmentValue($envKey, $envValue)
{
$envFile = app()->environmentFilePath();
$str = file_get_contents($envFile);
$str .= "\n"; // In case the searched variable is in the last line without \n
$keyPosition = strpos($str, "{$envKey}=");
$endOfLinePosition = strpos($str, PHP_EOL, $keyPosition);
$oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition);
$str = str_replace($oldLine, "{$envKey}={$envValue}", $str);
$str = substr($str, 0, -1);
$fp = fopen($envFile, 'w');
fwrite($fp, $str);
fclose($fp);
}
Explanation
This doesn't use strtok that might not work for some people, or env() that won't work with double-quoted .env variables which are evaluated and also interpolates embedded variables
KEY="Something with spaces or variables ${KEY2}"
If you don't need to save your changes to .env file, you could simply user this code :
\Illuminate\Support\Env::getRepository()->set('APP_NAME','New app name');
In the event that you want these settings to be persisted to the environment file so they be loaded again later (even if the configuration is cached), you can use a function like this. I'll put the security caveat in there, that calls to a method like this should be gaurded tightly and user input should be sanitized properly.
private function setEnvironmentValue($environmentName, $configKey, $newValue) {
file_put_contents(App::environmentFilePath(), str_replace(
$environmentName . '=' . Config::get($configKey),
$environmentName . '=' . $newValue,
file_get_contents(App::environmentFilePath())
));
Config::set($configKey, $newValue);
// Reload the cached config
if (file_exists(App::getCachedConfigPath())) {
Artisan::call("config:cache");
}
}
An example of it's use would be;
$this->setEnvironmentValue('APP_LOG_LEVEL', 'app.log_level', 'debug');
$environmentName is the key in the environment file (example.. APP_LOG_LEVEL)
$configKey is the key used to access the configuration at runtime (example.. app.log_level (tinker config('app.log_level')).
$newValue is of course the new value you wish to persist.
If you want to change it temporarily (e.g. for a test), this should work for Laravel 8:
<?php
namespace App\Helpers;
use Dotenv\Repository\Adapter\ImmutableWriter;
use Dotenv\Repository\AdapterRepository;
use Illuminate\Support\Env;
class DynamicEnvironment
{
public static function set(string $key, string $value)
{
$closure_adapter = \Closure::bind(function &(AdapterRepository $class) {
$closure_writer = \Closure::bind(function &(ImmutableWriter $class) {
return $class->writer;
}, null, ImmutableWriter::class);
return $closure_writer($class->writer);
}, null, AdapterRepository::class);
return $closure_adapter(Env::getRepository())->write($key, $value);
}
}
Usage:
App\Helpers\DynamicEnvironment::set("key_name", "value");
Based on totymedli's answer and Oluwafisayo's answer.
I set a little modification to change the .env file, It works too fine in Laravel 5.8, but when after I changed it the .env file was modificated I could see variables did not change after I restart with php artisan serve, so I tried to clear cache and others but I can not see a solution.
public function setEnvironmentValue(array $values)
{
$envFile = app()->environmentFilePath();
$str = file_get_contents($envFile);
$str .= "\r\n";
if (count($values) > 0) {
foreach ($values as $envKey => $envValue) {
$keyPosition = strpos($str, "$envKey=");
$endOfLinePosition = strpos($str, "\n", $keyPosition);
$oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition);
if (is_bool($keyPosition) && $keyPosition === false) {
// variable doesnot exist
$str .= "$envKey=$envValue";
$str .= "\r\n";
} else {
// variable exist
$str = str_replace($oldLine, "$envKey=$envValue", $str);
}
}
}
$str = substr($str, 0, -1);
if (!file_put_contents($envFile, $str)) {
return false;
}
app()->loadEnvironmentFrom($envFile);
return true;
}
So it rewrites correctly the .env file with the funtion setEnvironmentValue, but How can Laravel reload the new .env without to restart the system?
I was looking information about that and I found
Artisan::call('cache:clear');
but in local it does not work! for me, but when I uploaded the code and test in my serve it works to fine.
I tested it in Larave 5.8 and worked in my serve...
This could be a tip when you have a variable with more than one word and separetly with a space, the solution i did
public function update($variable, $value)
{
if ($variable == "APP_NAME" || $variable == "MAIL_FROM_NAME") {
$value = "\"$value\"";
}
$values = array(
$variable=>$value
);
$this->setEnvironmentValue($values);
Artisan::call('config:clear');
return true;
}
Tailor Otwell generate the laravel application key and set its value using this code (code modified for example purpose):
$escaped = preg_quote('='.config('broadcasting.default'), '/');
file_put_contents(app()->environmentFilePath(), preg_replace("/^BROADCAST_DRIVER{$escaped}/m", 'BROADCAST_DRIVER='.'pusher',
file_get_contents(app()->environmentFilePath())
));
You can find the code in the key generation class:
Illuminate\Foundation\Console\KeyGenerateCommand
you can use this package
https://github.com/ImLiam/laravel-env-set-command
then use Artisan Facade to call artisan commands ex:
Artisan::call('php artisan env:set app_name Example')
PHP 8 solution
This solution is using env() so should only be run when the configuration is NOT cached.
/**
* #param string $key
* #param string $value
*/
protected function setEnvValue(string $key, string $value)
{
$path = app()->environmentFilePath();
$env = file_get_contents($path);
$old_value = env($key);
if (!str_contains($env, $key.'=')) {
$env .= sprintf("%s=%s\n", $key, $value);
} else if ($old_value) {
$env = str_replace(sprintf('%s=%s', $key, $old_value), sprintf('%s=%s', $key, $value), $env);
} else {
$env = str_replace(sprintf('%s=', $key), sprintf('%s=%s',$key, $value), $env);
}
file_put_contents($path, $env);
}
this function update new value of existing key or add new key=value to end of file
function setEnv($envKey, $envValue) {
$path = app()->environmentFilePath();
$escaped = preg_quote('='.env($envKey), '/');
//update value of existing key
file_put_contents($path, preg_replace(
"/^{$envKey}{$escaped}/m",
"{$envKey}={$envValue}",
file_get_contents($path)
));
//if key not exist append key=value to end of file
$fp = fopen($path, "r");
$content = fread($fp, filesize($path));
fclose($fp);
if (strpos($content, $envKey .'=' . $envValue) == false && strpos($content, $envKey .'=' . '\"'.$envValue.'\"') == false){
file_put_contents($path, $content. "\n". $envKey .'=' . $envValue);
}
}
$envFile = app()->environmentFilePath();
$_ENV['APP_NAME'] = "New Project Name";
$txt= "";
foreach ($_ENV as $key => $value) {
$txt .= $key."=".$value."\n"."\n";
}
file_put_contents($envFile,$txt);
This solution can create, replace and save the values given in the .env file.
I created this to be used in a command package:install to add the .env key-value pairs automatically to the .env file.
/**
* Set .env key-value pair
*
* #param array $keyPairs Desired key name
* #return void
*/
public function saveArrayToEnv(array $keyPairs)
{
$envFile = app()->environmentFilePath();
$newEnv = file_get_contents($envFile);
$newlyInserted = false;
foreach ($keyPairs as $key => $value) {
// Make sure key is uppercase (can be left out)
$key = Str::upper($key);
if (str_contains($newEnv, "$key=")) {
// If key exists, replace value
$newEnv = preg_replace("/$key=(.*)\n/", "$key=$value\n", $newEnv);
} else {
// Check if spacing is correct
if (!str_ends_with($newEnv, "\n\n") && !$newlyInserted) {
$newEnv .= str_ends_with($newEnv, "\n") ? "\n" : "\n\n";
$newlyInserted = true;
}
// Append new
$newEnv .= "$key=$value\n";
}
}
$fp = fopen($envFile, 'w');
fwrite($fp, $newEnv);
fclose($fp);
}
You pass it an associative array:
$keyPairs = [
'KEY' => 'VALUE',
'API_KEY' => 'XXXXXX-XXXXXX-XXXXX'
]
And it will add them or replace they values of the given keys with the given values.
Of course there is room for improvement like checking if the array is actually an associative array. But this will do for hard coded stuff in things like Laravel command classes.
This solution builds upon the one provided by Elias Tutungi, it accepts multiple value changes and uses a Laravel Collection because foreach's are gross
function set_environment_value($values = [])
{
$path = app()->environmentFilePath();
collect($values)->map(function ($value, $key) use ($path) {
$escaped = preg_quote('='.env($key), '/');
file_put_contents($path, preg_replace(
"/^{$key}{$escaped}/m",
"{$key}={$value}",
file_get_contents($path)
));
});
return true;
}
You can use this custom method i located from internet ,
/**
* Calls the method
*/
public function something(){
// some code
$env_update = $this->changeEnv([
'DB_DATABASE' => 'new_db_name',
'DB_USERNAME' => 'new_db_user',
'DB_HOST' => 'new_db_host'
]);
if($env_update){
// Do something
} else {
// Do something else
}
// more code
}
protected function changeEnv($data = array()){
if(count($data) > 0){
// Read .env-file
$env = file_get_contents(base_path() . '/.env');
// Split string on every " " and write into array
$env = preg_split('/\s+/', $env);;
// Loop through given data
foreach((array)$data as $key => $value){
// Loop through .env-data
foreach($env as $env_key => $env_value){
// Turn the value into an array and stop after the first split
// So it's not possible to split e.g. the App-Key by accident
$entry = explode("=", $env_value, 2);
// Check, if new key fits the actual .env-key
if($entry[0] == $key){
// If yes, overwrite it with the new one
$env[$env_key] = $key . "=" . $value;
} else {
// If not, keep the old one
$env[$env_key] = $env_value;
}
}
}
// Turn the array back to an String
$env = implode("\n", $env);
// And overwrite the .env with the new data
file_put_contents(base_path() . '/.env', $env);
return true;
} else {
return false;
}
}
use the function below to change values in .env file laravel
public static function envUpdate($key, $value)
{
$path = base_path('.env');
if (file_exists($path)) {
file_put_contents($path, str_replace(
$key . '=' . env($key), $key . '=' . $value, file_get_contents($path)
));
}
}
Replace single value in .env file function:
/**
* #param string $key
* #param string $value
* #param null $env_path
*/
function set_env(string $key, string $value, $env_path = null)
{
$value = preg_replace('/\s+/', '', $value); //replace special ch
$key = strtoupper($key); //force upper for security
$env = file_get_contents(isset($env_path) ? $env_path : base_path('.env')); //fet .env file
$env = str_replace("$key=" . env($key), "$key=" . $value, $env); //replace value
/** Save file eith new content */
$env = file_put_contents(isset($env_path) ? $env_path : base_path('.env'), $env);
}
Example to local (laravel) use: set_env('APP_VERSION', 1.8).
Example to use custom path: set_env('APP_VERSION', 1.8, $envfilepath).
I got a problem.
I make a function to update my config.json file.
The problem is, my config.json is a multdimensional array. To get a value of a key i use this function:
public function read($key)
{
$read = explode('.', $key);
$config = $this->config;
foreach ($read as $key) {
if (array_key_exists($key, $config)) {
$config = $config[$key];
}
}
return $config;
}
I also made a function to update a key. But the problem is if i make update('database.host', 'new value'); it dont updates only that key but it overrides the whole array.
This is my update function
public function update($key, $value)
{
$read = explode('.', $key);
$config = $this->config;
foreach ($read as $key) {
if (array_key_exists($key, $config)) {
if ($key === end($read)) {
$config[$key] = $value;
}
$config = $config[$key];
}
}
print_r( $config );
}
my config.json looks like this:
{
"database": {
"host": "want to update with new value",
"user": "root",
"pass": "1234",
"name": "dev"
},
some more content...
}
I have a working function but thats not really good. I know that the max of the indexes only can be three, so I count the exploded $key and update the value:
public function update($key, $value)
{
$read = explode('.', $key);
$count = count($read);
if ($count === 1) {
$this->config[$read[0]] = $value;
} elseif ($count === 2) {
$this->config[$read[0]][$read[1]] = $value;
} elseif ($count === 3) {
$this->config[$read[0]][$read[1]][$read[3]] = $value;
}
print_r($this->config);
}
Just to know: the variable $this->config is my config.json parsed to an php array, so nothing wrong with this :)
After I had read your question better I now understand what you want, and your read function, though not very clear, works fine.
Your update can be improved though by using assign by reference & to loop over your indexes and assign the new value to the correct element of the array.
What the below code does is assign the complete config object to a temporary variable newconfig using call by reference, this means that whenever we change the newconfig variable we also change the this->config variable.
Using this "trick" multiple times we can in the end assign the new value to the newconfig variable and because of the call by reference assignments the correct element of the this->config object should be updated.
public function update($key, $value)
{
$read = explode('.', $key);
$count = count($read);
$newconfig = &$this->config; //assign a temp config variable to work with
foreach($read as $key){
//update the newconfig variable by reference to a part of the original object till we have the part of the config object we want to change.
$newconfig = &$newconfig[$key];
}
$newconfig = $value;
print_r($this->config);
}
You can try something like this:
public function update($path, $value)
{
array_replace_recursive(
$this->config,
$this->pathToArray("$path.$value")
);
var_dump($this->config);
}
protected function pathToArray($path)
{
$pos = strpos($path, '.');
if ($pos === false) {
return $path;
}
$key = substr($path, 0, $pos);
$path = substr($path, $pos + 1);
return array(
$key => $this->pathToArray($path),
);
}
Please note that you can improve it to accept all data types for value, not only scalar ones
Using PHP, I would like to write a function that accomplishes what is shown by this pseudo code:
function return_value($input_string='array:subArray:arrayKey')
{
$segments = explode(':',$input_string);
$array_depth = count(segments) - 1;
//Now the bit I'm not sure about
//I need to dynamically generate X number of square brackets to get the value
//So that I'm left with the below:
return $array[$subArray][$arrayKey];
}
Is the above possible? I'd really appreciate some pointer on how to acheive it.
You can use a recursive function (or its iterative equivalent since it's tail recursion):
function return_value($array, $input_string) {
$segments = explode(':',$input_string);
// Can we go next step?
if (!array_key_exists($segments[0], $array)) {
return false; // cannot exist
}
// Yes, do so.
$nextlevel = $array[$segments[0]];
if (!is_array($nextlevel)) {
if (1 == count($segments)) {
// Found!
return $nextlevel;
}
// We can return $nextlevel, which is an array. Or an error.
return false;
}
array_shift($segments);
$nextsegments = implode(':', $segments);
// We can also use tail recursion here, enclosing the whole kit and kaboodle
// into a loop until $segments is empty.
return return_value($nextlevel, $nextsegments);
}
Passing one object
Let's say we want this to be an API and pass only a single string (please remember that HTTP has some method limitation in this, and you may need to POST the string instead of GET).
The string would need to contain both the array data and the "key" location. It's best if we send first the key and then the array:
function decodeJSONblob($input) {
// Step 1: extract the key address. We do this is a dirty way,
// exploiting the fact that a serialized array starts with
// a:<NUMBEROFITEMS>:{ and there will be no "{" in the key address.
$n = strpos($input, ':{');
$items = explode(':', substr($input, 0, $n));
// The last two items of $items will be "a" and "NUMBEROFITEMS"
$ni = array_pop($items);
if ("a" != ($a = array_pop($items))) {
die("Something strange at offset $n, expecting 'a', found {$a}");
}
$array = unserialize("a:{$ni}:".substr($input, $n+1));
while (!empty($items)) {
$key = array_shift($items);
if (!array_key_exists($key, $array)) {
// there is not this item in the array.
}
if (!is_array($array[$key])) {
// Error.
}
$array = $array[$key];
}
return $array;
}
$arr = array(
0 => array(
'hello' => array(
'joe','jack',
array('jill')
)));
print decodeJSONblob("0:hello:1:" . serialize($arr));
print decodeJSONblob("0:hello:2:0" . serialize($arr));
returns
jack
jill
while asking for 0:hello:2: would get you an array { 0: 'jill' }.
you could use recursion and array_key_exists to walk down to the level of said key.
function get_array_element($key, $array)
{
if(stripos(($key,':') !== FALSE) {
$currentKey = substr($key,0,stripos($key,':'));
$remainingKeys = substr($key,stripos($key,':')+1);
if(array_key_exists($currentKey,$array)) {
return ($remainingKeys,$array[$currentKey]);
}
else {
// handle error
return null;
}
}
elseif(array_key_exists($key,$array)) {
return $array[$key];
}
else {
//handle error
return null;
}
}
Use a recursive function like the following or a loop using references to array keys
<?php
function lookup($array,$lookup){
if(!is_array($lookup)){
$lookup=explode(":",$lookup);
}
$key = array_shift($lookup);
if(!isset($array[$key])){
//throw exception if key is not found so false values can also be looked up
throw new Exception("Key does not exist");
}else{
$val = $array[$key];
if(count($lookup)){
return lookup($val,$lookup);
}
return $val;
}
}
$config = array(
'db'=>array(
'host'=>'localhost',
'user'=>'user',
'pass'=>'pass'
),
'data'=>array(
'test1'=>'test1',
'test2'=>array(
'nested'=>'foo'
)
)
);
echo "Host: ".lookup($config,'db:host')."\n";
echo "User: ".lookup($config,'db:user')."\n";
echo "More levels: ".lookup($config,'data:test2:nested')."\n";
Output:
Host: localhost
User: user
More levels: foo
I want to record downloads in a text file
Someone comes to my site and downloads something, it will add a new row to the text file if it hasn't already or increment the current one.
I have tried
$filename = 'a.txt';
$lines = file($filename);
$linea = array();
foreach ($lines as $line)
{
$linea[] = explode("|",$line);
}
$linea[0][1] ++;
$a = $linea[0][0] . "|" . $linea[0][1];
file_put_contents($filename, $a);
but it always increments it by more than 1
The text file format is
name|download_count
You're doing your incrementing outside of the for loop, and only accessing the [0]th element so nothing is changing anywhere else.
This should probably look something like:
$filename = 'a.txt';
$lines = file($filename);
// $k = key, $v = value
foreach ($lines as $k=>$v) {
$exploded = explode("|", $v);
// Does this match the site name you're trying to increment?
if ($exploded[0] == "some_name_up_to_you") {
$exploded[1]++;
// To make changes to the source array,
// it must be referenced using the key.
// (If you just change $v, the source won't be updated.)
$lines[$k] = implode("|", $exploded);
}
}
// Write.
file_put_contents($filename, $lines);
You should probably be using a database for this, though. Check out PDO and MYSQL and you'll be on your way to awesomeness.
EDIT
To do what you mentioned in your comments, you can set a boolean flag, and trigger it as you walk through the array. This may warrant a break, too, if you're only looking for one thing:
...
$found = false;
foreach ($lines as $k=>$v) {
$exploded = explode("|", $v);
if ($exploded[0] == "some_name_up_to_you") {
$found = true;
$exploded[1]++;
$lines[$k] = implode("|", $exploded);
break; // ???
}
}
if (!$found) {
$lines[] = "THE_NEW_SITE|1";
}
...
one hand you are using a foreach loop, another hand you are write only the first line into your file after storing it in $a... it's making me confuse what do you have in your .txt file...
Try this below code... hope it will solve your problem...
$filename = 'a.txt';
// get file contents and split it...
$data = explode('|',file_get_contents($filename));
// increment the counting number...
$data[1]++;
// join the contents...
$data = implode('|',$data);
file_put_contents($filename, $data);
Instead of creating your own structure inside a text file, why not just use PHP arrays to keep track? You should also apply proper locking to prevent race conditions:
function recordDownload($download, $counter = 'default')
{
// open lock file and acquire exclusive lock
if (false === ($f = fopen("$counter.lock", "c"))) {
return;
}
flock($f, LOCK_EX);
// read counter data
if (file_exists("$counter.stats")) {
$stats = include "$counter.stats";
} else {
$stats = array();
}
if (isset($stats[$download])) {
$stats[$download]++;
} else {
$stats[$download] = 1;
}
// write back counter data
file_put_contents('counter.txt', '<?php return ' . var_export($stats, true) . '?>');
// release exclusive lock
fclose($f);
}
recordDownload('product1'); // will save in default.stats
recordDownload('product2', 'special'); // will save in special.stats
personally i suggest using a json blob as the content of the text file. then you can read the file into php, decode it (json_decode), manipulate the data, then resave it.