Completed
Push — master ( a06a10...bbb282 )
by Loz
12:34
created

DatabaseAdmin::autoBuild()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 11
rs 9.4285
c 1
b 1
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A DatabaseAdmin::buildDefaults() 0 8 2
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 30 and the first side effect is on line 19.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace SilverStripe\ORM;
4
5
use Controller;
6
use SapphireTest;
7
use Director;
8
9
10
use SS_ClassLoader;
11
use ClassInfo;
12
use TestOnly;
13
use Deprecation;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\Permission;
16
17
18
// Include the DB class
19
require_once("DB.php");
20
21
/**
22
 * DatabaseAdmin class
23
 *
24
 * Utility functions for administrating the database. These can be accessed
25
 * via URL, e.g. http://www.yourdomain.com/db/build.
26
 *
27
 * @package framework
28
 * @subpackage orm
29
 */
30
class DatabaseAdmin extends Controller {
31
32
	/// SECURITY ///
33
	private static $allowed_actions = array(
34
		'index',
35
		'build',
36
		'cleanup',
37
		'import'
38
	);
39
40
	/**
41
	 * Obsolete classname values that should be remapped in dev/build
42
	 */
43
	private static $classname_value_remapping = [
44
		'Group' => 'SilverStripe\\Security\\Group',
45
		'LoginAttempt' => 'SilverStripe\\Security\\LoginAttempt',
46
		'Member' => 'SilverStripe\\Security\\Member',
47
		'MemberPassword' => 'SilverStripe\\Security\\MemberPassword',
48
		'Permission' => 'SilverStripe\\Security\\Permission',
49
		'PermissionRole' => 'SilverStripe\\Security\\PermissionRole',
50
		'PermissionRoleCode' => 'SilverStripe\\Security\\PermissionRoleCode',
51
		'RememberLoginHash' => 'SilverStripe\\Security\\RememberLoginHash',
52
	];
53
54
	protected function init() {
55
		parent::init();
56
57
		// We allow access to this controller regardless of live-status or ADMIN permission only
58
		// if on CLI or with the database not ready. The latter makes it less errorprone to do an
59
		// initial schema build without requiring a default-admin login.
60
		// Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
61
		$isRunningTests = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
62
		$canAccess = (
63
			Director::isDev()
64
			|| !Security::database_is_ready()
65
			// We need to ensure that DevelopmentAdminTest can simulate permission failures when running
66
			// "dev/tests" from CLI.
67
			|| (Director::is_cli() && !$isRunningTests)
68
			|| Permission::check("ADMIN")
69
		);
70
		if(!$canAccess) {
71
			return Security::permissionFailure($this,
72
				"This page is secured and you need administrator rights to access it. " .
73
				"Enter your credentials below and we will send you right along.");
74
		}
75
	}
76
77
	/**
78
	 * Get the data classes, grouped by their root class
79
	 *
80
	 * @return array Array of data classes, grouped by their root class
81
	 */
82
	public function groupedDataClasses() {
83
		// Get all root data objects
84
		$allClasses = get_declared_classes();
85
		foreach($allClasses as $class) {
86
			if(get_parent_class($class) == 'SilverStripe\ORM\DataObject')
87
				$rootClasses[$class] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$rootClasses was never initialized. Although not strictly required by PHP, it is generally a good practice to add $rootClasses = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
88
		}
89
90
		// Assign every other data object one of those
91
		foreach($allClasses as $class) {
92
			if(!isset($rootClasses[$class]) && is_subclass_of($class, 'SilverStripe\ORM\DataObject')) {
93
				foreach($rootClasses as $rootClass => $dummy) {
94
					if(is_subclass_of($class, $rootClass)) {
95
						$rootClasses[$rootClass][] = $class;
0 ignored issues
show
Bug introduced by
The variable $rootClasses does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
96
						break;
97
					}
98
				}
99
			}
100
		}
101
		return $rootClasses;
102
	}
103
104
105
	/**
106
	 * When we're called as /dev/build, that's actually the index. Do the same
107
	 * as /dev/build/build.
108
	 */
109
	public function index() {
110
		return $this->build();
111
	}
112
113
	/**
114
	 * Updates the database schema, creating tables & fields as necessary.
115
	 */
116
	public function build() {
117
		// The default time limit of 30 seconds is normally not enough
118
		increase_time_limit_to(600);
119
120
		// Get all our classes
121
		SS_ClassLoader::instance()->getManifest()->regenerate();
122
123
		$url = $this->getReturnURL();
124
		if($url) {
125
			echo "<p>Setting up the database; you will be returned to your site shortly....</p>";
126
			$this->doBuild(true);
127
			echo "<p>Done!</p>";
128
			$this->redirect($url);
129
		} else {
130
			$quiet = $this->request->requestVar('quiet') !== null;
131
			$fromInstaller = $this->request->requestVar('from_installer') !== null;
132
			$populate = $this->request->requestVar('dont_populate') === null;
133
			$this->doBuild($quiet || $fromInstaller, $populate);
134
		}
135
	}
136
137
	/**
138
	 * Gets the url to return to after build
139
	 *
140
	 * @return string|null
141
	 */
142
	protected function getReturnURL() {
143
		$url = $this->request->getVar('returnURL');
144
145
		// Check that this url is a site url
146
		if(empty($url) || !Director::is_site_url($url)) {
147
			return null;
148
		}
149
150
		// Convert to absolute URL
151
		return Director::absoluteURL($url, true);
152
	}
153
154
	/**
155
	 * Build the default data, calling requireDefaultRecords on all
156
	 * DataObject classes
157
	 */
158
	public function buildDefaults() {
159
		$dataClasses = ClassInfo::subclassesFor('SilverStripe\ORM\DataObject');
160
		array_shift($dataClasses);
161
		foreach($dataClasses as $dataClass) {
162
			singleton($dataClass)->requireDefaultRecords();
163
			print "Defaults loaded for $dataClass<br/>";
164
		}
165
	}
166
167
	/**
168
	 * Returns the timestamp of the time that the database was last built
169
	 *
170
	 * @return string Returns the timestamp of the time that the database was
171
	 *                last built
172
	 */
173
	public static function lastBuilt() {
174
		$file = TEMP_FOLDER . '/database-last-generated-' .
175
			str_replace(array('\\','/',':'), '.' , Director::baseFolder());
176
177
		if(file_exists($file)) {
178
			return filemtime($file);
179
		}
180
	}
181
182
183
	/**
184
	 * Updates the database schema, creating tables & fields as necessary.
185
	 *
186
	 * @param boolean $quiet Don't show messages
187
	 * @param boolean $populate Populate the database, as well as setting up its schema
188
	 * @param bool $testMode
189
	 */
190
	public function doBuild($quiet = false, $populate = true, $testMode = false) {
0 ignored issues
show
Coding Style introduced by
doBuild uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
191
		if($quiet) {
192
			DB::quiet();
193
		} else {
194
			$conn = DB::get_conn();
195
			// Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database")
196
			$dbType = substr(get_class($conn), 0, -8);
197
			$dbVersion = $conn->getVersion();
198
			$databaseName = (method_exists($conn, 'currentDatabase')) ? $conn->getSelectedDatabase() : "";
199
200
			if(Director::is_cli()) {
201
				echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion);
202
			} else {
203
				echo sprintf("<h2>Building database %s using %s %s</h2>", $databaseName, $dbType, $dbVersion);
204
			}
205
		}
206
207
		// Set up the initial database
208
		if(!DB::is_active()) {
209
			if(!$quiet) {
210
				echo '<p><b>Creating database</b></p>';
211
			}
212
213
			// Load parameters from existing configuration
214
			global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
215
			if(empty($databaseConfig) && empty($_REQUEST['db'])) {
216
				user_error("No database configuration available", E_USER_ERROR);
217
			}
218
			$parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
219
220
			// Check database name is given
221
			if(empty($parameters['database'])) {
222
				user_error("No database name given; please give a value for \$databaseConfig['database']",
223
							E_USER_ERROR);
224
			}
225
			$database = $parameters['database'];
226
227
			// Establish connection and create database in two steps
228
			unset($parameters['database']);
229
			DB::connect($parameters);
230
			DB::create_database($database);
231
		}
232
233
		// Build the database.  Most of the hard work is handled by DataObject
234
		$dataClasses = ClassInfo::subclassesFor('SilverStripe\ORM\DataObject');
235
		array_shift($dataClasses);
236
237
		if(!$quiet) {
238
			if(Director::is_cli()) echo "\nCREATING DATABASE TABLES\n\n";
239
			else echo "\n<p><b>Creating database tables</b></p>\n\n";
240
		}
241
242
		// Initiate schema update
243
		$dbSchema = DB::get_schema();
244
		$dbSchema->schemaUpdate(function() use($dataClasses, $testMode, $quiet){
245
			foreach($dataClasses as $dataClass) {
246
				// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
247
				if(!class_exists($dataClass)) continue;
248
249
				// Check if this class should be excluded as per testing conventions
250
				$SNG = singleton($dataClass);
251
				if(!$testMode && $SNG instanceof TestOnly) continue;
252
253
				// Log data
254
				if(!$quiet) {
255
					if(Director::is_cli()) echo " * $dataClass\n";
256
					else echo "<li>$dataClass</li>\n";
257
				}
258
259
				// Instruct the class to apply its schema to the database
260
				$SNG->requireTable();
261
			}
262
		});
263
		ClassInfo::reset_db_cache();
264
265
		if($populate) {
266
			if(!$quiet) {
267
				if(Director::is_cli()) echo "\nCREATING DATABASE RECORDS\n\n";
268
				else echo "\n<p><b>Creating database records</b></p>\n\n";
269
			}
270
271
			foreach($dataClasses as $dataClass) {
272
				// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
273
				// Test_ indicates that it's the data class is part of testing system
274
				if(strpos($dataClass,'Test_') === false && class_exists($dataClass)) {
275
					if(!$quiet) {
276
						if(Director::is_cli()) echo " * $dataClass\n";
277
						else echo "<li>$dataClass</li>\n";
278
					}
279
280
					singleton($dataClass)->requireDefaultRecords();
281
				}
282
			}
283
284
			// Remap obsolete class names
285
			$schema = DataObject::getSchema();
286
			foreach ($this->config()->classname_value_remapping as $oldClassName => $newClassName) {
287
				$badRecordCount = $newClassName::get()->filter(["ClassName" => $oldClassName ])->count();
288
				if($badRecordCount > 0) {
289
					if(Director::is_cli()) echo " * Correcting $badRecordCount obsolete classname values for $newClassName\n";
290
					else echo "<li>Correcting $badRecordCount obsolete classname values for $newClassName</li>\n";
291
					$table = $schema->baseDataTable($newClassName);
292
					DB::prepared_query("UPDATE \"$table\" SET \"ClassName\" = ? WHERE \"ClassName\" = ?", [ $newClassName, $oldClassName ]);
293
				}
294
			}
295
296
		}
297
298
		touch(TEMP_FOLDER
299
			. '/database-last-generated-'
300
			. str_replace(array('\\', '/', ':'), '.', Director::baseFolder())
301
		);
302
303
		if(isset($_REQUEST['from_installer'])) {
304
			echo "OK";
305
		}
306
307
		if(!$quiet) {
308
			echo (Director::is_cli()) ? "\n Database build completed!\n\n" :"<p>Database build completed!</p>";
309
		}
310
311
		ClassInfo::reset_db_cache();
312
	}
313
314
	/**
315
	 * Clear all data out of the database
316
	 *
317
	 * @deprecated since version 4.0
318
	 */
319
	public function clearAllData() {
320
		Deprecation::notice('4.0', 'Use DB::get_conn()->clearAllData() instead');
321
		DB::get_conn()->clearAllData();
322
	}
323
324
	/**
325
	 * Remove invalid records from tables - that is, records that don't have
326
	 * corresponding records in their parent class tables.
327
	 */
328
	public function cleanup() {
329
		$allClasses = get_declared_classes();
330
		foreach($allClasses as $class) {
331
			if(get_parent_class($class) == 'SilverStripe\ORM\DataObject') {
332
				$baseClasses[] = $class;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$baseClasses was never initialized. Although not strictly required by PHP, it is generally a good practice to add $baseClasses = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
333
			}
334
		}
335
336
		foreach($baseClasses as $baseClass) {
0 ignored issues
show
Bug introduced by
The variable $baseClasses does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
337
			// Get data classes
338
			$subclasses = ClassInfo::subclassesFor($baseClass);
339
			unset($subclasses[0]);
340
			foreach($subclasses as $k => $subclass) {
341
				if(DataObject::has_own_table($subclass)) {
342
					unset($subclasses[$k]);
343
				}
344
			}
345
346
			if($subclasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
347
				$records = DB::query("SELECT * FROM \"$baseClass\"");
348
349
350
				foreach($subclasses as $subclass) {
351
					$recordExists[$subclass] =
0 ignored issues
show
Coding Style Comprehensibility introduced by
$recordExists was never initialized. Although not strictly required by PHP, it is generally a good practice to add $recordExists = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
352
						DB::query("SELECT \"ID\" FROM \"$subclass\"")->keyedColumn();
353
				}
354
355
				foreach($records as $record) {
356
					foreach($subclasses as $subclass) {
357
						$id = $record['ID'];
358
						if(($record['ClassName'] != $subclass) &&
359
							(!is_subclass_of($record['ClassName'], $subclass)) &&
360
								(isset($recordExists[$subclass][$id]))) {
361
							$sql = "DELETE FROM \"$subclass\" WHERE \"ID\" = $record[ID]";
362
							echo "<li>$sql";
363
							DB::query($sql);
364
						}
365
					}
366
				}
367
			}
368
		}
369
	}
370
371
}
372
373