Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
W
webbasics
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Taddeüs Kroes
webbasics
Commits
62641b42
Commit
62641b42
authored
Oct 09, 2012
by
Taddeus Kroes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Simplified Autoloader class into an easy-to-use singleton
parent
c7d4d826
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
192 additions
and
243 deletions
+192
-243
autoloader.php
autoloader.php
+120
-130
base.php
base.php
+1
-1
tests/test_autoloader.php
tests/test_autoloader.php
+71
-112
No files found.
autoloader.php
View file @
62641b42
...
...
@@ -15,163 +15,138 @@ require_once 'base.php';
*
* Simple example: all classes are located in the 'classes' directory.
* <code>
* $loader = new Autoloader('classes');
* $loader->loadClass('FooBar'); // Includes file 'classes/foo_bar.php'
* $loader = webbasics\Autoloader::getInstance();
* $loader->addDirectory('classes'); // Add "classes" directory to global class path
* $loader->loadClass('FooBar'); // Includes file "classes/FooBar.php"
* </code>
*
*
An Autoloader instance can register
itself to the SPL autoload stack, so
*
The Autoloader instance registers
itself to the SPL autoload stack, so
* that explicit 'include' statements for classes are not necessary anymore.
*
Applied to the example above
:
*
Therefore, the call to loadClass() in the example above is not necessary
:
* <code>
* $loader = new Autoloader('classes');
* $loader->register();
* $foobar = new FooBar(); // File 'classes/foo_bar.php' is automatically included
* webbasics\Autoloader::getInstance()->addDirectory('classes');
* $foobar = new FooBar(); // File "classes/FooBar.php" is automatically included
* </code>
*
* Namespaces are assumed to indicate subdirectories:
* <code>
*
Autoloader::create('classes')->register(
);
* $bar = new
Foo\Bar(); // Includes 'classes/foo/bar.php'
* $baz = new
Foo\Bar\Baz(); // Includes 'classes/foo/bar/baz.php'
*
webbasics\Autoloader::getInstance()->addDirectory('classes'
);
* $bar = new
foo\Bar(); // Includes "classes/foo/Bar.php"
* $baz = new
foo\bar\Baz(); // Includes "classes/foo/bar/Baz.php"
* </code>
*
* Multiple autoloaders can be registered at the same time:
* To load classes within some namespace efficiently, directories can be
* assigned to a root namespace:
* <code>
*
<code>
*
File structure:
*
classes/
*
| foo.php // Contains class 'Foo'
*
other_classes/
*
| bar.php // Contains class 'Bar'
*
$loader = webbasics\Autoloader::getInstance();
*
$loader->addDirectory('models', 'models');
*
*
// File "models/Foo.php"
*
namespace models;
*
class Foo extends ActiveRecord\Model { ... }
* </code>
* Autoloader::create('classes')->register();
* Autoloader::create('other_classes', true)->register();
* $foo = new Foo(); // Includes 'classes/foo.php'
* $bar = new Bar(); // Includes 'other_classes/bar.php', since 'classes/bar.php' does not exist
* $baz = new Baz(); // Throws a FileNotFoundError, since 'other_classes/baz.php' does not exist
*
* Exception throwing can be enabled in case a class does not exist:
* <code>
* $loader = webbasics\Autoloader::getInstance();
* $loader->addDirectory('classes');
* $loader->setThrowExceptions(true);
*
* try {
* new Foo();
* } catch (webbasics\AutoloadError $e) {
* // "classes/Foo.php" does not exist
* }
* </code>
*
* @package WebBasics
*/
class
Autoloader
extends
Base
{
class
Autoloader
extends
Base
implements
Singleton
{
/**
* The root directory to look in.
*
* @var string
* Namespaces mapping to lists of directories.
* @var array
*/
private
$
root_directory
;
private
$
directories
=
array
(
'\\'
=>
array
())
;
/**
* The namespace classes in the root directory are expected to be in.
*
* This namespace is removed from the beginning of loaded class names.
*
* @var string
*/
private
$root_namespace
=
'\\'
;
/**
* Whether to throw an exception when a class file does not exist.
*
* Whether to throw an exception if a class file does not exist.
* @var bool
*/
private
$throw_e
rrors
;
private
$throw_e
xceptions
=
false
;
/**
* Create a new Autoloader instance.
*
* @param string $root_directory Root directory of the autoloader.
* @param string $root_namespace Root namespace of classes loaded by the autoloader.
* @param bool $throw Whether to throw an exception when a class file does not exist.
* @see Singleton::$instance
*/
function
__construct
(
$root_directory
,
$root_namespace
=
'\\'
,
$throw
=
false
)
{
$this
->
setRootDirectory
(
$root_directory
);
$this
->
setRootNamespace
(
$root_namespace
);
$this
->
setThrowErrors
(
$throw
);
}
private
static
$instance
;
/**
* Set whether to throw an exception when a class file does not exist.
*
* @param bool $throw Whether to throw exceptions.
* @see Singleton::getInstance()
*/
function
setThrowErrors
(
$throw
)
{
$this
->
throw_errors
=
!!
$throw
;
static
function
getInstance
()
{
if
(
self
::
$instance
===
null
)
self
::
$instance
=
new
self
;
return
self
::
$instance
;
}
/**
*
Whether an exception is thrown when a class file does not exist
.
*
Create a new Autoloader instance
.
*
*
@return bool
*
Registers the {@link loadClass()} function to the SPL autoload stack.
*/
function
getThrowErrors
()
{
return
$this
->
throw_errors
;
private
function
__construct
()
{
spl_autoload_register
(
array
(
$this
,
'loadClass'
),
true
)
;
}
/**
* Set
the root directory from which classes are loaded
.
* Set
whether to throw an exception when a class file does not exist
.
*
* @param
string $directory The new root directory
.
* @param
bool $throw Whether to throw exceptions
.
*/
function
set
RootDirectory
(
$directory
)
{
$this
->
root_directory
=
self
::
pathWithSlash
(
$directory
)
;
function
set
ThrowExceptions
(
$throw
)
{
$this
->
throw_exceptions
=
(
bool
)
$throw
;
}
/**
*
Get the root directory from which classes are loaded
.
*
Whether an exception is thrown if a class file does not exist
.
*
* @return
string
* @return
bool
*/
function
get
RootDirectory
()
{
return
$this
->
root_directory
;
function
get
ThrowExceptions
()
{
return
$this
->
throw_exceptions
;
}
/**
* Set the root namespace that loaded classes are expected to be in.
*
* @param string $namespace The new root namespace.
* Add a new directory to look in while looking for a class within the given namespace.
*/
function
setRootNamespace
(
$namespace
)
{
// Assert that the namespace ends with a backslash
if
(
$namespace
[
strlen
(
$namespace
)
-
1
]
!=
'\\'
)
$namespace
.=
'\\'
;
function
addDirectory
(
$directory
,
$namespace
=
'\\'
)
{
$directory
=
self
::
pathWithSlash
(
$directory
);
$this
->
root_namespace
=
$namespace
;
}
/**
* Get the root namespace that loaded classes are expected to be in.
*
* @return string
*/
function
getRootNamespace
()
{
return
$this
->
root_namespace
;
}
/**
* Convert a class name to a file name.
*
* Uppercase letters are converted to lowercase and prepended
* by an underscore ('_').
*
* @param string $classname The class name to convert.
* @return string
*/
static
function
classnameToFilename
(
$classname
)
{
return
strtolower
(
preg_replace
(
'/(?<=.)([A-Z])/'
,
'_\\1'
,
$classname
));
if
(
$namespace
[
0
]
!=
'\\'
)
$namespace
=
'\\'
.
$namespace
;
if
(
!
isset
(
$this
->
directories
[
$namespace
]))
$this
->
directories
[
$namespace
]
=
array
();
if
(
!
in_array
(
$directory
,
$this
->
directories
[
$namespace
]))
$this
->
directories
[
$namespace
][]
=
$directory
;
}
/**
* Strip
the root
namespace from the beginning of a class name.
* Strip
a
namespace from the beginning of a class name.
*
* @param string $namespace The namespace to strip.
* @param string $classname The name of the class to strip the namespace from.
* @return string The stripped class name.
*/
private
function
stripRootNamespace
(
$classname
)
{
$begin
=
substr
(
$classname
,
0
,
strlen
(
$this
->
root_namespace
));
private
static
function
stripNamespace
(
$namespace
,
$classname
)
{
if
(
$namespace
!=
'\\'
)
$namespace
.=
'\\'
;
$begin
=
substr
(
$classname
,
0
,
strlen
(
$namespace
));
if
(
$begin
==
$
this
->
root_
namespace
)
$classname
=
substr
(
$classname
,
strlen
(
$
this
->
root_
namespace
));
if
(
$begin
==
$namespace
)
$classname
=
substr
(
$classname
,
strlen
(
$namespace
));
return
$classname
;
}
...
...
@@ -184,16 +159,14 @@ class Autoloader extends Base {
*
* @param string $classname The name of the class to create the file path of.
*/
function
createPath
(
$classname
)
{
$namespaces
=
array_filter
(
explode
(
'\\'
,
$classname
));
$dirs
=
array_map
(
'self::classnameToFilename'
,
$namespaces
);
$path
=
$this
->
root_directory
;
private
static
function
createPath
(
$classname
)
{
$parts
=
array_filter
(
explode
(
'\\'
,
$classname
));
$path
=
''
;
if
(
count
(
$
dir
s
)
>
1
)
$path
.=
implode
(
'/'
,
array_slice
(
$
dirs
,
0
,
count
(
$dirs
)
-
1
))
.
'/'
;
if
(
count
(
$
part
s
)
>
1
)
$path
.=
implode
(
'/'
,
array_slice
(
$
parts
,
0
,
count
(
$parts
)
-
1
))
.
'/'
;
$path
.=
end
(
$dirs
)
.
'.php'
;
return
strtolower
(
$path
);
return
$path
.
end
(
$parts
)
.
'.php'
;
}
/**
...
...
@@ -203,33 +176,50 @@ class Autoloader extends Base {
* namespace levels are used to indicate directory names.
*
* @param string $classname The name of the class to load, including pepended namespace.
* @
param bool $throw Whether to throw an exception if the class file does not exist
.
* @
return bool
* @t
hrows FileNotFoundError If the class file does not exist.
* @
return bool Whether the class file could be found
.
* @
throws ClassNotFoundError If the class file does not exist.
* @t
odo Unit test reverse-order namespace traversal
*/
function
loadClass
(
$classname
,
$throw
=
null
)
{
$classname
=
$this
->
stripRootNamespace
(
$classname
);
$path
=
$this
->
createPath
(
$classname
);
function
loadClass
(
$classname
)
{
// Prepend at least the root namespace
if
(
$classname
[
0
]
!=
'\\'
)
$classname
=
'\\'
.
$classname
;
if
(
!
file_exists
(
$path
))
{
if
(
$throw
||
(
$throw
===
null
&&
$this
->
throw_errors
))
throw
new
FileNotFoundError
(
$path
);
// Find namespace directory
$parts
=
array_filter
(
explode
(
'\\'
,
$classname
));
// Try larger namespaces first, getting smaller and smaller up to the global namespace
for
(
$i
=
count
(
$parts
);
$i
>=
0
;
$i
--
)
{
$namespace
=
'\\'
.
implode
(
'\\'
,
array_slice
(
$parts
,
0
,
$i
));
return
false
;
// If the namespace is mapped to a list of directories, attempt to
// load the class file from there
if
(
isset
(
$this
->
directories
[
$namespace
]))
{
foreach
(
$this
->
directories
[
$namespace
]
as
$directory
)
{
$class
=
self
::
stripNamespace
(
$namespace
,
$classname
);
$path
=
$directory
.
self
::
createPath
(
$class
);
if
(
file_exists
(
$path
))
{
require_once
$path
;
return
true
;
}
}
}
}
require_once
$path
;
return
true
;
if
(
$this
->
throw_exceptions
)
throw
new
ClassNotFoundError
(
$classname
);
return
false
;
}
/**
* Register the autoloader object to the SPL autoload stack.
*
* @param bool $prepend Whether to prepend the autoloader function to
* the stack, instead of appending it.
*/
function
register
(
$prepend
=
false
)
{
spl_autoload_register
(
array
(
$this
,
'loadClass'
),
true
,
$prepend
);
}
/**
* Exception, thrown when a class file could not be found.
*/
class
ClassNotFoundError
extends
FormattedException
{
function
__construct
(
$classname
)
{
parent
::
__construct
(
'could not load class "%s"'
,
$classname
);
}
}
...
...
base.php
View file @
62641b42
...
...
@@ -63,7 +63,7 @@ abstract class Base {
* @return string
*/
static
function
pathWithSlash
(
$directory
)
{
return
$directory
[
strlen
(
$directory
)
-
1
]
==
'/'
?
$directory
:
$directory
.
'/'
;
return
$directory
[
strlen
(
$directory
)
-
1
]
==
'/'
?
$directory
:
$directory
.
'/'
;
}
}
...
...
tests/test_autoloader.php
View file @
62641b42
<?php
require_once
'SingletonTestCase.php'
;
require_once
'autoloader.php'
;
use
webbasics\Autoloader
;
define
(
'PATH'
,
'tests/_files/'
);
class
AutoloaderTest
extends
PHPUnit_Framework_
TestCase
{
function
setUp
()
{
$this
->
autoloader
=
new
Autoloader
(
PATH
)
;
class
AutoloaderTest
extends
Singleton
TestCase
{
function
getClassName
()
{
return
'webbasics\Autoloader'
;
}
function
testSetRootNamespace
()
{
$this
->
assertAttributeEquals
(
'\\'
,
'root_namespace'
,
$this
->
autoloader
);
$this
->
autoloader
->
setRootNamespace
(
'Foo'
);
$this
->
assertAttributeEquals
(
'Foo\\'
,
'root_namespace'
,
$this
->
autoloader
);
$this
->
autoloader
->
setRootNamespace
(
'Foo\\'
);
$this
->
assertAttributeEquals
(
'Foo\\'
,
'root_namespace'
,
$this
->
autoloader
);
function
tearDown
()
{
Autoloader
::
getInstance
()
->
setThrowExceptions
(
false
);
}
/**
* @depends testSetRootNamespace
*/
function
testGetRootNamespace
()
{
$this
->
autoloader
->
setRootNamespace
(
'Foo'
);
$this
->
assertEquals
(
$this
->
autoloader
->
getRootNamespace
(),
'Foo\\'
);
}
/**
* @depends testSetRootNamespace
*/
function
testConstructRootNamespace
()
{
$autoloader
=
new
Autoloader
(
PATH
,
'Foo'
);
$this
->
assertAttributeEquals
(
'Foo\\'
,
'root_namespace'
,
$autoloader
);
function
testStripNamespace
()
{
$rmethod
=
new
ReflectionMethod
(
'webbasics\Autoloader'
,
'stripNamespace'
);
$rmethod
->
setAccessible
(
true
);
$this
->
assertEquals
(
'Bar'
,
$rmethod
->
invoke
(
null
,
'foo'
,
'foo\Bar'
));
$this
->
assertEquals
(
'Bar'
,
$rmethod
->
invoke
(
null
,
'\foo'
,
'\foo\Bar'
));
}
/**
* @depends testSetRootNamespace
*/
function
testStripRootNamespace
()
{
$strip
=
new
ReflectionMethod
(
'webbasics\Autoloader'
,
'stripRootNamespace'
);
$strip
->
setAccessible
(
true
);
function
testAddDirectory
()
{
$autoloader
=
Autoloader
::
getInstance
();
$rprop
=
new
ReflectionProperty
(
$autoloader
,
'directories'
);
$rprop
->
setAccessible
(
true
);
$this
->
autoloader
->
setRootNamespace
(
'Foo'
);
$this
->
assertEquals
(
$strip
->
invoke
(
$this
->
autoloader
,
'Foo\Bar'
),
'Bar'
);
}
function
testSetRootDirectory
()
{
$this
->
autoloader
->
setRootDirectory
(
'tests'
);
$this
->
assertEquals
(
$this
->
autoloader
->
getRootDirectory
(),
'tests/'
);
}
function
testClassnameToFilename
()
{
$this
->
assertEquals
(
Autoloader
::
classnameToFilename
(
'Foo'
),
'foo'
);
$this
->
assertEquals
(
Autoloader
::
classnameToFilename
(
'FooBar'
),
'foo_bar'
);
$this
->
assertEquals
(
Autoloader
::
classnameToFilename
(
'fooBar'
),
'foo_bar'
);
$this
->
assertEquals
(
Autoloader
::
classnameToFilename
(
'FooBarBaz'
),
'foo_bar_baz'
);
$autoloader
->
addDirectory
(
PATH
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
)
),
$rprop
->
getValue
(
$autoloader
));
$autoloader
->
addDirectory
(
PATH
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
)
),
$rprop
->
getValue
(
$autoloader
));
$autoloader
->
addDirectory
(
'foo'
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
,
'foo/'
)
),
$rprop
->
getValue
(
$autoloader
));
$autoloader
->
addDirectory
(
'bar'
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
,
'foo/'
,
'bar/'
)
),
$rprop
->
getValue
(
$autoloader
));
$autoloader
->
addDirectory
(
'foodir'
,
'foo'
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
,
'foo/'
,
'bar/'
),
'\foo'
=>
array
(
'foodir/'
)
),
$rprop
->
getValue
(
$autoloader
));
$autoloader
->
addDirectory
(
'foobardir'
,
'foobar'
);
$this
->
assertEquals
(
array
(
'\\'
=>
array
(
PATH
,
'foo/'
,
'bar/'
),
'\foo'
=>
array
(
'foodir/'
),
'\foobar'
=>
array
(
'foobardir/'
)
),
$rprop
->
getValue
(
$autoloader
));
}
/**
* @depends testClassnameToFilename
*/
function
testCreatePath
()
{
$this
->
assertEquals
(
$this
->
autoloader
->
createPath
(
'Foo'
),
PATH
.
'foo.php'
);
$this
->
assertEquals
(
$this
->
autoloader
->
createPath
(
'\Foo'
),
PATH
.
'foo.php'
);
$this
->
assertEquals
(
$this
->
autoloader
->
createPath
(
'Foo\Bar'
),
PATH
.
'foo/bar.php'
);
$this
->
assertEquals
(
$this
->
autoloader
->
createPath
(
'Foo\Bar\Baz'
),
PATH
.
'foo/bar/baz.php'
);
$this
->
assertEquals
(
$this
->
autoloader
->
createPath
(
'FooBar\Baz'
),
PATH
.
'foo_bar/baz.php'
);
$rmethod
=
new
ReflectionMethod
(
'webbasics\Autoloader'
,
'createPath'
);
$rmethod
->
setAccessible
(
true
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'Foo'
),
'Foo.php'
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'\Foo'
),
'Foo.php'
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'foo\Bar'
),
'foo/Bar.php'
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'foo\Bar\Baz'
),
'foo/Bar/Baz.php'
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'fooBar\Baz'
),
'fooBar/Baz.php'
);
$this
->
assertEquals
(
$rmethod
->
invoke
(
null
,
'foo_bar\Baz'
),
'foo_bar/Baz.php'
);
}
function
testThrowErrors
()
{
$this
->
assertFalse
(
$this
->
autoloader
->
getThrowErrors
());
$this
->
autoloader
->
setThrowErrors
(
true
);
$this
->
assertTrue
(
$this
->
autoloader
->
getThrowErrors
());
function
testThrowExceptions
()
{
$autoloader
=
Autoloader
::
getInstance
();
$this
->
assertFalse
(
$autoloader
->
getThrowExceptions
());
$autoloader
->
setThrowExceptions
(
true
);
$this
->
assertTrue
(
$autoloader
->
getThrowExceptions
());
}
/**
* @depends testCreatePath
* @depends testThrowE
rror
s
* @depends testThrowE
xception
s
*/
function
testLoadClassNotFound
()
{
$this
->
assertFalse
(
$this
->
autoloader
->
loadClass
(
'foobar'
));
}
/**
* @depends testLoadClassNotFound
* @expectedException webbasics\FileNotFoundError
* @expectedExceptionMessage File "tests/_files/foobar.php" does not exist.
*/
function
testLoadClassNotFoundError
()
{
$this
->
autoloader
->
setThrowErrors
(
true
);
$this
->
autoloader
->
loadClass
(
'foobar'
);
$this
->
assertFalse
(
Autoloader
::
getInstance
()
->
loadClass
(
'foobar'
));
}
/**
* @depends testLoadClassNotFound
* @expectedException webbasics\FileNotFoundError
* @expectedExceptionMessage File "tests/_files/foobar.php" does not exist.
* @expectedException webbasics\ClassNotFoundError
*/
function
testLoadClassNotFoundNoerrorOverwrite
()
{
$this
->
autoloader
->
loadClass
(
'foobar'
,
true
);
function
testLoadClassNotFoundException
()
{
$autoloader
=
Autoloader
::
getInstance
();
$autoloader
->
setThrowExceptions
(
true
);
$autoloader
->
loadClass
(
'foobar'
);
}
/**
* @depends testLoadClassNotFound
*/
function
testLoadClassNotFoundErrorOverwrite
()
{
$this
->
autoloader
->
setThrowErrors
(
true
);
$this
->
assertFalse
(
$this
->
autoloader
->
loadClass
(
'foobar'
,
false
));
}
/**
* @depends testLoadClassNotFound
*/
function
testLoadClass
()
{
$this
->
assertTrue
(
$this
->
autoloader
->
loadClass
(
'Foo'
));
function
testLoadClassSuccess
()
{
$autoloader
=
Autoloader
::
getInstance
();
$this
->
assertTrue
(
$autoloader
->
loadClass
(
'Foo'
));
$this
->
assertTrue
(
class_exists
(
'Foo'
,
false
));
$this
->
assertTrue
(
$
this
->
autoloader
->
loadClass
(
'Foo\Bar'
));
$this
->
assertTrue
(
$autoloader
->
loadClass
(
'Foo\Bar'
));
$this
->
assertTrue
(
class_exists
(
'Foo\Bar'
,
false
));
}
/**
* @depends testLoadClass
* @depends testStripRootNamespace
*/
function
testLoadClassRootNamespace
()
{
$autoloader
=
new
Autoloader
(
PATH
.
'foo'
);
$autoloader
->
setRootNamespace
(
'Foo'
);
$this
->
assertTrue
(
$autoloader
->
loadClass
(
'Bar'
));
$this
->
assertTrue
(
class_exists
(
'Foo\Bar'
,
false
));
}
/**
* @depends testLoadClass
*/
function
testRegister
()
{
$this
->
autoloader
->
register
();
$this
->
assertTrue
(
class_exists
(
'Baz'
));
}
/**
* @depends testRegister
* @depends testThrowErrors
*/
function
testRegisterPrepend
()
{
$second_loader
=
new
Autoloader
(
PATH
.
'second'
);
$this
->
autoloader
->
register
();
$second_loader
->
register
(
true
);
// Prepend so that the second loader attemps to load Bar first
$this
->
assertInstanceOf
(
'Foo'
,
new
FooBaz
());
}
}
?>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment