From d92a4d0215b7fa39663fb3a36b67c05e6a24f747 Mon Sep 17 00:00:00 2001 From: Matej Lenarcic Date: Thu, 15 Nov 2018 14:24:36 +0100 Subject: [PATCH 1/6] Main functionality --- SOLUTION.md | 14 +- app/config.yml | 6 +- composer.lock | 599 +++++++++++--------------- src/Command/ReportYearlyCommand.php | 80 +++- src/Repository/ProfilesRepository.php | 62 +++ src/Utils/ValidationUtils.php | 15 + 6 files changed, 417 insertions(+), 359 deletions(-) create mode 100644 src/Repository/ProfilesRepository.php create mode 100644 src/Utils/ValidationUtils.php diff --git a/SOLUTION.md b/SOLUTION.md index defe675..28c1600 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -3,7 +3,7 @@ SOLUTION Estimation ---------- -Estimated: n hours +Estimated: 6 hours Spent: x hours @@ -11,3 +11,15 @@ Spent: x hours Solution -------- Comments on your solution + +What could be done better: +- using Doctrine ORM with entities and native repositories +- ProfilesRepository could return []Profile +- used code standard from original code + +Possible test cases: +- No year passed but no error is shown +- Year is of invalid type but no error is shown +- No historical data but no error is shown +- Profiles are not alphabetically listed +- Empty columns do not have 'n/a' \ No newline at end of file diff --git a/app/config.yml b/app/config.yml index 9ccf136..62986ae 100755 --- a/app/config.yml +++ b/app/config.yml @@ -5,4 +5,8 @@ services: database_connection: class: \Doctrine\DBAL\Driver\Connection factory: [\Doctrine\DBAL\DriverManager, getConnection] - arguments: [{url: 'mysql://bof-test:bof-test@127.0.0.1/bof_test'}, '@database_configuration'] \ No newline at end of file + arguments: [{url: 'mysql://bof-test:bof-test@127.0.0.1/bof_test'}, '@database_configuration'] + + repository.profiles: + class: \BOF\Repository\ProfilesRepository + arguments: ['@database_connection'] \ No newline at end of file diff --git a/composer.lock b/composer.lock index a637872..7163127 100755 --- a/composer.lock +++ b/composer.lock @@ -1,108 +1,45 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "eb07b9698db13dac12f90dc9cd3489af", "packages": [ - { - "name": "doctrine/annotations", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "bd4461328621bde0ae6b1b2675fbc6aca4ceb558" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/bd4461328621bde0ae6b1b2675fbc6aca4ceb558", - "reference": "bd4461328621bde0ae6b1b2675fbc6aca4ceb558", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "^5.6.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "time": "2016-12-30T15:59:45+00:00" - }, { "name": "doctrine/cache", - "version": "v1.6.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3" + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/b6f544a20f4807e81f7044d31e679ccbb1866dc3", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", "shasum": "" }, "require": { - "php": "~5.5|~7.0" + "php": "~7.1" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0", - "predis/predis": "~1.0", - "satooshi/php-coveralls": "~0.6" + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^4.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -137,174 +74,41 @@ } ], "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org", "keywords": [ "cache", "caching" ], - "time": "2016-10-29T11:16:17+00:00" - }, - { - "name": "doctrine/collections", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "doctrine/coding-standard": "~0.1@dev", - "phpunit/phpunit": "^5.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "array", - "collections", - "iterator" - ], - "time": "2017-01-03T10:49:41+00:00" - }, - { - "name": "doctrine/common", - "version": "v2.7.2", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common.git", - "reference": "930297026c8009a567ac051fd545bf6124150347" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", - "reference": "930297026c8009a567ac051fd545bf6124150347", - "shasum": "" - }, - "require": { - "doctrine/annotations": "1.*", - "doctrine/cache": "1.*", - "doctrine/collections": "1.*", - "doctrine/inflector": "1.*", - "doctrine/lexer": "1.*", - "php": "~5.6|~7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "lib/Doctrine/Common" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Common Library for Doctrine projects", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "collections", - "eventmanager", - "persistence", - "spl" - ], - "time": "2017-01-13T14:02:13+00:00" + "time": "2018-08-21T18:01:43+00:00" }, { "name": "doctrine/dbal", - "version": "v2.5.10", + "version": "v2.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "fc376f7a61498e18520cd6fa083752a4ca08072b" + "reference": "5140a64c08b4b607b9bedaae0cedd26f04a0e621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/fc376f7a61498e18520cd6fa083752a4ca08072b", - "reference": "fc376f7a61498e18520cd6fa083752a4ca08072b", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/5140a64c08b4b607b9bedaae0cedd26f04a0e621", + "reference": "5140a64c08b4b607b9bedaae0cedd26f04a0e621", "shasum": "" }, "require": { - "doctrine/common": ">=2.4,<2.8-dev", - "php": ">=5.3.2" + "doctrine/cache": "^1.0", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "4.*", - "symfony/console": "2.*||^3.0" + "doctrine/coding-standard": "^4.0", + "jetbrains/phpstorm-stubs": "^2018.1.2", + "phpstan/phpstan": "^0.10.1", + "phpunit/phpunit": "^7.1.2", + "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5", + "symfony/console": "^2.0.5|^3.0|^4.0", + "symfony/phpunit-bridge": "^3.4.5|^4.0.5" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -315,7 +119,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5.x-dev" + "dev-master": "2.8.x-dev", + "dev-develop": "3.0.x-dev" } }, "autoload": { @@ -353,37 +158,41 @@ "persistence", "queryobject" ], - "time": "2017-01-23T23:17:10+00:00" + "time": "2018-07-13T03:16:35+00:00" }, { - "name": "doctrine/inflector", - "version": "v1.1.0", + "name": "doctrine/event-manager", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" + "url": "https://github.com/doctrine/event-manager.git", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" }, "require-dev": { - "phpunit/phpunit": "4.*" + "doctrine/coding-standard": "^4.0", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" } }, "notification-url": "https://packagist.org/downloads/", @@ -410,34 +219,37 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" } ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", + "description": "Doctrine Event Manager component", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", "keywords": [ - "inflection", - "pluralize", - "singularize", - "string" + "event", + "eventdispatcher", + "eventmanager" ], - "time": "2015-11-06T14:35:42+00:00" + "time": "2018-06-11T11:59:03+00:00" }, { - "name": "doctrine/lexer", - "version": "v1.0.1", + "name": "psr/container", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": ">=5.3.0" }, "type": "library", "extra": { @@ -446,8 +258,8 @@ } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" + "psr-4": { + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -456,25 +268,20 @@ ], "authors": [ { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "lexer", - "parser" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "time": "2014-09-09T13:34:57+00:00" + "time": "2017-02-14T16:28:37+00:00" }, { "name": "psr/log", @@ -525,24 +332,32 @@ }, { "name": "symfony/config", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "c5ea878b5a7f6a01b9a2f182f905831711b9ff3f" + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/c5ea878b5a7f6a01b9a2f182f905831711b9ff3f", - "reference": "c5ea878b5a7f6a01b9a2f182f905831711b9ff3f", + "url": "https://api.github.com/repos/symfony/config/zipball/99b2fa8acc244e656cdf324ff419fbe6fd300a4d", + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/filesystem": "~2.8|~3.0" + "php": "^5.5.9|>=7.0.8", + "symfony/filesystem": "~2.8|~3.0|~4.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" }, "require-dev": { - "symfony/yaml": "~3.0" + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", + "symfony/finder": "~3.3|~4.0", + "symfony/yaml": "~3.0|~4.0" }, "suggest": { "symfony/yaml": "To use the yaml reference dumper" @@ -550,7 +365,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -577,43 +392,49 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-01-02T20:32:22+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/console", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd" + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4f9e449e76996adf310498a8ca955c6deebe29dd", - "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd", + "url": "https://api.github.com/repos/symfony/console/zipball/1d228fb4602047d7b26a0554e0d3efd567da5803", + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/debug": "~2.8|~3.0", + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/filesystem": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0" + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" }, "suggest": { - "psr/log": "For using the console logger", + "psr/log-implementation": "For using the console logger", "symfony/event-dispatcher": "", - "symfony/filesystem": "", + "symfony/lock": "", "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -640,37 +461,36 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-01-08T20:47:33+00:00" + "time": "2018-10-30T16:50:50+00:00" }, { "name": "symfony/debug", - "version": "v3.2.2", + "version": "v4.1.7", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05" + "reference": "19090917b848a799cbae4800abf740fe4eb71c1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/810ba5c1c5352a4ddb15d4719e8936751dff0b05", - "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05", + "url": "https://api.github.com/repos/symfony/debug/zipball/19090917b848a799cbae4800abf740fe4eb71c1d", + "reference": "19090917b848a799cbae4800abf740fe4eb71c1d", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^7.1.3", "psr/log": "~1.0" }, "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + "symfony/http-kernel": "<3.4" }, "require-dev": { - "symfony/class-loader": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0" + "symfony/http-kernel": "~3.4|~4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -697,43 +517,51 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-01-02T20:32:22+00:00" + "time": "2018-10-31T09:09:42+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "22b2c97cffc6a612db82084f9e7823b095958751" + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/22b2c97cffc6a612db82084f9e7823b095958751", - "reference": "22b2c97cffc6a612db82084f9e7823b095958751", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9c98452ac7fff4b538956775630bc9701f5384ba", + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8", + "psr/container": "^1.0" }, "conflict": { - "symfony/yaml": "<3.2" + "symfony/config": "<3.3.7", + "symfony/finder": "<3.3", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" }, "require-dev": { - "symfony/config": "~2.8|~3.0", - "symfony/expression-language": "~2.8|~3.0", - "symfony/yaml": "~3.2" + "symfony/config": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" }, "suggest": { "symfony/config": "", "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", "symfony/yaml": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -760,29 +588,30 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2017-01-10T14:21:25+00:00" + "time": "2018-10-31T10:49:51+00:00" }, { "name": "symfony/filesystem", - "version": "v3.2.2", + "version": "v4.1.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4" + "reference": "fd7bd6535beb1f0a0a9e3ee960666d0598546981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/a0c6ef2dc78d33b58d91d3a49f49797a184d06f4", - "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd7bd6535beb1f0a0a9e3ee960666d0598546981", + "reference": "fd7bd6535beb1f0a0a9e3ee960666d0598546981", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -809,29 +638,29 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-01-08T20:47:33+00:00" + "time": "2018-10-30T13:18:25+00:00" }, { "name": "symfony/finder", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8c71141cae8e2957946b403cc71a67213c0380d6" + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8c71141cae8e2957946b403cc71a67213c0380d6", - "reference": "8c71141cae8e2957946b403cc71a67213c0380d6", + "url": "https://api.github.com/repos/symfony/finder/zipball/54ba444dddc5bd5708a34bd095ea67c6eb54644d", + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -858,20 +687,78 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-01-02T20:32:22+00:00" + "time": "2018-10-03T08:46:40+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -883,7 +770,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -917,27 +804,31 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/yaml", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "50eadbd7926e31842893c957eca362b21592a97d" + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/50eadbd7926e31842893c957eca362b21592a97d", - "reference": "50eadbd7926e31842893c957eca362b21592a97d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/640b6c27fed4066d64b64d5903a86043f4a4de7f", + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" }, "require-dev": { - "symfony/console": "~2.8|~3.0" + "symfony/console": "~3.4|~4.0" }, "suggest": { "symfony/console": "For validating YAML files using the lint command" @@ -945,7 +836,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -972,38 +863,44 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-01-03T13:51:32+00:00" + "time": "2018-10-02T16:33:53+00:00" } ], "packages-dev": [ { "name": "symfony/var-dumper", - "version": "v3.2.2", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "b54b23f9a19b465e76fdaac0f6732410467c83b2" + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b54b23f9a19b465e76fdaac0f6732410467c83b2", - "reference": "b54b23f9a19b465e76fdaac0f6732410467c83b2", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ff8ac19e97e5c7c3979236b584719a1190f84181", + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, "require-dev": { - "twig/twig": "~1.20|~2.0" + "ext-iconv": "*", + "twig/twig": "~1.34|~2.4" }, "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", "ext-symfony_debug": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1037,7 +934,7 @@ "debug", "dump" ], - "time": "2017-01-03T08:53:57+00:00" + "time": "2018-10-02T16:33:53+00:00" } ], "aliases": [], diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index 97f026f..042b4a8 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -1,32 +1,100 @@ setName('report:profiles:yearly') ->setDescription('Page views report') - ; + ->addArgument('year', InputArgument::REQUIRED, 'Which year to display. ' . + 'Should be bigger than ' . self::MIN_YEAR); } protected function execute(InputInterface $input, OutputInterface $output) { /** @var $db Connection */ - $io = new SymfonyStyle($input,$output); - $db = $this->getContainer()->get('database_connection'); + $io = new SymfonyStyle($input, $output); + + $year = $input->getArgument('year'); + if (!filter_var($year, FILTER_VALIDATE_INT, [ + 'options' => [ + 'min_range' => 1900 + ] + ])) { + throw new InvalidArgumentException('Year should be numeric and greater than ' . self::MIN_YEAR); + } + + /** @var ProfilesRepository $profilesRepository */ + $profilesRepository = $this->getContainer()->get('repository.profiles'); + $profiles = $profilesRepository->getMonthlyViewsCount($year); + + if (count($profiles) == 0) { + $io->error('No historical data'); + return; + } + + // pivot DB data + $profilesPivot = $this->pivotData($profiles); - $profiles = $db->query('SELECT profile_name FROM profiles')->fetchAll(); + // sort profiles by name + usort($profilesPivot, function ($a, $b) + { + return strcmp($a[0], $b[0]); + }); + + $io->title('Yearly report for year ' . $year); // Show data in a table - headers, data - $io->table(['Profile'], $profiles); + $io->table(array_merge(['Profile'], self::MONTH_NAMES), $profilesPivot); + + } + + /** + * Pivots profiles data so that it can be passed to a Table component. + * + * @param $profiles array array of profiles + * @return array DB data + */ + private function pivotData($profiles): array + { + $profilesPivot = []; + foreach ($profiles as $profile) { + $profilesPivot[$profile['profile_id']][0] = $profile['profile_name']; + $profilesPivot[$profile['profile_id']][$profile['month']] = number_format($profile['sum_views']); + } + foreach ($profilesPivot as &$profilePivot) { + for ($i = 1; $i < count(self::MONTH_NAMES) + 1; $i++) { + if (!isset($profilePivot[$i])) { + $profilePivot[$i] = self::NOT_AVAILABLE; + } + } + // make sure that array elements are at correct places + // (actually, converts a map to array and preserves correct array positions) + ksort($profilePivot); + } + return $profilesPivot; } } diff --git a/src/Repository/ProfilesRepository.php b/src/Repository/ProfilesRepository.php new file mode 100644 index 0000000..dfce244 --- /dev/null +++ b/src/Repository/ProfilesRepository.php @@ -0,0 +1,62 @@ +db = $db; + } + + /** + * Returns views by profile, year and month. + * + * @param int|null $year specified year to filter. If null, returns data for all years + * @return array array of profile_id, profile_name, year, month, sum_views + */ + public function getMonthlyViewsCount(?int $year = null) : array { + + $binds = []; + $sql = 'SELECT + p.profile_id, + p.profile_name, + YEAR(v.date) AS year, + MONTH(v.date) AS month, + SUM(v.views) AS sum_views + FROM + profiles p + LEFT JOIN + views v ON v.profile_id = p.profile_id'; + + if ($year != null) { + $sql .= ' WHERE YEAR(v.date) = :year'; + $binds['year'] = $year; + } + + $sql .= ' + GROUP BY + p.profile_id, p.profile_name, year, month + ORDER BY + p.profile_name, year, month'; + + $stmt = $this->db->prepare($sql); + $stmt->execute($binds); + return $stmt->fetchAll(); + + } +} \ No newline at end of file diff --git a/src/Utils/ValidationUtils.php b/src/Utils/ValidationUtils.php new file mode 100644 index 0000000..2e5787d --- /dev/null +++ b/src/Utils/ValidationUtils.php @@ -0,0 +1,15 @@ + Date: Thu, 15 Nov 2018 15:34:10 +0100 Subject: [PATCH 2/6] Added table, Gherkin tests and solution documentation --- SOLUTION.md | 16 ++++++--- src/Command/ReportYearlyCommand.php | 50 +++++++++++++++++++++-------- src/Tests/YearlyReport.feature | 12 +++++++ src/Utils/ValidationUtils.php | 15 --------- 4 files changed, 59 insertions(+), 34 deletions(-) delete mode 100644 src/Utils/ValidationUtils.php diff --git a/SOLUTION.md b/SOLUTION.md index 28c1600..5a3f24e 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -3,21 +3,27 @@ SOLUTION Estimation ---------- -Estimated: 6 hours +Estimated: 7 hours Spent: x hours Solution -------- -Comments on your solution +ReportYearlyCommand exposes command for yearly report of views per profile. +Added functionality to specify year by command line. + +Future ideas +------------ What could be done better: -- using Doctrine ORM with entities and native repositories +- Using Doctrine ORM with entities and native Doctrine repositories - ProfilesRepository could return []Profile -- used code standard from original code +- Private methods ReportYearlyCommand::pivotData and ReportYearlyCommand::fillEmpty could be set public static and moved to separate utils if needed. Personally I did not find it necessary because there would be too many parameters passed and would make code less readable for single use case + -Possible test cases: +Possible test cases +------------------- - No year passed but no error is shown - Year is of invalid type but no error is shown - No historical data but no error is shown diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index 042b4a8..40dbff3 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -3,8 +3,9 @@ namespace BOF\Command; use BOF\Repository\ProfilesRepository; -use Doctrine\DBAL\Driver\Connection; use InvalidArgumentException; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -34,7 +35,6 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - /** @var $db Connection */ $io = new SymfonyStyle($input, $output); $year = $input->getArgument('year'); @@ -55,19 +55,33 @@ protected function execute(InputInterface $input, OutputInterface $output) return; } - // pivot DB data + // Pivot DB data $profilesPivot = $this->pivotData($profiles); - // sort profiles by name + // Fill non-existent values + $this->fillEmpty($profilesPivot); + + // Sort profiles by name usort($profilesPivot, function ($a, $b) { return strcmp($a[0], $b[0]); }); + // using Table with TableStyle to make values right aligned + $rightAligned = new TableStyle(); + $rightAligned->setPadType(STR_PAD_LEFT); + + $table = new Table($output); + for ($i = 1; $i < count(self::MONTH_NAMES) + 1; $i++) { + $table->setColumnStyle($i, $rightAligned); + } + + // Display title $io->title('Yearly report for year ' . $year); - // Show data in a table - headers, data - $io->table(array_merge(['Profile'], self::MONTH_NAMES), $profilesPivot); + $table->setHeaders(array_merge(['Profile'], self::MONTH_NAMES)); + $table->setRows($profilesPivot); + $table->render(); } @@ -77,24 +91,32 @@ protected function execute(InputInterface $input, OutputInterface $output) * @param $profiles array array of profiles * @return array DB data */ - private function pivotData($profiles): array + private function pivotData(array $profiles): array { $profilesPivot = []; foreach ($profiles as $profile) { $profilesPivot[$profile['profile_id']][0] = $profile['profile_name']; $profilesPivot[$profile['profile_id']][$profile['month']] = number_format($profile['sum_views']); } + return $profilesPivot; + } - foreach ($profilesPivot as &$profilePivot) { + /** + * Fills empty values with 'n/a' when there is no monthly data available + * + * @param array $data array which will be checked for empty values + */ + private function fillEmpty(array &$data) + { + foreach ($data as &$d) { for ($i = 1; $i < count(self::MONTH_NAMES) + 1; $i++) { - if (!isset($profilePivot[$i])) { - $profilePivot[$i] = self::NOT_AVAILABLE; + if (!isset($d[$i])) { + $d[$i] = self::NOT_AVAILABLE; } } - // make sure that array elements are at correct places - // (actually, converts a map to array and preserves correct array positions) - ksort($profilePivot); + // Make sure that array elements are at correct places + // (actually, converts a map to an array and preserves correct array positions) + ksort($d); } - return $profilesPivot; } } diff --git a/src/Tests/YearlyReport.feature b/src/Tests/YearlyReport.feature index e69de29..bc3833a 100755 --- a/src/Tests/YearlyReport.feature +++ b/src/Tests/YearlyReport.feature @@ -0,0 +1,12 @@ +Business Need: Monthly views report for each profile + Scenario: Execute the Yearly Views report + Given That there is historical data available + Then See a monthly breakdown of the total views per profiles + + Scenario: Execute the Yearly Views report + Given That there is historical data available + Then Have the profiles names listed in alphabetical order + + Scenario: Execute the Yearly Views report + Given That there is historical data available + Then Expect to see "n/a" when data is not available diff --git a/src/Utils/ValidationUtils.php b/src/Utils/ValidationUtils.php deleted file mode 100644 index 2e5787d..0000000 --- a/src/Utils/ValidationUtils.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Thu, 15 Nov 2018 15:37:00 +0100 Subject: [PATCH 3/6] Added some error handling --- SOLUTION.md | 2 +- src/Command/ReportYearlyCommand.php | 3 ++- src/Repository/ProfilesRepository.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SOLUTION.md b/SOLUTION.md index 5a3f24e..576c6db 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -5,7 +5,7 @@ Estimation ---------- Estimated: 7 hours -Spent: x hours +Spent: 7 hours Solution diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index 40dbff3..07d4e23 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -43,7 +43,8 @@ protected function execute(InputInterface $input, OutputInterface $output) 'min_range' => 1900 ] ])) { - throw new InvalidArgumentException('Year should be numeric and greater than ' . self::MIN_YEAR); + $io->error('Year should be numeric and greater than ' . self::MIN_YEAR); + return; } /** @var ProfilesRepository $profilesRepository */ diff --git a/src/Repository/ProfilesRepository.php b/src/Repository/ProfilesRepository.php index dfce244..02b80d5 100644 --- a/src/Repository/ProfilesRepository.php +++ b/src/Repository/ProfilesRepository.php @@ -14,7 +14,7 @@ class ProfilesRepository { /** - * A database connection. + * A database connection.e * @var Connection */ private $db; From ff1de8ca543f474b480333f76cb6f9f503a4fdb8 Mon Sep 17 00:00:00 2001 From: Matej Lenarcic Date: Thu, 15 Nov 2018 15:43:53 +0100 Subject: [PATCH 4/6] Fixed typos and added some tests --- SOLUTION.md | 2 +- src/Tests/YearlyReport.feature | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/SOLUTION.md b/SOLUTION.md index 576c6db..6c3693e 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -16,7 +16,7 @@ Added functionality to specify year by command line. Future ideas ------------ -What could be done better: +What could have been done better: - Using Doctrine ORM with entities and native Doctrine repositories - ProfilesRepository could return []Profile - Private methods ReportYearlyCommand::pivotData and ReportYearlyCommand::fillEmpty could be set public static and moved to separate utils if needed. Personally I did not find it necessary because there would be too many parameters passed and would make code less readable for single use case diff --git a/src/Tests/YearlyReport.feature b/src/Tests/YearlyReport.feature index bc3833a..807249f 100755 --- a/src/Tests/YearlyReport.feature +++ b/src/Tests/YearlyReport.feature @@ -10,3 +10,11 @@ Business Need: Monthly views report for each profile Scenario: Execute the Yearly Views report Given That there is historical data available Then Expect to see "n/a" when data is not available + + Scenario: Execute the Yearly Views report + Given That there is no year param passed + Then Expect to see an error + + Scenario: Execute the Yearly Views report + Given That there is year param not numeric + Then Expect to see an error \ No newline at end of file From a534ea0419e954ff3f6a22db4a683e6cb8e221d7 Mon Sep 17 00:00:00 2001 From: Matej Lenarcic Date: Thu, 15 Nov 2018 15:52:48 +0100 Subject: [PATCH 5/6] Added DDL SQL's to documentation --- SOLUTION.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SOLUTION.md b/SOLUTION.md index 6c3693e..1041a58 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -13,6 +13,16 @@ Solution ReportYearlyCommand exposes command for yearly report of views per profile. Added functionality to specify year by command line. +Added PRIMARY KEY and auto_increment to profiles.profile_id and FOREIGN KEY to views.profile_id. Additional SQL needed: + +``` SQL +ALTER TABLE profiles ADD PRIMARY KEY (profile_id); +ALTER TABLE profiles MODIFY profile_id int(11) NOT NULL auto_increment; +ALTER TABLE views +ADD CONSTRAINT views_profiles_profile_id_fk +FOREIGN KEY (profile_id) REFERENCES profiles (profile_id); +``` + Future ideas ------------ From 7044bca6f7d212bc8050507dd3d148a1218047ef Mon Sep 17 00:00:00 2001 From: Matej Lenarcic Date: Thu, 15 Nov 2018 15:53:58 +0100 Subject: [PATCH 6/6] Added DDL SQL's to documentation --- SOLUTION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SOLUTION.md b/SOLUTION.md index 1041a58..bac29fc 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -3,7 +3,7 @@ SOLUTION Estimation ---------- -Estimated: 7 hours +Estimated: 6 hours Spent: 7 hours