Overriding Concrete Config FileSaver Not Working

I’m trying to override a Concrete 9.4.4 core file (concrete/src/Config/FileSaver.php) and it is not working. The attempted override is not overriding the core file. Here is my code (located in applicaton/src/Concrete/Config/FileSaver.php):

namespace Application\Concrete\Config;

use Concrete\Core\Config\FileSaver as CoreFileSaver;

class FileSaver extends CoreFileSaver
{
public function save($item, $value, $environment, $group, $namespace = null)
{
// custom code
$result = parent::save($item, $value, $environment, $group, $namespace);
// custom code
return $result;
}
}

That file path does not exist, Application\Config does though.
Maybe try changing the namespace to Application\Config;

Unfortunately, that didn’t work either. Thanks.

Just chiming in to make sure you have Overrides caching off in the Dashboard.

Only a small set of files can be overridden this way. I don’t think Config is one of them.

This tutorial shows how to specifically override the core logger:

Also you might not need to override it depending on what you’re trying to achieve.

Can you tell us more?

Overrides caching is set to off.

As you said, I checked the documentation and it doesn’t appear that Config can be overridden. I really don’t want to change the core file, but will if I have to.

I’m trying to log the username of any user that makes a change to the configuration. I’m definitely open to other ways of doing it.

Thanks for the help given so far!

Can you explain your application. This seems like a strange class to override. With some knowledge of what you are aiming to achieve one of us may be able to direct you to other solutions.

I’m trying to log the username of any user that makes a change to the configuration. I’m definitely open to other ways of doing it.

The Config system is used for other storage as well as stuff that originates from user action in the dashboard. Some is in files and some is in a database table. You could be overwhelmed with unwanted log entries or an entry for every granule of data saved. As well as the dashboard, some blocks use Config to keep track of global options.

My first thought is to consider an on_start event handler (not well considered - others may have better ideas) : IF in_dashboard and the request has POST or GET parameter data, then log it. The log already has columns for page and user. So how much you log is up to you. You would probably want to filter out things like ccm tokens and general dashboard background stuff. You may also need to catch ajax requests with data and filter out anything to do with logging so it doesn’t get stuck in a loop.

The configuration initialization occurs at a very early stage in the bootstrap process of ConcreteCMS.

I think the only viable way to do that is via the application/bootstrap/start.php file.

In that file you should have something like this:

<?php

use Concrete\Core\Application\Application;

$app = new Application();

// ...etcetera...

you can tell Concrete to use your own class when a configuration setting is saved: simply replace the lines above with:

<?php

use Concrete\Core\Application\Application;
use Concrete\Core\Config\SaverInterface;
use Concrete\Core\User\User;

class MySaver implements SaverInterface
{
    /**
     * @var SaverInterface
     */
    private $actualSaver;

    /**
     * @var Application
     */
    private $app;

    public function __construct(SaverInterface $actualSaver, Application $app)
    {
        $this->actualSaver = $actualSaver;
        $this->app = $app;
    }

    /**
     * {@inheritdoc}
     *
     * @see \Concrete\Core\Config\SaverInterface::save()
     */
    public function save($item, $value, $environment, $group, $namespace = null)
    {
        $user = $this->app->make(User::class);
        // do whatever you want
        $userName = $user->isRegistered() ? $user->getUserName() : '<anonymous>';
        $valueSerialized = json_encode($value, JSON_PRETTY_PRINT);
        $logMessage = <<<EOT
            User: {$userName}
            Environment: {$environment}
            Namesmapce: {$namespace}
            Key: {$item}
            New value: {$valueSerialized}
            EOT
        ;
        // Store $logMessage somewhere

        return $this->actualSaver->save($item, $value, $environment, $group, $namespace);
    }
}

$app = new Application();

$app->extend(
    SaverInterface::class,
    static function(SaverInterface $saver, Application $app) {
        return new MySaver($saver, $app);
    }
);

// ...etcetera...

Thanks! This works! Is there a way to move the MySaver class into a separate file in the application folder?

Sure!

You can save your classes under application/src/Concrete, and their namespace must be Application\Concrete.

For example, you can define the class Application\Concrete\MySaver in the file application/src/Concrete/MySaver.php containing:

<?php

namespace Application\Concrete;

class MySaver
{
    // ...
}

Of course, you can use sub-directories, mapped to sub-namespace names.

For example you could store the class Application\Concrete\Config\MySaver in the file application/src/Concrete/Config/MySaver.php containing:

<?php

namespace Application\Concrete\Config;

class MySaver
{
    // ...
}

If you don’t like the Application prefix of the namespace, you can set/add something like this to the application/config/app.php file:

<?php

return [
    'namespace' => 'MyWonderfulNamespace',
];

Then you can you can define the class MyWonderfulNamespace\Concrete\Config\MySaver in the file application/src/Concrete/Config/MySaver.php containing:

<?php

namespace MyWonderfulNamespace\Concrete\Config;

class MySaver
{
    // ...
}

(I’m 99% sure the Concrete part of the namespace and file path must be present, unless you configure a custom autoloader).

And double check case sensitivity, as in the examples above…

I already attempted it and it doesn’t seem to be working. Do I need to modify anything in the “application/bootstrap/start.php” file?

I moved the class out and I’m no longer seeing changes to the configuration in the log. I’m also not seeing any errors.

What’s the path and name of the file where you stored your class?

What’s its namespace and class name?

Could you share the relevant section of your application/bootstrap/start.php file?

Stored the class Application\Concrete\Config\MySaver in the file application/src/Concrete/Config/MySaver.php.

The namespace is “Application\Concrete\Config”.

In the application/bootstrap/start.php file:

use Concrete\Core\Application\Application;
use Application\Concrete\Config;

$app = new Application();

$app->detectEnvironment(
    array(
        'local' => array(
            'hostname',
        ),
        'production' => array(
            'live.site',
        ),
    ));

$app->extend(
    SaverInterface::class,
    static function(SaverInterface $saver, Application $app) {
        return new MySaver($saver, $app);
    }
);

return $app

You have

use Application\Concrete\Config;

//...

return new MySaver(...);

That’s not correct. You can use one of the following two alternatives:

use Application\Concrete\Config;

//...

return new Config\MySaver(...);

or

use Application\Concrete\Config\MySaver;

//...

return new MySaver(...);

Also, be sure you have this line too:

use Concrete\Core\Config\SaverInterface;

I have that line in the MySaver.php file. Is that correct?

Also, I tried both of your suggestions above and it is still not working.

Disregard. I figured it out mostly what you provided so far and correcting my mistakes. Thanks again for everyone’s help!

Here is what I put in the application/bootstrap/start.php file:

<?php

use Concrete\Core\Application\Application;
use Concrete\Core\Config\SaverInterface;
use Application\Concrete\Config\MySaver;

$app = new Application();

$app->detectEnvironment(
    array(
        'local' => array(
            'hostname',
        ),
        'production' => array(
            'live.site',
        ),
    ));

$app->extend(
    SaverInterface::class,
    static function(SaverInterface $saver, Application $app) {
        return new MySaver($saver, $app);
    }
);

return $app;

Here is what I put in the application/src/Concrete/Config/MySaver.php file:

<?php
namespace Application\Concrete\Config;

use Concrete\Core\Config\SaverInterface;
use Concrete\Core\User\User;
use Concrete\Core\Application\Application;

class MySaver implements SaverInterface
{
    private $actualSaver;

    private $app;

    public function __construct(SaverInterface $actualSaver, Application $app)
    {
        $this->actualSaver = $actualSaver;
        $this->app = $app;
    }

    public function save($item, $value, $environment, $group, $namespace = null)
    {
        $user = $this->app->make(User::class);
        $userName = $user->isRegistered() ? $user->getUserName() : '<anonymous>';
        $valueSerialized = json_encode($value, JSON_PRETTY_PRINT);
        $logMessage = <<<EOT

            User: {$userName}
            Environment: {$environment}
            Namespace: {$namespace}
            Key: {$item}
            New value: {$valueSerialized}
            EOT
        ;
        \Log::addInfo(t('The following configuration changes were made: %s.', $logMessage));

        return $this->actualSaver->save($item, $value, $environment, $group, $namespace);
    }
}