diff --git a/.gitignore b/.gitignore index c0ad1d9..efd9f46 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ yarn-error.log* *.sln *.sw* /data/stilus.log +/coverage.clover diff --git a/.stilus.yml b/.stilus.yml index 93c28f3..80bd45b 100644 --- a/.stilus.yml +++ b/.stilus.yml @@ -15,6 +15,7 @@ api: log_file: "./data/stilus.log" dashboard: + language: en_EN web_server: host: "0.0.0.0" port: 8090 diff --git a/composer.json b/composer.json index 0c97d2c..8e39062 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,5 @@ { "name": "igniphp/stilus", - "version": "0.0.1", "description": "", "keywords": [], "license": "BSD-3-Clause", @@ -13,22 +12,48 @@ "require": { "php": ">=7.1.0", "igniphp/framework": "^2.0", - "igniphp/storage": "^0.4.2", + "igniphp/storage": "^0.6.0", "igniphp/validation": "^1.1.0", "symfony/yaml": "^4.1", "zendframework/zend-mail": "^2.10", - "zendframework/zend-crypt": "^3.3" + "zendframework/zend-crypt": "^3.3", + "league/flysystem": "^1.0", + "zircote/swagger-php": "^3.0" }, "scripts": { - "start": "php src/api/Stilus.php", - "stop": "kill $(cat ./data/stilus.pid)" + "post-install-cmd": [ + + ], + "post-update-cmd": [], + "migrate": [ + "Stilus\\Kernel\\Migration\\MigrationCommand::synchronize" + ], + "ci": [ + "composer validate --no-check-all --strict", + "@phpcs", + "@test-coverage" + ], + "start": "php src/Stilus.php", + "stop": "kill $(cat ./data/stilus.pid)", + "phpcs": "phpcs --standard=PSR2 src", + "test": "phpunit", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" + }, + "scripts-descriptions": { + "phpcs": "Checks that the application code conforms to coding standard", + "test-coverage": "Launches the preconfigured PHPUnit with coverage", + "ci": "Continues integration checks", + "migrate": "Runs migrations, example usage: composer migration 1.0.0" }, "require-dev": { "phpunit/phpunit": ">=5.7.0", - "mockery/mockery": ">=0.9.4", - "phpunit/php-code-coverage": ">=4.0.0" + "phpunit/php-code-coverage": ">=4.0.0", + "fzaninotto/faker": "^1.8" }, "autoload": { + "exclude-from-classmap": [ + "src/api/Stilus.php" + ], "psr-4": { "Stilus\\": "src/api/" } diff --git a/composer.lock b/composer.lock index 26a09fd..3c22999 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4a32763295390bfef6319ecbfa848c3e", + "content-hash": "29fad14a5be7546f891c0ebf94758a92", "packages": [ { "name": "cache/adapter-common", @@ -843,16 +843,16 @@ }, { "name": "igniphp/storage", - "version": "0.4.2", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/igniphp/storage.git", - "reference": "475b44b9b530a1737f4293b0c3cd3bb9fca99334" + "reference": "dbfa7f0a29a67f02b113efe448346f195180bcf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igniphp/storage/zipball/475b44b9b530a1737f4293b0c3cd3bb9fca99334", - "reference": "475b44b9b530a1737f4293b0c3cd3bb9fca99334", + "url": "https://api.github.com/repos/igniphp/storage/zipball/dbfa7f0a29a67f02b113efe448346f195180bcf8", + "reference": "dbfa7f0a29a67f02b113efe448346f195180bcf8", "shasum": "" }, "require": { @@ -911,7 +911,7 @@ "sqlite", "unit of work" ], - "time": "2018-07-16T07:58:44+00:00" + "time": "2018-09-27T06:35:43+00:00" }, { "name": "igniphp/uuid", @@ -1002,6 +1002,90 @@ ], "time": "2018-03-20T17:20:48+00:00" }, + { + "name": "league/flysystem", + "version": "1.0.47", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "a11e4a75f256bdacf99d20780ce42d3b8272975c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a11e4a75f256bdacf99d20780ce42d3b8272975c", + "reference": "a11e4a75f256bdacf99d20780ce42d3b8272975c", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.10" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2018-09-14T15:30:29+00:00" + }, { "name": "paragonie/random_compat", "version": "v2.0.17", @@ -1449,6 +1533,55 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "symfony/finder", + "version": "v4.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "1f17195b44543017a9c9b2d437c670627e96ad06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/1f17195b44543017a9c9b2d437c670627e96ad06", + "reference": "1f17195b44543017a9c9b2d437c670627e96ad06", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2018-10-03T08:47:56+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.9.0", @@ -2243,6 +2376,69 @@ "zf2" ], "time": "2018-02-01T17:05:33+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "8fc3bc059a7f71b3f100bcfd84a96b5b8fcf6fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/8fc3bc059a7f71b3f100bcfd84a96b5b8fcf6fcf", + "reference": "8fc3bc059a7f71b3f100bcfd84a96b5b8fcf6fcf", + "shasum": "" + }, + "require": { + "doctrine/annotations": "*", + "php": ">=7.0", + "symfony/finder": ">=2.2", + "symfony/yaml": ">=2.8" + }, + "require-dev": { + "phpunit/phpunit": ">=6.3", + "squizlabs/php_codesniffer": ">=3.3", + "zendframework/zend-form": "<2.8" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "autoload": { + "psr-4": { + "OpenApi\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com", + "homepage": "http://www.zircote.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "http://bfanger.nl" + } + ], + "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "time": "2018-09-30T12:19:07+00:00" } ], "packages-dev": [ @@ -2301,118 +2497,54 @@ "time": "2017-07-22T11:58:36+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.0.0", + "name": "fzaninotto/faker", + "version": "v1.8.0", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de", + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de", "shasum": "" }, "require": { - "php": "^5.3|^7.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" - }, - "require-dev": { - "phpunit/php-file-iterator": "1.3.3", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "hamcrest" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "description": "This is the PHP port of Hamcrest Matchers", - "keywords": [ - "test" - ], - "time": "2016-01-20T08:20:44+00:00" - }, - { - "name": "mockery/mockery", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "99e29d3596b16dabe4982548527d5ddf90232e99" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/99e29d3596b16dabe4982548527d5ddf90232e99", - "reference": "99e29d3596b16dabe4982548527d5ddf90232e99", - "shasum": "" - }, - "require": { - "hamcrest/hamcrest-php": "~2.0", - "lib-pcre": ">=7.0", - "php": ">=5.6.0" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "~5.7.10|~6.5" + "ext-intl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7", + "squizlabs/php_codesniffer": "^1.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.8-dev" } }, "autoload": { - "psr-0": { - "Mockery": "library/" + "psr-4": { + "Faker\\": "src/Faker/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "name": "François Zaninotto" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", + "description": "Faker is a PHP library that generates fake data for you.", "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2018-05-08T08:54:48+00:00" + "data", + "faker", + "fixtures" + ], + "time": "2018-07-12T10:23:15+00:00" }, { "name": "myclabs/deep-copy", diff --git a/logo/stilus.svg b/logo/stilus.svg deleted file mode 100644 index edf060d..0000000 --- a/logo/stilus.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - logo - - - - - - diff --git a/phpunit.xml b/phpunit.xml index e7c9331..fc8cc21 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - bootstrap="tests/bootstrap.php" + bootstrap="tests/api/bootstrap.php" > diff --git a/src/api/Exception/BootException.php b/src/api/Exception/BootException.php index 72ff5cb..f662884 100644 --- a/src/api/Exception/BootException.php +++ b/src/api/Exception/BootException.php @@ -8,29 +8,30 @@ class BootException extends RuntimeException { public static function forInvalidPHPVersion(string $currentVersion): self { - return self::withMessage("Stilus requires PHP 7.1.0 or higher, you are running PHP {$currentVersion}."); + return new self("Stilus requires PHP 7.1.0 or higher, you are running PHP {$currentVersion}."); } public static function forMissingComposer(): self { - return self::withMessage('`vendor` dir is missing. Did you forgot to run `composer install?`'); + return new self('`vendor` dir is missing. Did you forgot to run `composer install?`'); } public static function forMissingBaseConfiguration(): self { - return self::withMessage('`.stilus.yml` file is missing. Did you deleted it by accident?'); + return new self('`.stilus.yml` file is missing. Did you deleted it by accident?'); } public static function forInvalidBaseConfiguration(Throwable $previous): self { - return self::withPrevious( + return new self( 'There was a problem with parsing `.stilus.yml` file. Please check the config file.', + $previous->getCode(), $previous ); } public static function forMissingConfigurationOption(string $name): self { - return self::withMessage("`{$name}`` configuration option is missing."); + return new self("`{$name}`` configuration option is missing."); } } diff --git a/src/api/Exception/DomainException.php b/src/api/Exception/DomainException.php new file mode 100644 index 0000000..ab741b5 --- /dev/null +++ b/src/api/Exception/DomainException.php @@ -0,0 +1,7 @@ +getCode(), $previous); - } -} diff --git a/src/api/Exception/RuntimeException.php b/src/api/Exception/RuntimeException.php index 1a9b9ac..3c977d6 100644 --- a/src/api/Exception/RuntimeException.php +++ b/src/api/Exception/RuntimeException.php @@ -2,7 +2,8 @@ namespace Stilus\Exception; -class RuntimeException extends \RuntimeException implements StilusException +use RuntimeException as PhpRuntimeException; + +class RuntimeException extends PhpRuntimeException implements StilusException { - use ExceptionTrait; } diff --git a/src/api/Kernel/Installer.php b/src/api/Kernel/Installer.php new file mode 100644 index 0000000..de16fd5 --- /dev/null +++ b/src/api/Kernel/Installer.php @@ -0,0 +1,25 @@ +createServiceLocator(); + $connection = $system->createDatabaseConnection(); + $container->set(Connection::class, $connection); + + $versionSynchronizer = new VersionSynchronizer($connection); + $migrationManager = new MigrationManager($versionSynchronizer); + $container->set(MigrationManager::class, $migrationManager); + + self::loadModules($container); + $arguments = $event->getArguments(); + + if (isset($arguments[0])) { + $migrationManager->migrate(Version::fromString($arguments[0])); + } else { + $migrationManager->migrate(); + } + } + + private static function loadModules(ServiceLocator $locator): void + { + $modules = []; + + foreach (System::BASE_MODULES as $module) { + if (!class_exists($module)) { + continue; + } + $modules[] = new $module; + } + + foreach ($modules as $module) { + if ($module instanceof ConfigProvider) { + $module->provideConfig($locator->get(Config::class)); + } + } + + foreach ($modules as $module) { + if ($module instanceof ServiceProvider) { + $module->provideServices($locator); + } + } + } +} diff --git a/src/api/Kernel/Migration/VersionSynchronizer.php b/src/api/Kernel/Migration/VersionSynchronizer.php new file mode 100644 index 0000000..46e4251 --- /dev/null +++ b/src/api/Kernel/Migration/VersionSynchronizer.php @@ -0,0 +1,75 @@ +connection = $connection; + + $this->prepareMigrationTable(); + } + + private function prepareMigrationTable(): void + { + $cursor = $this->connection->createCursor( + "SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'" + ); + + $tableExists = $cursor->current(); + + if (!$tableExists) { + $this->createMigrationTable(); + } + } + + private function createMigrationTable(): void + { + $cursor = $this->connection->createCursor( + "CREATE TABLE migrations ( + major INTEGER NOT NULL DEFAULT 0, + minor INTEGER NOT NULL DEFAULT 0, + patch INTEGER NOT NULL DEFAULT 0 + )" + ); + + $cursor->execute(); + } + + public function getVersion(): Version + { + $cursor = $this->connection->createCursor( + 'SELECT major, minor, patch FROM migrations ORDER BY major DESC, minor DESC, patch DESC' + ); + + $current = $cursor->current(); + if ($current === null) { + $current = Version::fromString('0.0.0'); + } else { + $current = Version::fromString(implode('.', $current)); + } + + return $current; + } + + public function setVersion(Version $version): void + { + $cursor = $this->connection->createCursor( + 'INSERT INTO migrations (major, minor, patch) VALUES (:major, :minor, :patch)', + [ + $version->getMajor(), + $version->getMinor(), + $version->getPatch() + ] + ); + + $cursor->execute(); + } +} diff --git a/src/api/Kernel/Module.php b/src/api/Kernel/Module.php new file mode 100644 index 0000000..83e1fe1 --- /dev/null +++ b/src/api/Kernel/Module.php @@ -0,0 +1,10 @@ +')) { + throw BootException::forInvalidPHPVersion(PHP_VERSION); + } + + if (!file_exists(self::VENDOR_AUTOLOADER)) { + throw BootException::forMissingComposer(); + } + + require_once self::VENDOR_AUTOLOADER; + } + + public function createDatabaseConnection(): Connection + { + if (!ConnectionManager::has('default')) { + ConnectionManager::register('default', new Connection('sqlite:' . self::DB_PATH)); + } + + return ConnectionManager::get('default'); + } + + public function getBaseConfig(): Config + { + if ($this->config instanceof Config) { + return $this->config; + } + + $config = $this->loadBaseConfig(); + return $this->config = new Config([ + 'dir.basedir' => System::DIR, + 'dir.config' => realpath(System::DIR . DIRECTORY_SEPARATOR . $config['paths']['config']), + 'dir.database' => realpath(System::DIR . DIRECTORY_SEPARATOR . $config['paths']['database']), + 'dir.themes' => realpath(System::DIR . DIRECTORY_SEPARATOR . $config['paths']['themes']), + ]); + } + + private function loadBaseConfig(): array + { + if (!is_readable(self::BASE_CONFIG)) { + throw BootException::forMissingBaseConfiguration(); + } + + try { + return $configuration = Yaml::parseFile(self::BASE_CONFIG); + } catch (Throwable $throwable) { + throw BootException::forInvalidBaseConfiguration($throwable); + } + } + + public function createServiceLocator(): ServiceLocator + { + if (!$this->container instanceof ContainerInterface) { + $this->container = new ServiceLocator(); + $this->container->set(Config::class, $this->getBaseConfig()); + } + + return $this->container; + } +} diff --git a/src/api/Platform/Controller/CreatePlatform.php b/src/api/Platform/Controller/CreatePlatform.php deleted file mode 100644 index 1ac2d01..0000000 --- a/src/api/Platform/Controller/CreatePlatform.php +++ /dev/null @@ -1,21 +0,0 @@ -platformService = $platformService; } public function __invoke(ServerRequestInterface $request): ResponseInterface diff --git a/src/api/Platform/Controller/InstallPlatform.php b/src/api/Platform/Controller/InstallPlatform.php new file mode 100644 index 0000000..015705e --- /dev/null +++ b/src/api/Platform/Controller/InstallPlatform.php @@ -0,0 +1,48 @@ +platformService = $platformService; + } + + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $this->platformService->install(); + } + + public static function getRoute(): Route + { + return Route::post('/platform'); + } +} diff --git a/src/api/Platform/Exception/PlartformException.php b/src/api/Platform/Exception/PlartformException.php new file mode 100644 index 0000000..f9ed1ce --- /dev/null +++ b/src/api/Platform/Exception/PlartformException.php @@ -0,0 +1,9 @@ +id = new Uuid(); + $this->email = $email; + $this->createPassword($password); + $this->validate(); + } + + public function getId(): Id + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function validatePassword(string $password): bool + { + return password_verify($password, $this->password); + } + + public function createPassword(string $password): void + { + $this->password = password_hash($password, PASSWORD_BCRYPT); + } + + public function changePassword(string $oldPassword, string $newPassword): bool + { + if (!$this->validatePassword($oldPassword)) { + return false; + } + + $this->createPassword($newPassword); + + return true; + } + + private function validate() + { + if (!Constraint::email()->validate($this->email)) { + throw UserException::forCreationFailure(); + } + } +} diff --git a/src/api/Platform/Persistence/UserRepository.php b/src/api/Platform/Persistence/UserRepository.php new file mode 100644 index 0000000..514e165 --- /dev/null +++ b/src/api/Platform/Persistence/UserRepository.php @@ -0,0 +1,33 @@ +query( + 'SELECT * FROM users WHERE email = :email LIMIT 1', + [ + 'email' => $email, + ] + ); + $cursor->hydrateWith($this->hydrator); + $user = $cursor->current(); + $cursor->close(); + + if ($user === null) { + throw UserException::forNotFound(); + } + + return $user; + } + + public static function getEntityClass(): string + { + return User::class; + } +} diff --git a/src/api/Platform/Persistence/UserSchema.php b/src/api/Platform/Persistence/UserSchema.php new file mode 100644 index 0000000..27c4a4e --- /dev/null +++ b/src/api/Platform/Persistence/UserSchema.php @@ -0,0 +1,46 @@ +connection = $connection; + } + + public function up(): void + { + $this->connection + ->createCursor('CREATE TABLE IF NOT EXISTS "users" ( + "id" char(22) PRIMARY KEY NOT NULL, + "email" char(128) NOT NULL, + "password" char(128) NOT NULL + )') + ->execute(); + } + + public function down(): void + { + $this->connection + ->createCursor('DROP TABLE IF EXISTS "users"') + ->execute(); + } + + public function getVersion(): Version + { + return Version::fromString('1.0.0'); + } + + public static function factory(ContainerInterface $container): self + { + return new self($container->get(Connection::class)); + } +} diff --git a/src/api/Platform/PlatformModule.php b/src/api/Platform/PlatformModule.php index 779ee75..2028c7c 100644 --- a/src/api/Platform/PlatformModule.php +++ b/src/api/Platform/PlatformModule.php @@ -7,14 +7,15 @@ use Igni\Application\Providers\ServiceProvider; use Igni\Container\ServiceLocator; use Psr\Container\ContainerInterface; -use Stilus\Platform\Controller\CreatePlatform; +use Stilus\Kernel\Module; +use Stilus\Platform\Controller\InstallPlatform; use Stilus\Platform\Controller\GetPlatformStatus; -class PlatformModule implements ControllerProvider, ServiceProvider +class PlatformModule implements ControllerProvider, ServiceProvider, Module { public function provideControllers(ControllerAggregator $controllers): void { - $controllers->register(CreatePlatform::class); + $controllers->register(InstallPlatform::class); $controllers->register(GetPlatformStatus::class); } @@ -25,4 +26,9 @@ public function provideServices(ContainerInterface $container): void { $container->share(PlatformService::class); } + + public static function install(ContainerInterface $container) + { + + } } diff --git a/src/api/Platform/PlatformService.php b/src/api/Platform/PlatformService.php index 85d83a2..ca8161b 100644 --- a/src/api/Platform/PlatformService.php +++ b/src/api/Platform/PlatformService.php @@ -16,6 +16,27 @@ public function __construct(Config $paths) $this->config = $paths; } + public function setLanguage(): void + { + + } + + public function setupDatabase(): void + { + + } + + public function createAdmin(string $email, string $password): void + { + + } + + public function postInstall(): void + { + + } + + public function getStatus() { diff --git a/src/api/Stilus.php b/src/api/Stilus.php index 8177778..fb54c2b 100644 --- a/src/api/Stilus.php +++ b/src/api/Stilus.php @@ -1,57 +1,41 @@ ')) { - throw BootException::forInvalidPHPVersion(PHP_VERSION); +use Stilus\Kernel\System; +use Igni\Storage\Driver\Connection; +use OpenApi\Annotations as Doc; + +// Composer is autoloading this file even when test are run, this hack stops from +// excecution while tests are runnnig +if (defined('STILUS_TEST')) { + return; } -const STILUS_MODULES = [ - PlatformModule::class -]; +const STILUS_VERSION = "1.0.0"; -const STILUS_DIR = __DIR__ . '/../..'; - -const STILUS_BASE_CONFIG = STILUS_DIR . '/.stilus.yml'; - -const STILUS_VENDOR_DIR = __DIR__ . '/../../vendor'; - -const STILUS_VENDOR_AUTOLOADER = __DIR__ . '/../../vendor/autoload.php'; +/** + * @Doc\Info( + * version=STILUS_VERSION, + * title="Stilus API", + * @Doc\License( + * name="BSD-3-Clause", + * url="https://www.opensource.org/licenses/BSD-3-Clause" + * ) + * ) + */ +$system = new System(); // Bootstrap -(new class { - - private function setupAutoload(): void - { - if (!file_exists(STILUS_VENDOR_AUTOLOADER)) { - throw BootException::forMissingComposer(); - } +(new class($system) { - require STILUS_VENDOR_AUTOLOADER; - } + private $system; - private function loadBootstrapConfig(): array + public function __construct(System $system) { - if (!is_readable(STILUS_BASE_CONFIG)) { - throw BootException::forMissingBaseConfiguration(); - } - - try { - return $configuration = Yaml::parseFile(STILUS_BASE_CONFIG); - } catch (Throwable $throwable) { - throw BootException::forInvalidBaseConfiguration($throwable); - } + $this->system = $system; } private function setupServer(array $config): HttpServer @@ -86,26 +70,22 @@ private function setupServer(array $config): HttpServer public function main(): void { - $this->setupAutoload(); - - $config = $this->loadBootstrapConfig(); - $container = new ServiceLocator(); - $container->share(Config::class, function() use ($config) { - return new Config([ - 'dir.basedir', STILUS_DIR, - 'dir.config' => realpath(STILUS_DIR . DIRECTORY_SEPARATOR . $config['paths']['config']), - 'dir.database', realpath(STILUS_DIR . DIRECTORY_SEPARATOR . $config['paths']['database']), - 'dir.themes', realpath(STILUS_DIR . DIRECTORY_SEPARATOR . $config['paths']['themes']), - ]); - }); - $application = new HttpApplication($container); - - foreach (STILUS_MODULES as $module) { + $config = $this->system->getBaseConfig(); + $connection = $this->system->createDatabaseConnection(); + + $serviceLocator = $this->system->createServiceLocator(); + $serviceLocator->set(Connection::class, $connection); + $application = new HttpApplication($serviceLocator); + + foreach (System::STILUS_MODULES as $module) { $application->extend($module); } $server = null; - if (isset($config['api']) && + + // Server should be only available in sapi mode and when proper config is set + if (php_sapi_name() == "cli" && + isset($config['api']) && isset($config['api']['http_server']) && isset($config['api']['http_server']['enable']) && $config['api']['http_server']['enable'] diff --git a/src/dashboard/i18n/en_EN.json b/src/dashboard/i18n/en_EN.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/src/dashboard/i18n/en_EN.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/tests/api/Fixtures/.gitkeep b/tests/api/Fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/api/Fixtures/test.db b/tests/api/Fixtures/test.db new file mode 100644 index 0000000..8b17d50 Binary files /dev/null and b/tests/api/Fixtures/test.db differ diff --git a/tests/api/Functional/Kernel/Migration/MigrationServiceTest.php b/tests/api/Functional/Kernel/Migration/MigrationServiceTest.php new file mode 100644 index 0000000..440a30b --- /dev/null +++ b/tests/api/Functional/Kernel/Migration/MigrationServiceTest.php @@ -0,0 +1,10 @@ +createConnection(); + $this->connection->createCursor('DROP TABLE IF EXISTS migrations')->execute(); + parent::setUp(); + } + + public function testCanInstantiate(): void + { + $synchronizer = new VersionSynchronizer($this->connection); + self::assertInstanceOf(VersionSynchronizer::class, $synchronizer); + } + + public function testSetAndGetVersion(): void + { + $synchronizer = new VersionSynchronizer($this->connection); + $synchronizer->setVersion(Version::fromString('1.0.0')); + $synchronizer->setVersion(Version::fromString('1.2.0')); + + $synchronizer = new VersionSynchronizer($this->connection); + + self::assertTrue($synchronizer->getVersion()->equalsLiteral('1.2.0')); + + $cursor = $this->connection->createCursor('SELECT *FROM migrations'); + self::assertCount(2, $cursor->toArray()); + } +} diff --git a/tests/api/Functional/Platform/UserRepositoryTest.php b/tests/api/Functional/Platform/UserRepositoryTest.php new file mode 100644 index 0000000..b828127 --- /dev/null +++ b/tests/api/Functional/Platform/UserRepositoryTest.php @@ -0,0 +1,69 @@ +createConnection(); + $this->faker = FakerFactory::create(); + parent::setUp(); + } + + public function testFindByEmail(): void + { + $repository = $this->getUserRepository(); + $this->createTestUsers($repository); + + $email = 'test@user.com'; + $repository->create(new User($email, 'test')); + + $user = $repository->findUserByEmail($email); + self::assertInstanceOf(User::class, $user); + self::assertSame($email, $user->getEmail()); + self::assertTrue($user->validatePassword('test')); + } + + public function testFailFindByEmail(): void + { + $this->expectException(EntityNotFound::class); + $repository = $this->getUserRepository(); + $this->createTestUsers($repository); + + $repository->findUserByEmail('test'); + } + + private function getUserRepository(): UserRepository + { + $entityManager = new EntityManager(); + $repository = new UserRepository($entityManager, $this->connection); + $schema = new UserSchema($this->connection); + $schema->down(); + $schema->up(); + return $repository; + } + + private function createTestUsers(UserRepository $repository, int $amount = 5) + { + for ($i = 0; $i < $amount; $i++) { + $user = new User($this->faker->email, $this->faker->password); + $repository->create($user); + } + } +} diff --git a/tests/api/StorageTestTrait.php b/tests/api/StorageTestTrait.php new file mode 100644 index 0000000..a747e69 --- /dev/null +++ b/tests/api/StorageTestTrait.php @@ -0,0 +1,42 @@ +connection = ConnectionManager::get($name); + } + + $this->connection = new Connection('sqlite:' . STILUS_TEST_FIXTURE_DIR . '/test.db'); + ConnectionManager::register($name, $this->connection); + + return $this->connection; + } + + public function createEntityManager(): EntityManager + { + $this->entityManager = new EntityManager(); + + return $this->entityManager; + } + + public function createStorage(): Storage + { + $this->storage = new Storage($this->entityManager ?? $this->createEntityManager()); + } +} diff --git a/tests/api/Unit/Platform/UserTest.php b/tests/api/Unit/Platform/UserTest.php new file mode 100644 index 0000000..eb97c84 --- /dev/null +++ b/tests/api/Unit/Platform/UserTest.php @@ -0,0 +1,37 @@ +expectException(UserException::class); + $this->expectExceptionCode(ExceptionCode::INVALID_USER_EMAIL); + new User('invalidemail', 'aa'); + } + + public function testCreatePassword(): void + { + $user = new User('test@email.com', 'password'); + self::assertTrue($user->validatePassword('password')); + } + + public function testVerifyPassword(): void + { + $user = new User('test@email.com', 'password'); + self::assertTrue($user->validatePassword('password')); + self::assertFalse($user->validatePassword('invalid')); + self::assertFalse($user->validatePassword('error')); + } +} diff --git a/tests/api/bootstrap.php b/tests/api/bootstrap.php index 58ce587..09b24b2 100644 --- a/tests/api/bootstrap.php +++ b/tests/api/bootstrap.php @@ -1,4 +1,7 @@ -