It can happen that you call Settings::getSingleton() or just the procedural wrapper, settings(), before it is initialized.
The components which do that do not properly check if the returned result is not NULL.
Most prominent example is index.php,

try {
  drupal_handle_request();
}
catch (Exception $e) {
  $message = 'If you have just changed code (for example deployed a new module or moved an existing one) read <a href="http://drupal.org/documentation/rebuild">http://drupal.org/documentation/rebuild</a>';
  if (settings()->get('rebuild_access', FALSE)) {

Ouch.

There are some options to get out of this:

  1. Let getSingleton() throw an exception if not initialized.
  2. Split in two accessor methods:
    Settings::getSingleton() -> may return NULL, which needs to be properly checked for.
    Settings::requireSingleton() -> will never return NULL, but will throw an exception if not initialized.
  3. Rethink the entire singleton design. (*)

--------------

(*) There are many things wrong about the singleton "pattern", which is nicely summarized here,
http://stackoverflow.com/questions/137975/what-is-so-bad-about-singleton...
Points 1., 3. and 4 refer to the smell of global state, which I think we can excuse by being surrounded by (legacy) code which depends on global state.

But point 2 is interesting,

They violate the Single Responsibility Principle: by virtue of the fact that they control their own creation and lifecycle.

In fact, our Drupal 7 style procedural static cache pattern did not have this flaw (depending how it was implemented, of course): The static cache variable lived in a different place than the class whose instance was being cached. Whether to call that a singleton or not, I dunno.

But in the particular implementation we use in the Settings class, the flaw mentioned above is clearly present:

  function __construct(array $settings) {
    $this->storage = $settings;
    self::$instance = $this;
  }

Problems:

  1. Why is it the constructor's job to set the static instance?
    A constructor should have no side effects, this one has. And it could be so easily avoided.
  2. Why do the instantiated class be the same class as the one that has the static accessor method and static instance variable?

Both of these violate the single responsibility principle.
The second issue is kinda acceptable, because while the static stuff and the non-static stuff live in a class with the same name, they do not have to share anything. Except the potential leak of non-static private variables into the static context - but the class is small enough to see that it is not the case.

And yes, this might be a textbook implementation of the singleton pattern. But then you should get a new textbook.

And see this, in bootstrap.inc:

function drupal_settings_initialize() {
  [..]
  // Initialize Settings.
  new Settings($settings);
}

Calling a constructor here is semantically wrong. Instead, this should be a call to a static method.

Patch will follow.

Comments

damien tournoud’s picture

Settings has zero conceptual reason for being a singleton:

(1) Settings depend on the website (via the configuration path), which depends on the request.
(2) Symfony supports executing multiple requests in the same PHP environment.
(3) As a consequence, there could be different settings in the same PHP environment.

It is only a singleton by laziness, because we still haven't come up with a bootstrap process that makes sense.

donquixote’s picture

We can and should design a new bootstrap process that eliminates the need for a singleton or static access.

But we can also make this stuff suck less, for the time until a real solution lands. We should not invest too much into it, but I think some basic improvement as suggested above could still be a good thing.

donquixote’s picture

Duh.. I think I first want to see stuff like this to get in: #2210565: Docblock and code style improvements in core/includes/bootstrap.inc
This will allow to really find all occurances of "new Settings" or "Settings::getSingleton()", so we can replace the latter with requireInstance().

(btw, about singletons.. #1757536-139: Move settings.php to /settings directory, fold sites.php into settings.php)

donquixote’s picture

Status: Active » Needs review
StatusFileSize
new5.19 KB

Introducing Settings::requireSingleton().

sun’s picture

I agree with the (main) problem statement of this issue:

Settings::get() should blow up with an exception upon too early access.

But the proposed patch doesn't really make sense to me.

In any case, I disagree with @Damien in #1, and here is why:

  1. Yes, Settings indeed depends on the site directory, which depends on the [master] request.
  2. Yes, HttpKernel allows us to execute sub-requests in the same PHP environment.
  3. However, sub-requests are always to be treated internally to the Drupal site environment of the master request. It is not possible to change the site directory within a single request.
  4. The site directory defines the filesystem location from which settings.php is read, and the one and only purpose of Settings is to be a read-only reincarnation of $settings variables from settings.php.
  5. The site directory also defines the list of available extensions. It is not possible to change that list mid-request. Doing so would require to boot a new kernel with a completely new module list.

    In turn, a HttpKernel would have to reboot a new instance of its parent Kernel. I am very confident we do not want to get there; the current DrupalKernel reboot situation upon module installation is painful enough already.

  6. As a consequence, there can only be one Settings instance in the entire lifetime of a PHP request, regardless of how many sub-requests may be issued. The Settings singleton represents the settings.php of the master request. Only the request changes, no other parts of Drupal's kernel are rebooted.

andypost’s picture

marvil07’s picture

Title: Settings::getSingleton() should throw an exception if called before being initialized. » Settings::getInstance() should throw an exception if called before being initialized
Status: Needs review » Needs work
Issue tags: -singleton, -settings +Novice

I do not see why we need a wrapper, instead of doing it directly on the method.

In any case \Drupal\Component\Utility\Settings is now \Drupal\Core\Site\Settings.

Proposed resolution: do it directly on the method (BTWgetSingleton() is now getInstance())

vlad.n’s picture

Added exception directly in the methods. Also removed global variables that were not used.

vlad.n’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 8: D8-2210521-8-Settings-requireSingleton.patch, failed testing.

vlad.n’s picture

StatusFileSize
new838 bytes

Using exception only on getInstance.

vlad.n’s picture

Status: Needs work » Needs review
dawehner’s picture

+1 for doing that, though we should also have a test

This improves developer experience a bit

dawehner’s picture

Status: Needs review » Needs work

Needs work as test are needed still

mitrpaka’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests +drupalcampfi
StatusFileSize
new1.82 KB
new865 bytes

Simple unit test cases added.

lauriii’s picture

Status: Needs review » Needs work

Thanks for your work!

  1. +++ b/core/lib/Drupal/Core/Site/Settings.php
    @@ -48,9 +48,13 @@ public function __construct(array $settings) {
    +   * @throws \BadMethodCallException
        * @return \Drupal\Core\Site\Settings
    

    @thows should be after @return. There should be also additional line break between them.

  2. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsGetInstanceTest.php
    @@ -0,0 +1,36 @@
    +class SettingsGetInstanceTest extends UnitTestCase {
    

    Should we also test the scenario where the exception is being thrown?

mitrpaka’s picture

Status: Needs work » Needs review
StatusFileSize
new1.75 KB
new0 bytes

Updated patch.
1) @thows should be after @return. There should be also additional line break between them.

Changed.

2) Should we also test the scenario where the exception is being thrown?

Actually, testGetInstanceWithMissingInstance() test case is testing exception thrown.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

Sorry I was blind and didn't see that :) RTBC for me!

dawehner’s picture

Status: Reviewed & tested by the community » Needs work

IMHO we should extend the existing test in \Drupal\Tests\Core\Site\SettingsTest

mitrpaka’s picture

@dawehner Settings class is singleton and settings are initialized in SettingsTest::SetUp() - How would you test exception thrown then?

dawehner’s picture

@dawehner Settings class is singleton and settings are initialized in SettingsTest::SetUp() - How would you test exception thrown then?

You could unset it first, can't you, at least using reflection :)

Ema.jar’s picture

Issue tags: +SprintWeekend2016

I'm working on this issue for the SpringWeekend2016 in Milan with fusillicode!!

Ema.jar’s picture

Assigned: Unassigned » Ema.jar
fusillicode’s picture

Assigned: Ema.jar » fusillicode

Working on it with Ema.jar directly from Milan SprintWeekend2016 ;)

Ema.jar’s picture

Status: Needs work » Needs review
StatusFileSize
new1.63 KB
new1.88 KB

We've created a test based on reflection, as dawehner said, and added it in \Drupal\Tests\Core\Site\SettingsTest

imiksu’s picture

Issue tags: -drupalcampfi

Cleaning up drupalcampfi tags.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Great DX improvement in situations like unit tests

alexpott’s picture

Version: 8.0.x-dev » 8.2.x-dev

8.1.x has entered the RC phase and 8.0.x has entered it's last RC. As silly as it might seem something mught be relying on the NULL behaviour - however there is a DX improvement here. Therefore we should move this to 8.2.x. It is also only a task,

dawehner’s picture

The alternative would be to trigger and error and maybe return a non singleton instance?

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Site/Settings.php
    @@ -49,8 +49,13 @@ public function __construct(array $settings) {
    +   * @throws \BadMethodCallException
    

    An @throws should document under what circumstances the exception is thrown.

  2. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
    @@ -131,4 +131,19 @@ public function testGetApcuPrefix() {
    +    $settings = new Settings(array());
    

    So this will change the singleton for all other tests - doesn't break anything atm and if it did the other tests would be written wrong.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

klausi’s picture

@throws does not need to have a comment if the exception is that obvious, see https://www.drupal.org/docs/develop/coding-standards/api-documentation-a... .

  1. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
    @@ -131,4 +131,19 @@ public function testGetApcuPrefix() {
    +   * @expectedException \BadMethodCallException
    

    Testing best practices say that the exception annotation should not be used and we should call $this->setExpectedException() right before the last line in the test instead.

  2. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
    @@ -131,4 +131,19 @@ public function testGetApcuPrefix() {
    +    $class = new \ReflectionClass('Drupal\Core\Site\Settings');
    

    this should use the Settings::class instead of the class name in a string.

  3. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
    @@ -131,4 +131,19 @@ public function testGetApcuPrefix() {
    +    $singleton = $settings->getInstance();
    

    $singleton is never used and can be removed.

mitrpaka’s picture

Assigned: fusillicode » Unassigned
Status: Needs work » Needs review
Issue tags: -SprintWeekend2016
StatusFileSize
new947 bytes
new1.48 KB

Updated patch based on review comments by @klausi. Thanks for the review and guidance.

mitrpaka’s picture

StatusFileSize
new1.67 KB
new904 bytes

Correct patch file attached.

The last submitted patch, 33: settings_getinstance-2210521-33.patch, failed testing.

dawehner’s picture

+++ b/core/lib/Drupal/Core/Site/Settings.php
@@ -44,8 +44,13 @@ public function __construct(array $settings) {
+      throw new \BadMethodCallException('Settings::$instance is not initialized yet.');

What about telling people what they should do instead? So probably call new Settings or Settings::initialize?

klausi’s picture

Status: Needs review » Needs work

Agreed, so the message could be 'Settings::$instance is not initialized yet. Call Settings::initialize() first.'

+++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
@@ -126,4 +126,21 @@ public function testGetApcuPrefix() {
+    $this->setExpectedException('BadMethodCallException');

this should use \BadMethodCallException::class and not as string.

mitrpaka’s picture

Status: Needs work » Needs review
StatusFileSize
new1.79 KB
new1.17 KB

Thanks for the reviews and comments. Patch updated.

donquixote’s picture

What about telling people what they should do instead? So probably call new Settings or Settings::initialize?

Agreed, so the message could be 'Settings::$instance is not initialized yet. Call Settings::initialize() first.'

Is this good advice? Do we want people / contrib modules to call Settings::initialize(), or should this be the job of some dedicated snippet in core?

If a contrib / custom developer runs into this problem, likely this developer did something too early in the request.

So, "Whatever you are trying to do, it might be too early for that. You could call Settings::initialize(), but it is probably better to wait until it is called in the regular way. Also check for recursions.".

Maybe we could help with the recursion check?

klausi’s picture

Status: Needs review » Needs work

Well, if people run into this problem then they are trying to access settings too early, probably outside of the usual Drupal request flow. The only solution to that is to initialize the settings if they really need settings. They probably cannot wait for later?

  1. +++ b/core/lib/Drupal/Core/Site/Settings.php
    @@ -44,8 +44,14 @@ public function __construct(array $settings) {
    +   * @throws \BadMethodCallException
    +   *  Thrown when the settings instance called before being initialized.
    

    That sentence is a bit broken, did you mean "Thrown when the settings instance has not been initialized yet."?

  2. +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
    @@ -126,4 +126,21 @@ public function testGetApcuPrefix() {
    +  /**
    +   * Tests Settings::testGetInstanceReflection().
    +   *
    

    That method does not exist on the Settings class. Should be "Tests that an excpetion is thrown when settings are not initialized yet."

mitrpaka’s picture

Status: Needs work » Needs review
StatusFileSize
new1.81 KB
new947 bytes

Updated patch.

klausi’s picture

Status: Needs review » Reviewed & tested by the community

Looks good!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Site/Settings.php
@@ -44,8 +44,14 @@ public function __construct(array $settings) {
+      throw new \BadMethodCallException('Settings::$instance is not initialized yet. Call Settings::initialize() first.');

I think that rather than telling them what to do an explanation of why this occurs might be more useful. I'm sympathetic to @donquixote's point that just telling a developer to call initialize() without also making them aware of why might lead to blindly calling Settings::initialize() without considering your code's architecture.

mitrpaka’s picture

Status: Needs work » Needs review
StatusFileSize
new1.93 KB
new696 bytes

Patch updated.

klausi’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Site/Settings.php
@@ -50,7 +50,7 @@
+      throw new \BadMethodCallException('Whatever you are trying to do, it might be too early for that. You could call Settings::initialize(), but it is probably better to wait until it is called in the regular way. Also check for recursions.');

I think we should keep the first sentence "Settings::$instance is not initialized yet." and then add the rest. We should mention the reason why we have to throw the BadMethodCallException.

mitrpaka’s picture

Status: Needs work » Needs review
StatusFileSize
new1.97 KB
new863 bytes
klausi’s picture

Status: Needs review » Reviewed & tested by the community

Looks good, thanks!

  • catch committed e4893d4 on 8.3.x
    Issue #2210521 by mitrpaka, vlad.n, Ema.jar, donquixote, dawehner,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Tried to think of a better comment because the current one seems a bit cryptic, but that's because hitting the error in the first place is strange and couldn't improve on it.

Committed/pushed to 8.3.x, thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.