├── public ├── favicon.ico ├── robots.txt ├── images │ ├── sample-code.png │ ├── sample-report-min.jpg │ ├── laravel-kata-image.png │ ├── laravel-kata-image-min.png │ ├── laravel-kata-image-trans.png │ └── laravel-kata-image-transparent.png ├── .htaccess └── index.php ├── resources ├── css │ └── app.css └── js │ ├── app.js │ └── bootstrap.js ├── database ├── .gitignore ├── init.sh ├── seeders │ ├── Environments │ │ ├── LocalSeeder.php │ │ ├── TestingSeeder.php │ │ ├── ProductionSeeder.php │ │ └── EnvironmentSeeder.php │ ├── Models │ │ ├── ModelSeeder.php │ │ ├── CurrenciesSeeder.php │ │ ├── CountriesSeeder.php │ │ ├── UsersSeeder.php │ │ └── ExchangeRatesSeeder.php │ ├── DefaultSeeder.php │ └── DatabaseSeeder.php ├── factories │ ├── DateFactory.php │ ├── BlogFactory.php │ ├── CurrencyFactory.php │ ├── ExchangeRateFactory.php │ ├── CountryFactory.php │ ├── UserFactory.php │ └── ExperimentARecordFactory.php ├── migrations-squashed │ ├── 2023_07_06_164057_create_days_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2022_11_12_184330_create_currencies_table.php │ ├── 2024_01_28_073118_change_exchange_rate_precision.php │ ├── 2023_09_16_054345_create_products_table.php │ ├── 2022_11_08_181619_create_user_blogs_table.php │ ├── 2022_11_09_170037_create_countries_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ └── 2022_11_13_112131_create_exchange_rates_table.php └── migrations │ ├── 2024_09_21_173259_add_experiment_a_records_position.php │ └── 2024_09_16_172608_create_experiment_a_records_table.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore ├── framework │ ├── testing │ │ └── .gitignore │ ├── views │ │ └── .gitignore │ ├── cache │ │ ├── data │ │ │ └── .gitignore │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ └── .gitignore └── clockwork │ └── .gitignore ├── tests ├── .pest │ └── snapshots │ │ ├── NoData │ │ ├── HelpersTest │ │ │ ├── it_can_convert_bytes_to_human_with_data_set___0____0_.snap │ │ │ ├── it_can_convert_bytes_to_human_with_data_set___1____1_.snap │ │ │ ├── it_can_convert_bytes_to_human_with_data_set___2____2_.snap │ │ │ ├── it_can_convert_bytes_to_human_with_data_set___0_1____0_1_.snap │ │ │ ├── it_can_convert_time_to_human__digital__with_data_set___0____0_.snap │ │ │ ├── it_can_convert_time_to_human__digital__with_data_set___1____1_.snap │ │ │ ├── it_can_convert_time_to_human__digital__with_data_set___2____2_.snap │ │ │ ├── it_can_convert_time_to_human__false__with_data_set___0_1____0_1_.snap │ │ │ ├── it_can_convert_time_to_human__false__with_data_set___0____0_.snap │ │ │ ├── it_can_convert_time_to_human__false__with_data_set___1____1_.snap │ │ │ ├── it_can_convert_time_to_human__false__with_data_set___2____2_.snap │ │ │ ├── it_can_convert_bytes_to_human_with_data_set___9999999999999____9999999999999_.snap │ │ │ ├── it_can_convert_time_to_human__digital__with_data_set___0_1____0_1_.snap │ │ │ ├── it_can_convert_time_to_human__false__with_data_set___99999999____99999999_.snap │ │ │ └── it_can_convert_time_to_human__digital__with_data_set___9999999999999____9999999999999_.snap │ │ └── Utilities │ │ │ └── BenchmarkTest │ │ │ ├── it_can_read_file_with_data_set____sleep_xt______sleep_xt__.snap │ │ │ └── it_can_read_file_with_data_set____memory_xt______memory_xt__.snap │ │ └── Data │ │ ├── Models │ │ └── CountryTest │ │ │ ├── it_can_get_exchange_rates_aggregates_with_data_set___App_Enums_CountryCode_Enum__ZA___ZA______App_Enums_CountryCode_Enum__ZA___ZA___.snap │ │ │ └── it_can_get_exchange_rates_aggregates_with_data_set___App_Enums_CountryCode_Enum__NL___NL______App_Enums_CountryCode_Enum__NL___NL___.snap │ │ └── Services │ │ └── CountryServiceTest │ │ ├── it_can_get_exchange_rates_aggregates_with_data_set___App_Enums_CountryCode_Enum__ZA___ZA______App_Enums_CountryCode_Enum__ZA___ZA___.snap │ │ └── it_can_get_exchange_rates_aggregates_with_data_set___App_Enums_CountryCode_Enum__NL___NL______App_Enums_CountryCode_Enum__NL___NL___.snap ├── TestCaseNoData.php ├── Pest.php ├── NoData │ ├── Database │ │ └── Seeders │ │ │ └── Models │ │ │ ├── BlogsSeederTest.php │ │ │ ├── UsersSeederTest.php │ │ │ ├── CurrenciesSeederTest.php │ │ │ ├── CountriesSeederTest.php │ │ │ └── ExchangeRatesSeederTest.php │ ├── Utilities │ │ ├── BenchmarkTest.php │ │ └── Files │ │ │ ├── sleep.xt │ │ │ ├── memory.xt │ │ │ └── sample.xt │ ├── Feature │ │ └── MockExchangeRateHostTest.php │ ├── CacheTest.php │ ├── Vendor │ │ └── SebastianBergmann │ │ │ └── Diff │ │ │ └── CanUseDiff.php │ └── HelpersTest.php ├── Data │ ├── Console │ │ └── Commands │ │ │ ├── FakeChallenges │ │ │ ├── A │ │ │ │ ├── TooSlow.php │ │ │ │ ├── WrongOutput.php │ │ │ │ ├── NotUsingReturn.php │ │ │ │ └── NotFound.php │ │ │ └── B │ │ │ │ ├── WrongOutput.php │ │ │ │ └── TooSlow.php │ │ │ ├── KataTestCommandTest.php │ │ │ └── KataProfileCommandTest.php │ ├── Models │ │ ├── CurrencyTest.php │ │ ├── ExchangeRateTest.php │ │ ├── UserTest.php │ │ ├── BlogTest.php │ │ └── CountryTest.php │ ├── Challenges │ │ └── ChallengesReturnTest.php │ ├── Services │ │ └── CountryServiceTest.php │ ├── Feature │ │ └── KataFeatureTest.php │ ├── LaravelPlus │ │ └── Collections │ │ │ └── SmartCollectionTest.php │ └── Traits │ │ └── HasExitHintsTraitTest.php ├── TestCaseData.php ├── CreatesApplication.php ├── TestCase.php └── Datasets │ └── challenge-methods.php ├── pint.json ├── docker ├── pgsql │ └── create-testing-database.sql ├── mysql │ └── create-testing-database.sh └── 8.3 │ ├── php.ini │ ├── supervisord.conf │ └── start-container ├── app ├── Enums │ ├── KataRunnerMode.php │ ├── KataRunMode.php │ ├── KataRunnerIterationMode.php │ ├── CurrencyCode.php │ └── CountryCode.php ├── Exceptions │ ├── BaseCollectionException.php │ ├── KataChallengeException.php │ ├── BenchmarkProfileException.php │ ├── KataChallengeScoreException.php │ ├── KataInvalidRunModeException.php │ ├── KataChallengeNotFoundException.php │ ├── KataChallengeProfilingException.php │ ├── KataChallengeScoreOutputsMd5Exception.php │ └── Handler.php ├── Collections │ ├── UserCollection.php │ ├── CountryCollection.php │ ├── CurrencyCollection.php │ ├── BlogCollection.php │ └── ExchangeRateCollection.php ├── Models │ ├── Day.php │ ├── Blog.php │ ├── User.php │ ├── ExchangeRate.php │ ├── Currency.php │ └── Country.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrustHosts.php │ │ ├── TrimStrings.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── TrustProxies.php │ │ └── RedirectIfAuthenticated.php │ ├── Controllers │ │ └── Controller.php │ └── Kernel.php ├── Challenges │ ├── B │ │ ├── Silly.php │ │ ├── Php.php │ │ ├── FxConversion.php │ │ ├── Laravel.php │ │ ├── Sample.php │ │ ├── CleanCodeDatabase.php │ │ ├── MySql.php │ │ ├── CleanCode.php │ │ ├── Eloquent.php │ │ └── Concurrent.php │ └── A │ │ ├── Laravel.php │ │ ├── Php.php │ │ ├── silly.sh │ │ ├── CleanCode.php │ │ ├── CleanCodeDatabase.php │ │ ├── Eloquent.php │ │ ├── Sample.php │ │ ├── MySql.php │ │ └── FxConversion.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AuthServiceProvider.php │ ├── AppServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Traits │ ├── HasCollection.php │ ├── EnumTrait.php │ └── HasExitHintsTrait.php ├── Services │ └── CountryService.php ├── Console │ ├── Kernel.php │ └── Commands │ │ ├── KataTestCommand.php │ │ ├── KataProfileCommand.php │ │ └── KataRunCommand.php ├── Utilities │ └── CodeUtility.php ├── KataChallenge.php └── helpers.php ├── packages └── larawell │ ├── laravel-plus │ ├── src │ │ ├── Exceptions │ │ │ └── SmartCollectionException.php │ │ └── Console │ │ │ └── Commands │ │ │ └── Traits │ │ │ └── SmartChoice.php │ └── composer.json │ └── inspectation │ └── src │ ├── Functions.php │ ├── composer.json │ └── Inspectation.php ├── domain ├── CleanCode │ ├── Exceptions │ │ └── InvalidProductDataException.php │ ├── Contracts │ │ └── ShapeInterface.php │ ├── Repositories │ │ ├── ProductRepository.php │ │ └── Repository.php │ ├── Objects │ │ ├── Shape.php │ │ ├── ShapeCircle.php │ │ ├── ShapeSquare.php │ │ └── ShapeRectangle.php │ ├── Models │ │ └── Product.php │ ├── Services │ │ └── ProductService.php │ └── Controllers │ │ └── ProductController.php └── DirtyCode │ ├── shapes.php │ └── products.php ├── config ├── exchange-rates.php ├── modules │ └── fx-conversion.php ├── concurrency.php ├── cors.php ├── services.php ├── view.php ├── tinker.php ├── hashing.php ├── broadcasting.php ├── filesystems.php └── sanctum.php ├── bin ├── complexity.sh ├── bump.sh ├── test.sh ├── coverage.sh ├── benchmark.sh ├── k6.js └── restart.sh ├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── package.json ├── .env.circleci ├── routes ├── web.php ├── channels.php ├── console.php ├── mock.php └── api.php ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── .github ├── dependabot.yml └── workflows │ └── github-pages.yml ├── LICENSE ├── phpunit.xml ├── artisan ├── .env.example └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/clockwork/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.json.gz 3 | index 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_bytes_to_human_with_data_set___0____0_.snap: -------------------------------------------------------------------------------- 1 | 0 B -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_bytes_to_human_with_data_set___1____1_.snap: -------------------------------------------------------------------------------- 1 | 1 B -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_bytes_to_human_with_data_set___2____2_.snap: -------------------------------------------------------------------------------- 1 | 2 B -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_bytes_to_human_with_data_set___0_1____0_1_.snap: -------------------------------------------------------------------------------- 1 | 0 B -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "notPath": [ 4 | "app/Challenges/A/Silly.php" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /database/init.sh: -------------------------------------------------------------------------------- 1 | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL 2 | SHOW DATABASES; 3 | EOSQL 4 | -------------------------------------------------------------------------------- /public/images/sample-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/sample-code.png -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__digital__with_data_set___0____0_.snap: -------------------------------------------------------------------------------- 1 | 0.000000000 s -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__digital__with_data_set___1____1_.snap: -------------------------------------------------------------------------------- 1 | 00:00:00.001 -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__digital__with_data_set___2____2_.snap: -------------------------------------------------------------------------------- 1 | 00:00:00.002 -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__false__with_data_set___0_1____0_1_.snap: -------------------------------------------------------------------------------- 1 | 0.100000000 s -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__false__with_data_set___0____0_.snap: -------------------------------------------------------------------------------- 1 | 0.000000000 s -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__false__with_data_set___1____1_.snap: -------------------------------------------------------------------------------- 1 | 1.000000000 s -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__false__with_data_set___2____2_.snap: -------------------------------------------------------------------------------- 1 | 2.000000000 s -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_bytes_to_human_with_data_set___9999999999999____9999999999999_.snap: -------------------------------------------------------------------------------- 1 | 9 TB -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__digital__with_data_set___0_1____0_1_.snap: -------------------------------------------------------------------------------- 1 | 0.100000000 s -------------------------------------------------------------------------------- /public/images/sample-report-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/sample-report-min.jpg -------------------------------------------------------------------------------- /public/images/laravel-kata-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/laravel-kata-image.png -------------------------------------------------------------------------------- /public/images/laravel-kata-image-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/laravel-kata-image-min.png -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__false__with_data_set___99999999____99999999_.snap: -------------------------------------------------------------------------------- 1 | 99,999,999.000000000 s -------------------------------------------------------------------------------- /public/images/laravel-kata-image-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/laravel-kata-image-trans.png -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/HelpersTest/it_can_convert_time_to_human__digital__with_data_set___9999999999999____9999999999999_.snap: -------------------------------------------------------------------------------- 1 | 2777777:46:39.999 -------------------------------------------------------------------------------- /docker/pgsql/create-testing-database.sql: -------------------------------------------------------------------------------- 1 | SELECT 'CREATE DATABASE testing' 2 | WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec 3 | -------------------------------------------------------------------------------- /public/images/laravel-kata-image-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HendrikPrinsZA/laravel-kata/HEAD/public/images/laravel-kata-image-transparent.png -------------------------------------------------------------------------------- /app/Enums/KataRunnerMode.php: -------------------------------------------------------------------------------- 1 | '2010-01-01', 5 | 'api-host' => env('EXCHANGE_RATE_API_HOST', sprintf('%s/mock/exchangerate', env('APP_URL'))), 6 | 'api-key' => env('EXCHANGE_RATE_API_KEY', 'api-key'), 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in(sprintf('%s/%s', __DIR__, '/Data')); 8 | 9 | uses(TestCaseNoData::class) 10 | ->in(sprintf('%s/%s', __DIR__, '/NoData')); 11 | -------------------------------------------------------------------------------- /database/seeders/Environments/EnvironmentSeeder.php: -------------------------------------------------------------------------------- 1 | seed(BlogsSeeder::class); 8 | 9 | expect(Blog::count())->toBeGreaterThan(0); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/NoData/Database/Seeders/Models/UsersSeederTest.php: -------------------------------------------------------------------------------- 1 | seed(UsersSeeder::class); 8 | 9 | expect(User::count())->toBeGreaterThan(0); 10 | }); 11 | -------------------------------------------------------------------------------- /app/Collections/CountryCollection.php: -------------------------------------------------------------------------------- 1 | 'date:Y-m-d', 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /tests/NoData/Database/Seeders/Models/CurrenciesSeederTest.php: -------------------------------------------------------------------------------- 1 | seed(CurrenciesSeeder::class); 8 | 9 | expect(Currency::count()) 10 | ->toBeGreaterThan(0); 11 | }); 12 | -------------------------------------------------------------------------------- /domain/CleanCode/Repositories/ProductRepository.php: -------------------------------------------------------------------------------- 1 | area() + $this->circumference(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Data/Console/Commands/FakeChallenges/B/WrongOutput.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'script-caching' => [ 6 | 'enabled' => true, 7 | 'strategy' => 'monthly', // daily, monthly, yearly, all 8 | ], 9 | 'global-caching' => [ 10 | 'enabled' => true, 11 | 'strategy' => 'all', 12 | ], 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/NoData/Utilities/BenchmarkTest/it_can_read_file_with_data_set____memory_xt______memory_xt__.snap: -------------------------------------------------------------------------------- 1 | { 2 | "time": { 3 | "start": 1.607359, 4 | "end": 1.607404, 5 | "total": 4.500000000007276e-5 6 | }, 7 | "memory_usage": { 8 | "start": 25400640, 9 | "end": 25402024, 10 | "total": 1384 11 | } 12 | } -------------------------------------------------------------------------------- /tests/Data/Models/CurrencyTest.php: -------------------------------------------------------------------------------- 1 | toBeInstanceOf(Currency::class) 11 | ->id->not->toBeNull() 12 | ->countries->toContainOnlyInstancesOf(Country::class); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/NoData/Utilities/BenchmarkTest.php: -------------------------------------------------------------------------------- 1 | toMatchSnapshot(); 10 | })->with([ 11 | 'sleep.xt', 12 | 'memory.xt', 13 | ]); 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | RED='\033[0;31m' 2 | NC='\033[0m' # No Color 3 | 4 | echo "Linting" 5 | ./vendor/bin/sail php ./vendor/bin/tlint format --diff 6 | 7 | echo "Pinting" 8 | ./vendor/bin/sail php ./vendor/bin/pint --dirty -v 9 | 10 | if [[ $(git diff --stat) != '' ]]; then 11 | printf "\n${RED}Exception: Dirty files found, check and commit again!${NC}\n" 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /docker/8.3/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | pcov.directory = . 6 | memory_limit = 4G 7 | 8 | [opcache] 9 | opcache.enable_cli=1 10 | 11 | [xdebug] 12 | xdebug.mode=profile,trace 13 | xdebug.start_with_request=trigger 14 | xdebug.trace_output_dir=/var/www/html/storage/logs/xdebug 15 | xdebug.use_compression=false 16 | -------------------------------------------------------------------------------- /domain/CleanCode/Models/Product.php: -------------------------------------------------------------------------------- 1 | 'float', 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /tests/TestCaseData.php: -------------------------------------------------------------------------------- 1 | seed(DatabaseSeeder::class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/seeders/Models/ModelSeeder.php: -------------------------------------------------------------------------------- 1 | seed(); 12 | $this->cleanup(); 13 | } 14 | 15 | abstract public function seed(): void; 16 | 17 | protected function cleanup(): void {} 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Data/Console/Commands/FakeChallenges/A/NotFound.php: -------------------------------------------------------------------------------- 1 | seed(CurrenciesSeeder::class); 9 | $this->seed(CountriesSeeder::class); 10 | 11 | expect(Country::count()) 12 | ->toBeGreaterThan(0); 13 | }); 14 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Challenges/B/Silly.php: -------------------------------------------------------------------------------- 1 | self::PHP_MEM_MAX_ITERATIONS) 12 | ? self::PHP_MEM_MAX_ITERATIONS 13 | : $iteration; 14 | 15 | return $iteration % 2 == 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/factories/DateFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class DateFactory extends Factory 11 | { 12 | public function definition() 13 | { 14 | return [ 15 | 'day' => $this->faker->date(), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Data/Models/ExchangeRateTest.php: -------------------------------------------------------------------------------- 1 | toBeInstanceOf(ExchangeRate::class) 11 | ->id->not->toBeNull() 12 | ->baseCurrency->toBeInstanceOf(Currency::class) 13 | ->targetCurrency->toBeInstanceOf(Currency::class); 14 | }); 15 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | getRangeLimit($iteration))); 12 | } 13 | 14 | public function nativeSum(int $iteration): int 15 | { 16 | return array_sum(range(0, $this->getRangeLimit($iteration))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Data/Challenges/ChallengesReturnTest.php: -------------------------------------------------------------------------------- 1 | make($challengeA); 6 | $instanceB = app()->make($challengeB); 7 | 8 | $returnA = $instanceA->{$method}(2); 9 | $returnB = $instanceB->{$method}(2); 10 | expect($returnA)->toEqual($returnB); 11 | })->with('challenge-methods'); 12 | -------------------------------------------------------------------------------- /tests/NoData/Feature/MockExchangeRateHostTest.php: -------------------------------------------------------------------------------- 1 | toEndWith('/mock/exchangerate'); 6 | }); 7 | 8 | it('can get timeframe', function () { 9 | $response = $this->get('/mock/exchangerate/timeframe?access_key=fake-key&start_date=2024-01-28&end_date=2024-01-28&source=EUR¤cies=AED,GBP,USD,ZAR'); 10 | expect($response)->toMatchSnapshot(); 11 | }); 12 | -------------------------------------------------------------------------------- /domain/CleanCode/Objects/ShapeCircle.php: -------------------------------------------------------------------------------- 1 | radius ** 2; 15 | } 16 | 17 | public function circumference(): float 18 | { 19 | return 2 * pi() * $this->radius; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/larawell/inspectation/src/Functions.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function inspect(mixed $value = null): Inspectation 15 | { 16 | return new Inspectation($value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Enums/CurrencyCode.php: -------------------------------------------------------------------------------- 1 | where('code', $this->value)->first(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /domain/CleanCode/Objects/ShapeSquare.php: -------------------------------------------------------------------------------- 1 | sideLength, 2); 16 | } 17 | 18 | public function circumference(): float 19 | { 20 | return 4 * $this->sideLength; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Data/Models/UserTest.php: -------------------------------------------------------------------------------- 1 | make(); 7 | 8 | expect($record) 9 | ->toBeInstanceOf(User::class) 10 | ->id->toBeNull(); 11 | }); 12 | 13 | it('can create', function () { 14 | $record = User::factory()->create(); 15 | 16 | expect($record) 17 | ->toBeInstanceOf(User::class) 18 | ->id->not->toBeNull(); 19 | 20 | $record->delete(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/NoData/Database/Seeders/Models/ExchangeRatesSeederTest.php: -------------------------------------------------------------------------------- 1 | seed(CurrenciesSeeder::class); 9 | $this->seed(ExchangeRatesSeeder::class); 10 | 11 | expect(ExchangeRate::count())->toBe(1464); 12 | expect(ExchangeRate::all()->avg('rate')) 13 | ->toBe(0.9158994000000007); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>&1 ; pwd -P )" 4 | PATH_TO_REPO="$PATH_TO_SCRIPT_DIR/../" 5 | source $PATH_TO_REPO/.env 6 | 7 | if [ "${CI_MODE}" == "circleci" ]; then 8 | echo "LK_RUN_MODE=benchmark" >> "$PATH_TO_REPO/.env" 9 | echo "EXCHANGE_RATE_API_HOST=$EXCHANGE_RATE_API_HOST" >> "$PATH_TO_REPO/.env" 10 | echo "EXCHANGE_RATE_API_KEY=$EXCHANGE_RATE_API_KEY" >> "$PATH_TO_REPO/.env" 11 | fi 12 | 13 | ./vendor/bin/sail test --parallel 14 | -------------------------------------------------------------------------------- /domain/CleanCode/Objects/ShapeRectangle.php: -------------------------------------------------------------------------------- 1 | width * $this->height; 17 | } 18 | 19 | public function circumference(): float 20 | { 21 | return 2 * ($this->width + $this->height); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.env.circleci: -------------------------------------------------------------------------------- 1 | APP_URL=http://127.0.0.1:8000 2 | DB_HOST=127.0.0.1 3 | LK_PROGRESS_BAR_DISABLED=true 4 | LK_DD_MAX_USERS=1000 5 | LK_DD_MAX_USER_BLOGS=10 6 | XDEBUG_MODE=profile,trace 7 | XDEBUG_CONFIG="output_dir=/var/www/html/storage/logs/xdebug xdebug.use_compression=false" 8 | SAIL_XDEBUG_MODE=profile,trace 9 | SAIL_XDEBUG_MODE=profile,trace 10 | SAIL_XDEBUG_CONFIG="output_dir=/var/www/html/storage/logs/xdebug xdebug.use_compression=false" 11 | EXCHANGE_RATE_API_HOST=http://127.0.0.1:8000/mock/exchangerate 12 | EXCHANGE_RATE_API_KEY=fake-key 13 | -------------------------------------------------------------------------------- /tests/Data/Services/CountryServiceTest.php: -------------------------------------------------------------------------------- 1 | make(CountryService::class); 10 | $aggregates = $countryService->getExchangeRatesAggregates($country); 11 | 12 | expect($aggregates)->toMatchSnapshot(); 13 | })->with([ 14 | CountryCode::ZA, 15 | CountryCode::NL, 16 | ]); 17 | -------------------------------------------------------------------------------- /database/factories/BlogFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class BlogFactory extends Factory 12 | { 13 | public function definition() 14 | { 15 | return [ 16 | 'user_id' => User::factory(), 17 | 'title' => $this->faker->sentence(4), 18 | 'description' => $this->faker->text(), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /tests/Data/Models/BlogTest.php: -------------------------------------------------------------------------------- 1 | make(); 8 | 9 | expect($record) 10 | ->toBeInstanceOf(Blog::class) 11 | ->id->toBeNull() 12 | ->user->toBeInstanceOf(User::class); 13 | }); 14 | 15 | it('can create', function () { 16 | $record = Blog::factory()->create(); 17 | 18 | expect($record) 19 | ->toBeInstanceOf(Blog::class) 20 | ->id->not->toBeNull() 21 | ->user->toBeInstanceOf(User::class); 22 | 23 | $record->delete(); 24 | }); 25 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Traits/HasCollection.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->date('day')->unique(); 14 | $table->timestamps(); 15 | }); 16 | } 17 | 18 | public function down(): void 19 | { 20 | Schema::dropIfExists('days'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /app/Services/CountryService.php: -------------------------------------------------------------------------------- 1 | (float) $country->exchangeRates()->avg('rate'), 13 | 'sum' => (float) $country->exchangeRates()->sum('rate'), 14 | 'min' => (float) $country->exchangeRates()->min('rate'), 15 | 'max' => (float) $country->exchangeRates()->max('rate'), 16 | 'count' => (int) $country->exchangeRates()->count(), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | dev-deps: 13 | dependency-type: "development" 14 | prod-deps: 15 | dependency-type: "production" 16 | - package-ecosystem: "composer" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | groups: 21 | dev-deps: 22 | dependency-type: "development" 23 | prod-deps: 24 | dependency-type: "production" 25 | -------------------------------------------------------------------------------- /app/Traits/EnumTrait.php: -------------------------------------------------------------------------------- 1 | details(); 13 | } 14 | 15 | return [ 16 | 'code' => $case->value, 17 | 'name' => $case->name, 18 | ]; 19 | } 20 | 21 | public static function all(): Collection 22 | { 23 | return collect(self::cases())->map( 24 | fn (self $case) => $case->getMappingDetails($case) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/CurrencyFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class CurrencyFactory extends Factory 12 | { 13 | public function definition() 14 | { 15 | return [ 16 | 'code' => $this->faker->randomElement([ 17 | CurrencyCode::EUR, 18 | CurrencyCode::USD, 19 | CurrencyCode::ZAR, 20 | ]), 21 | 'name' => 'Euro', 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Challenges/B/FxConversion.php: -------------------------------------------------------------------------------- 1 | calculateTotalExchangeRate($iteration, true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/concurrency.php: -------------------------------------------------------------------------------- 1 | env('CONCURRENCY_DRIVER', 'process'), 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /packages/larawell/inspectation/src/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "larawell/inspectation", 3 | "description": "Experimental Laravel Extension for Pest", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Larawell\\Inspectation\\": "src/" 9 | }, 10 | "files": [ 11 | "src/Functions.php" 12 | ] 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Hendrik Prinsloo", 17 | "email": "info@hendrikprinsloo.co.za" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1.0" 22 | }, 23 | "require-dev": { } 24 | } 25 | -------------------------------------------------------------------------------- /app/Challenges/B/Laravel.php: -------------------------------------------------------------------------------- 1 | make(CountryService::class); 15 | 16 | $total = 0; 17 | foreach (Country::all() as $country) { 18 | $total += array_sum($countryService->getExchangeRatesAggregates($country)); 19 | } 20 | 21 | return $total; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /domain/DirtyCode/shapes.php: -------------------------------------------------------------------------------- 1 | productRepository->create($productData); 18 | } 19 | 20 | public function makeProduct(array $productData): ?Product 21 | { 22 | return $this->productRepository->make($productData); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /database/migrations-squashed/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 13 | $table->string('token'); 14 | $table->timestamp('created_at')->nullable(); 15 | }); 16 | } 17 | 18 | public function down() 19 | { 20 | Schema::dropIfExists('password_resets'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations-squashed/2022_11_12_184330_create_currencies_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('code', 3)->unique(); 14 | $table->string('name', 100); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('currencies'); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /docker/8.3/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then 4 | echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'." 5 | exit 1 6 | fi 7 | 8 | if [ ! -z "$WWWUSER" ]; then 9 | usermod -u $WWWUSER sail 10 | fi 11 | 12 | if [ ! -d /.composer ]; then 13 | mkdir /.composer 14 | fi 15 | 16 | chmod -R ugo+rw /.composer 17 | 18 | if [ $# -gt 0 ]; then 19 | if [ "$SUPERVISOR_PHP_USER" = "root" ]; then 20 | exec "$@" 21 | else 22 | exec gosu $WWWUSER "$@" 23 | fi 24 | else 25 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 26 | fi 27 | -------------------------------------------------------------------------------- /bin/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH_TO_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | PATH_TO_REPO="$PATH_TO_SCRIPT_DIR/.." 5 | PATH_TO_STORAGE_REL="storage/logs/coverage" 6 | PATH_TO_STORAGE="$PATH_TO_REPO/$PATH_TO_STORAGE_REL" 7 | 8 | if [ ! -d $PATH_TO_STORAGE ]; then 9 | mkdir -p $PATH_TO_STORAGE 10 | fi 11 | 12 | ./vendor/bin/sail test \ 13 | --coverage-html=$PATH_TO_STORAGE_REL/html \ 14 | --coverage-clover=$PATH_TO_STORAGE_REL/clover.xml \ 15 | --coverage-text=$PATH_TO_STORAGE_REL/coverage.txt \ 16 | --log-junit $PATH_TO_STORAGE_REL/junit.xml \ 17 | --stop-on-failure 18 | 19 | echo "See $PATH_TO_STORAGE/html/index.html" 20 | 21 | -------------------------------------------------------------------------------- /database/seeders/Models/CurrenciesSeeder.php: -------------------------------------------------------------------------------- 1 | exchangeRateService->getCurrencies(); 18 | $currencies->upsert(); 19 | } 20 | 21 | protected function cleanup(): void 22 | { 23 | Currency::whereNotIn('code', CurrencyCode::cases())->delete(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /database/migrations-squashed/2024_01_28_073118_change_exchange_rate_precision.php: -------------------------------------------------------------------------------- 1 | decimal('rate', 20, 10)->change(); 13 | }); 14 | } 15 | 16 | public function down(): void 17 | { 18 | Schema::table('exchange_rates', function (Blueprint $table) { 19 | $table->decimal('rate', 10, 3)->change(); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations-squashed/2023_09_16_054345_create_products_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name'); 14 | $table->text('description'); 15 | $table->decimal('price'); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | Schema::dropIfExists('products'); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/NoData/Utilities/Files/sleep.xt: -------------------------------------------------------------------------------- 1 | Version: 3.2.1 2 | File format: 4 3 | TRACE START [2023-09-17 18:29:00.168759] 4 | 22 54731 0 4.350674 25029952 App\KataRunner->App\{closure:/var/www/html/app/KataRunner.php:659-659} 1 /var/www/html/app/Utilities/Benchmark.php 100 0 5 | 23 54732 0 4.350688 25029952 App\Challenges\A\Sample->sleep 1 /var/www/html/app/KataRunner.php 659 1 3 6 | 24 54733 0 4.350712 25029952 sleep 0 /var/www/html/app/Challenges/A/Sample.php 11 1 3 7 | 24 54733 1 7.352751 25029984 8 | 23 54732 1 7.353625 25029952 9 | 22 54731 1 7.353879 25029952 10 | 22 54734 0 7.353963 25029952 xdebug_stop_trace 0 /var/www/html/app/Utilities/Benchmark.php 102 0 11 | 7.353992 25030000 12 | TRACE END [2023-09-17 18:29:03.172198] 13 | -------------------------------------------------------------------------------- /bin/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH_TO_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 4 | PATH_TO_REPO="$PATH_TO_SCRIPT_DIR/../" 5 | source $PATH_TO_REPO/.env 6 | 7 | if [ "${CI_MODE}" == "circleci" ]; then 8 | echo "LK_RUN_MODE=benchmark" >> "$PATH_TO_REPO/.env" 9 | echo "EXCHANGE_RATE_API_HOST=$EXCHANGE_RATE_API_HOST" >> "$PATH_TO_REPO/.env" 10 | echo "EXCHANGE_RATE_API_KEY=$EXCHANGE_RATE_API_KEY" >> "$PATH_TO_REPO/.env" 11 | php -d xdebug.mode=profile artisan kata:run --all 12 | else 13 | ./vendor/bin/sail artisan kata:run --all 14 | fi 15 | 16 | exitCode=$? 17 | if [ $exitCode -ne 0 ]; then 18 | echo "Error: Kata run failed" 19 | exit $exitCode 20 | fi 21 | 22 | exit 0 23 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $policies = [ 15 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 16 | ]; 17 | 18 | /** 19 | * Register any authentication / authorization services. 20 | * 21 | * @return void 22 | */ 23 | public function boot() 24 | { 25 | $this->registerPolicies(); 26 | 27 | // 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/ExchangeRateFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ExchangeRateFactory extends Factory 12 | { 13 | public function definition() 14 | { 15 | return [ 16 | 'base_currency_id' => Currency::factory(), 17 | 'target_currency_id' => Currency::factory(), 18 | 'target_currency_code' => 'Custom title for tests', 19 | 'rate' => $this->faker->randomFloat(4, 0, 3), 20 | 'date' => $this->faker->date(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Data/Console/Commands/KataTestCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('kata:test') 8 | ->expectsOutputToContain('Database: laravel') 9 | ->expectsOutputToContain('Database: testing') 10 | ->assertExitCode(Command::SUCCESS); 11 | }); 12 | 13 | it('fails when does not exist', function (string $database) { 14 | $configKey = sprintf('database.connections.%s.database', $database); 15 | Config::set($configKey, 'fake-database'); 16 | 17 | $this->artisan('kata:test') 18 | ->assertExitCode(Command::FAILURE); 19 | 20 | })->with([ 21 | 'mysql', 22 | 'testing', 23 | ]); 24 | -------------------------------------------------------------------------------- /database/migrations-squashed/2022_11_08_181619_create_user_blogs_table.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->foreignIdFor(User::class); 15 | $table->string('title', 255); 16 | $table->text('description'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('blogs'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 18 | } 19 | 20 | /** 21 | * Register the commands for the application. 22 | * 23 | * @return void 24 | */ 25 | protected function commands() 26 | { 27 | $this->load(__DIR__.'/Commands'); 28 | 29 | require base_path('routes/console.php'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/seeders/Models/CountriesSeeder.php: -------------------------------------------------------------------------------- 1 | runningUnitTests()) { 15 | if (Currency::count() === 0) { 16 | $this->seed(CurrenciesSeeder::class); 17 | } 18 | } 19 | 20 | $countries = CountryCollection::make(); 21 | CountryCode::all()->each( 22 | fn (array $details) => $countries->push(Country::make($details)) 23 | ); 24 | 25 | $countries->upsert(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /tests/NoData/CacheTest.php: -------------------------------------------------------------------------------- 1 | toBeFalse(); 19 | 20 | $value = sprintf('Value set at %s', now()->toDateTimeString()); 21 | $valueInitial = scopeCacheTestGet($key, $value); 22 | 23 | expect($valueInitial) 24 | ->toBe($value); 25 | 26 | $valueCached = scopeCacheTestGet($key); 27 | 28 | expect($valueCached) 29 | ->toBe($value); 30 | }); 31 | -------------------------------------------------------------------------------- /database/migrations/2024_09_21_173259_add_experiment_a_records_position.php: -------------------------------------------------------------------------------- 1 | unsignedSmallInteger('position')->default(0)->after('id'); 13 | 14 | $table->index('position'); 15 | }); 16 | } 17 | 18 | public function down(): void 19 | { 20 | Schema::table('experiment_a_records', function (Blueprint $table) { 21 | $table->dropColumn('position'); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations-squashed/2022_11_09_170037_create_countries_table.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->foreignIdFor(Currency::class); 15 | $table->string('code', 2)->unique(); 16 | $table->string('name', 255); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down(): void 22 | { 23 | Schema::dropIfExists('countries'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies = '*'; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /database/seeders/DefaultSeeder.php: -------------------------------------------------------------------------------- 1 | call(DaysSeeder::class); 18 | $this->call(UsersSeeder::class); 19 | $this->call(BlogsSeeder::class); 20 | $this->call(CurrenciesSeeder::class); 21 | $this->call(CountriesSeeder::class); 22 | $this->call(ExchangeRatesSeeder::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/CountryFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CountryFactory extends Factory 13 | { 14 | public function definition() 15 | { 16 | $countryCode = $this->faker->unique()->randomElement(CountryCode::cases()); 17 | $countryCodeDetails = $countryCode->details(); 18 | 19 | return [ 20 | 'currency_id' => $countryCodeDetails['currency_id'] ?? Currency::factory(), 21 | 'code' => $countryCodeDetails['code'], 22 | 'name' => $countryCodeDetails['name'], 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations-squashed/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name'); 14 | $table->string('email')->unique(); 15 | $table->timestamp('email_verified_at')->nullable(); 16 | $table->string('password'); 17 | $table->rememberToken(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('users'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /database/seeders/Models/UsersSeeder.php: -------------------------------------------------------------------------------- 1 | = $maxUsers) { 14 | return; 15 | } 16 | 17 | if (! User::firstWhere('email', 'test@example.com')) { 18 | User::factory()->makeOne([ 19 | 'name' => 'Test User', 20 | 'email' => 'test@example.com', 21 | ])->save(); 22 | } 23 | 24 | /** @var \App\Collections\UserCollection $users */ 25 | $users = User::factory($maxUsers - 1) 26 | ->make(); 27 | 28 | $users->upsert(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | hasMany(Blog::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations-squashed/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('uuid')->unique(); 14 | $table->text('connection'); 15 | $table->text('queue'); 16 | $table->longText('payload'); 17 | $table->longText('exception'); 18 | $table->timestamp('failed_at')->useCurrent(); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('failed_jobs'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->instance(FxConversionModule::class, FxConversionModule::make()); 20 | } 21 | 22 | /** 23 | * Bootstrap any application services. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | if ($this->app->environment('production')) { 30 | URL::forceScheme('https'); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withoutMiddleware( 19 | ThrottleRequests::class 20 | ); 21 | } 22 | 23 | protected function assertJsonResponseFormat(TestResponse $response, array $responseFormat): void 24 | { 25 | $response->assertJson( 26 | function (AssertableJson $json) use ($responseFormat) { 27 | return $json->whereAllType($responseFormat); 28 | } 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Models/ExchangeRate.php: -------------------------------------------------------------------------------- 1 | 'date:Y-m-d', 24 | 'rate' => 'float', 25 | ]; 26 | 27 | public function baseCurrency(): BelongsTo 28 | { 29 | return $this->belongsTo(Currency::class); 30 | } 31 | 32 | public function targetCurrency(): BelongsTo 33 | { 34 | return $this->belongsTo(Currency::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Currency.php: -------------------------------------------------------------------------------- 1 | CurrencyCode::class, 22 | ]; 23 | 24 | public function countries(): HasMany 25 | { 26 | return $this->hasMany(Country::class); 27 | } 28 | 29 | public function exchangeRates(): HasMany 30 | { 31 | return $this->hasMany( 32 | ExchangeRate::class, 33 | $this->code === CurrencyCode::EUR ? 'base_currency_id' : 'target_currency_id' 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations-squashed/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('tokenable'); 14 | $table->string('name'); 15 | $table->string('token', 64)->unique(); 16 | $table->text('abilities')->nullable(); 17 | $table->timestamp('last_used_at')->nullable(); 18 | $table->timestamp('expires_at')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('personal_access_tokens'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | guard($guard)->check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bin/k6.js: -------------------------------------------------------------------------------- 1 | import http from "k6/http"; 2 | import { sleep } from 'k6'; 3 | 4 | /** 5 | * Integrate load tests with CI/CD 6 | * 7 | * Refs 8 | * - https://k6.io/blog/integrating-load-testing-with-circleci/ 9 | * - https://circleci.com/developer/orbs/orb/k6io/test 10 | */ 11 | 12 | const URL = 'http://localhost/api/kata/KataChallengeMySQL/orVersusIn'; 13 | // const URL = 'http://localhost/api/kata/KataChallengeMySQLRecord/getCollectionUnique'; 14 | 15 | export let options = { 16 | vus: 5, 17 | stages: [ 18 | { duration: "10s", target: 10 }, 19 | { duration: "30s", target: 20 }, 20 | { duration: "1m", target: 100 }, 21 | { duration: "30s", target: 0 }, 22 | ] 23 | }; 24 | 25 | export function setup() { 26 | return { 27 | 'token': '123456' 28 | } 29 | } 30 | 31 | export default function (data) { 32 | http.get(URL); 33 | sleep(1); 34 | } 35 | 36 | export function teardown(data) { 37 | // console.log('teardown()'); 38 | } 39 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | public function definition() 15 | { 16 | $safeEmail = sprintf('user-%s@fake.com', uniqid()); 17 | 18 | return [ 19 | 'name' => fake()->name(), 20 | 'email' => $safeEmail, 21 | 'email_verified_at' => Carbon::now(), 22 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 23 | 'remember_token' => Str::random(10), 24 | ]; 25 | } 26 | 27 | public function unverified() 28 | { 29 | return $this->state(fn (array $attributes) => [ 30 | 'email_verified_at' => null, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Data/Models/CountryTest.php: -------------------------------------------------------------------------------- 1 | setExchangeRatesAggregates(); 10 | 11 | expect($country)->toBeInstanceOf(Country::class); 12 | 13 | expect([ 14 | 'avg' => $country->exchangeRatesAvg, 15 | 'sum' => $country->exchangeRatesSum, 16 | 'min' => $country->exchangeRatesMin, 17 | 'max' => $country->exchangeRatesMax, 18 | 'count' => $country->exchangeRatesCount, 19 | ])->toMatchSnapshot(); 20 | })->with([ 21 | CountryCode::ZA, 22 | CountryCode::NL, 23 | ]); 24 | 25 | it('can make model', function () { 26 | $country = Country::factory()->make(); 27 | 28 | expect($country) 29 | ->toBeInstanceOf(Country::class) 30 | ->id->toBeNull(); 31 | }); 32 | -------------------------------------------------------------------------------- /database/factories/ExperimentARecordFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ExperimentARecordFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'position' => $this->faker->unique()->numberBetween(1, 1000), 21 | 'unique_field_1' => $this->faker->unique()->word(), 22 | 'unique_field_2' => $this->faker->unique()->word(), 23 | 'unique_field_3' => $this->faker->unique()->word(), 24 | 'update_field_1' => $this->faker->word(), 25 | 'update_field_2' => $this->faker->word(), 26 | 'update_field_3' => $this->faker->word(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'storage/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /database/migrations/2024_09_16_172608_create_experiment_a_records_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('unique_field_1', 255); 14 | $table->string('unique_field_2', 255); 15 | $table->string('unique_field_3', 255); 16 | $table->string('update_field_1', 255); 17 | $table->string('update_field_2', 255); 18 | $table->string('update_field_3', 255); 19 | $table->timestamps(); 20 | 21 | $table->unique(['unique_field_1', 'unique_field_2', 'unique_field_3'], 'unique_fields'); 22 | }); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('experiment_a1_records'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations-squashed/2022_11_13_112131_create_exchange_rates_table.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->foreignIdFor(Currency::class, 'base_currency_id')->constrained('currencies'); 15 | $table->foreignIdFor(Currency::class, 'target_currency_id')->constrained('currencies'); 16 | $table->string('target_currency_code'); 17 | $table->date('date'); 18 | $table->decimal('rate', 10, 3); 19 | $table->timestamps(); 20 | 21 | $table->unique(['base_currency_id', 'target_currency_id', 'date']); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('exchange_rates'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app/Utilities/CodeUtility.php: -------------------------------------------------------------------------------- 1 | getFileName(); 15 | $startLine = $reflectionMethod->getStartLine() - 1; 16 | $endLine = $reflectionMethod->getEndLine(); 17 | $length = $endLine - $startLine; 18 | $lines = file($fileName); 19 | 20 | $code = collect(array_slice($lines, $startLine, $length)) 21 | ->map(fn ($line) => substr($line, 4)) 22 | ->map(fn ($line) => is_null($limit) ? $line : Str::limit($line, 80)) 23 | ->join(''); 24 | 25 | return $code; 26 | } 27 | 28 | public static function getCodeMd5(ReflectionMethod $reflectionMethod): string 29 | { 30 | $code = self::getCodeSnippet($reflectionMethod); 31 | 32 | return md5($code); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | protected $listen = [ 17 | Registered::class => [ 18 | SendEmailVerificationNotification::class, 19 | ], 20 | ]; 21 | 22 | /** 23 | * Register any events for your application. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | // 30 | } 31 | 32 | /** 33 | * Determine if events and listeners should be automatically discovered. 34 | * 35 | * @return bool 36 | */ 37 | public function shouldDiscoverEvents() 38 | { 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Challenges/A/Laravel.php: -------------------------------------------------------------------------------- 1 | setExchangeRatesAggregates(); 18 | $total += array_sum([ 19 | $country->exchangeRatesAvg, 20 | $country->exchangeRatesSum, 21 | $country->exchangeRatesMin, 22 | $country->exchangeRatesMax, 23 | $country->exchangeRatesCount, 24 | ]); 25 | } 26 | 27 | return $total; 28 | } 29 | 30 | protected function getCountryByIndex(int $iteration): Country 31 | { 32 | $countryCodes = CountryCode::cases(); 33 | $index = $iteration % count($countryCodes); 34 | 35 | return Country::firstWhere('code', $countryCodes[$index]->value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hendrik Prinsloo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/Challenges/B/Sample.php: -------------------------------------------------------------------------------- 1 | 'FizzBuzz', 19 | '|1' => 'Buzz', 20 | '1|' => 'Fizz', 21 | ]; 22 | for ($i = 1; $i <= $iteration; $i++) { 23 | $mod1 = $i % 3 == 0; 24 | $mod2 = $i % 5 == 0; 25 | 26 | $word = $fizzBuzz["$mod1|$mod2"] ?? $i; 27 | $result .= $word.'|'; 28 | } 29 | 30 | return $result; 31 | } 32 | 33 | public function memoryAllocation(int $iteration): string 34 | { 35 | $largeArray = range(1, $iteration); 36 | $resultArray = []; 37 | 38 | foreach ($largeArray as $item) { 39 | $resultArray[] = strrev(str_repeat($item, floor($iteration / 10) + 1)); 40 | } 41 | 42 | return md5(implode('|', $resultArray)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Challenges/B/CleanCodeDatabase.php: -------------------------------------------------------------------------------- 1 | 100 ? 100 : $iteration; 14 | 15 | $sum = 0; 16 | for ($i = 0; $i <= $iteration; $i++) { 17 | $price = 10.99 * $i; 18 | $product = product_create([ 19 | 'name' => 'Test product '.$i, 20 | 'description' => 'Test product description', 21 | 'price' => $price, 22 | ]); 23 | 24 | $sum += $product->price; 25 | } 26 | 27 | return $sum; 28 | } 29 | 30 | public function memoryAllocation(int $iteration): array 31 | { 32 | $largeArray = range(1, $iteration); 33 | $resultArray = []; 34 | 35 | foreach ($largeArray as $item) { 36 | $resultArray[] = strrev(str_repeat($item, 100)); 37 | } 38 | 39 | return $resultArray; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /domain/DirtyCode/products.php: -------------------------------------------------------------------------------- 1 | belongsTo(Currency::class); 24 | } 25 | 26 | public function exchangeRates(): HasMany 27 | { 28 | return $this->currency->exchangeRates(); 29 | } 30 | 31 | public function setExchangeRatesAggregates(): void 32 | { 33 | $this->exchangeRatesAvg = (float) $this->exchangeRates()->avg('rate'); 34 | $this->exchangeRatesSum = (float) $this->exchangeRates()->sum('rate'); 35 | $this->exchangeRatesMin = (float) $this->exchangeRates()->min('rate'); 36 | $this->exchangeRatesMax = (float) $this->exchangeRates()->max('rate'); 37 | $this->exchangeRatesCount = (int) $this->exchangeRates()->count(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Challenges/A/Php.php: -------------------------------------------------------------------------------- 1 | getRangeLimit($iteration); 19 | 20 | $range = []; 21 | for ($i = 0; $i <= $iteration; $i++) { 22 | $range[] = $i; 23 | } 24 | 25 | return count($range); 26 | } 27 | 28 | /** 29 | * Use native functions: array_sum 30 | * 31 | * See https://www.thegeekstuff.com/2014/04/optimize-php-code/ 32 | */ 33 | public function nativeSum(int $iteration): int 34 | { 35 | $numbers = range(0, $this->getRangeLimit($iteration)); 36 | 37 | $total = 0; 38 | foreach ($numbers as $number) { 39 | $total += $number; 40 | } 41 | 42 | return $total; 43 | } 44 | 45 | protected function getRangeLimit(int $iteration): int 46 | { 47 | return $iteration <= self::MAX_RANGE ? $iteration : self::MAX_RANGE; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed to the session on validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->reportable(function (Throwable $e) { 47 | // 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/NoData/Vendor/SebastianBergmann/Diff/CanUseDiff.php: -------------------------------------------------------------------------------- 1 | diff($a, $b); 9 | 10 | expect($diff)->toBe($expected); 11 | })->with([ 12 | 'booleans' => [ 13 | true, 14 | false, 15 | <<<'EXPECTED' 16 | --- A 17 | +++ B 18 | -1 19 | 20 | EXPECTED, 21 | ], 22 | 'strings' => [ 23 | 'line 1', 24 | 'line 2', 25 | <<<'EXPECTED' 26 | --- A 27 | +++ B 28 | -line 1 29 | +line 2 30 | 31 | EXPECTED, 32 | ], 33 | 'arrays-a' => [ 34 | [ 35 | 'line 1', 36 | 'line 2', 37 | ], 38 | [ 39 | 'line 2', 40 | ], 41 | <<<'EXPECTED' 42 | --- A 43 | +++ B 44 | -line 1 45 | 46 | EXPECTED, 47 | ], 48 | 'arrays-b' => [ 49 | [ 50 | 'line 1', 51 | ], 52 | [ 53 | 'line 2', 54 | 'line 3', 55 | ], 56 | <<<'EXPECTED' 57 | --- A 58 | +++ B 59 | -line 1 60 | +line 2 61 | +line 3 62 | 63 | EXPECTED, 64 | ], 65 | ]); 66 | -------------------------------------------------------------------------------- /app/Challenges/B/MySql.php: -------------------------------------------------------------------------------- 1 | $iteration, 24 | ]; 25 | 26 | $rows = $this->select($sql, $params); 27 | 28 | return array_map(fn ($row) => (array) $row, $rows); 29 | } 30 | 31 | public function orVersusInAggregate(int $iteration): float 32 | { 33 | $sql = <<<'SQL' 34 | SELECT 35 | AVG(E.rate) AS `rate` 36 | FROM exchange_rates E 37 | WHERE 38 | E.id <= :limit AND 39 | E.target_currency_code IN ('AED', 'EUR', 'GBP', 'USD', 'ZAR') 40 | SQL; 41 | 42 | $params = [ 43 | 'limit' => $iteration, 44 | ]; 45 | 46 | $value = $this->selectOne($sql, $params)->rate; 47 | 48 | return $value; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | window._ = _; 3 | 4 | /** 5 | * We'll load the axios HTTP library which allows us to easily issue requests 6 | * to our Laravel back-end. This library automatically handles sending the 7 | * CSRF token as a header based on the value of the "XSRF" token cookie. 8 | */ 9 | 10 | import axios from 'axios'; 11 | window.axios = axios; 12 | 13 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 14 | 15 | /** 16 | * Echo exposes an expressive API for subscribing to channels and listening 17 | * for events that are broadcast by Laravel. Echo and event broadcasting 18 | * allows your team to easily build robust real-time web applications. 19 | */ 20 | 21 | // import Echo from 'laravel-echo'; 22 | 23 | // import Pusher from 'pusher-js'; 24 | // window.Pusher = Pusher; 25 | 26 | // window.Echo = new Echo({ 27 | // broadcaster: 'pusher', 28 | // key: import.meta.env.VITE_PUSHER_APP_KEY, 29 | // wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, 30 | // wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 31 | // wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 32 | // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 33 | // enabledTransports: ['ws', 'wss'], 34 | // }); 35 | -------------------------------------------------------------------------------- /app/Challenges/A/silly.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | MAX_ITERATIONS=100 4 | FILE_NAME="Silly.php" 5 | 6 | # Start of the PHP file 7 | echo " $FILE_NAME 8 | echo "" >> $FILE_NAME 9 | echo "namespace App\Challenges\A;" >> $FILE_NAME 10 | echo "" >> $FILE_NAME 11 | echo "use App\KataChallenge;" >> $FILE_NAME 12 | echo "" >> $FILE_NAME 13 | echo "class Silly extends KataChallenge" >> $FILE_NAME 14 | echo "{" >> $FILE_NAME 15 | echo " public const SKIP_VIOLATIONS = true;" >> $FILE_NAME 16 | echo "" >> $FILE_NAME 17 | echo " protected const PHP_MEM_MAX_ITERATIONS = $MAX_ITERATIONS;" >> $FILE_NAME 18 | echo "" >> $FILE_NAME 19 | echo " public function isEven(int \$iteration): bool" >> $FILE_NAME 20 | echo " {" >> $FILE_NAME 21 | echo " \$iteration = (\$iteration > self::PHP_MEM_MAX_ITERATIONS) ? self::PHP_MEM_MAX_ITERATIONS : \$iteration;" >> $FILE_NAME 22 | echo "" >> $FILE_NAME 23 | # Loop to generate the conditions 24 | for (( i=0; i<=$MAX_ITERATIONS; i++ )) 25 | do 26 | if [ $((i % 2)) -eq 0 ]; then 27 | echo " if (\$iteration === $i) return true;" >> $FILE_NAME 28 | else 29 | echo " if (\$iteration === $i) return false;" >> $FILE_NAME 30 | fi 31 | 32 | echo "$i / $MAX_ITERATIONS" 33 | done 34 | 35 | # End of the function 36 | echo " }" >> $FILE_NAME 37 | 38 | # End of the class 39 | echo "}" >> $FILE_NAME 40 | -------------------------------------------------------------------------------- /app/Challenges/B/CleanCode.php: -------------------------------------------------------------------------------- 1 | 'Test product '.$i, 22 | 'description' => 'Test product description', 23 | 'price' => $price, 24 | ]); 25 | 26 | $sum += $product->price; 27 | } 28 | 29 | return $sum; 30 | } 31 | 32 | public function shapes(int $iteration): float 33 | { 34 | $sum = 0; 35 | for ($i = 0; $i <= $iteration; $i++) { 36 | $sum += 37 | circle_area_plus_circumference(5.0 * ($i + 1)) + 38 | square_area_plus_circumference(4.0 * ($i + 1)) + 39 | rectangle_area_plus_circumference(3.0 * ($i + 1), 7.0 * ($i + 1)); 40 | } 41 | 42 | return $sum; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Datasets/challenge-methods.php: -------------------------------------------------------------------------------- 1 | getMethods() as $reflectionMethod) { 34 | if ($reflectionMethod->getModifiers() !== ReflectionMethod::IS_PUBLIC) { 35 | continue; 36 | } 37 | 38 | if ($reflectionMethod->class === KataChallenge::class) { 39 | continue; 40 | } 41 | 42 | $challengeMethods[] = [ 43 | $challengeClass, 44 | $reflectionMethod->name, 45 | ]; 46 | } 47 | } 48 | 49 | dataset('challenge-methods', $challengeMethods); 50 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v5 35 | - name: Setup node 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: '18' 39 | - name: Build artifacts 40 | run: | 41 | npm i 42 | npm run client:build 43 | - name: Upload artifacts 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: './client/dist' 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /domain/CleanCode/Controllers/ProductController.php: -------------------------------------------------------------------------------- 1 | validateProductData($productData); 19 | 20 | return $this->productService->saveProduct($productData); 21 | } 22 | 23 | public function makeProduct(array $productData): ?Product 24 | { 25 | $this->validateProductData($productData); 26 | 27 | return $this->productService->makeProduct($productData); 28 | } 29 | 30 | private function validateProductData(array $productData): void 31 | { 32 | if (empty($productData['name'])) { 33 | throw new InvalidProductDataException('Expected property "name" cannot be empty.'); 34 | } 35 | 36 | if (empty($productData['description'])) { 37 | throw new InvalidProductDataException('Expected property "description" cannot be empty.'); 38 | } 39 | 40 | if (! is_numeric($productData['price'])) { 41 | throw new InvalidProductDataException('Expected property "price" must be numeric.'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /domain/CleanCode/Repositories/Repository.php: -------------------------------------------------------------------------------- 1 | model->query(); 19 | } 20 | 21 | public function all(): Collection 22 | { 23 | return $this->model->all(); 24 | } 25 | 26 | public function find(int $id): Model 27 | { 28 | return $this->model->find($id); 29 | } 30 | 31 | public function findMany(array $ids): Collection 32 | { 33 | return $this->query()->findMany($ids); 34 | } 35 | 36 | public function findOrFail(int $id): Model 37 | { 38 | return $this->query()->findOrFail($id); 39 | } 40 | 41 | public function make(array $data): Model 42 | { 43 | return $this->model->make($data); 44 | } 45 | 46 | public function create(array $data): Model 47 | { 48 | return $this->model->create($data); 49 | } 50 | 51 | public function update(Model $model, array $data): bool 52 | { 53 | return $model->update($data); 54 | } 55 | 56 | public function delete(int|string $id): int 57 | { 58 | return $this->model->destroy($id); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Data/Console/Commands/KataProfileCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('kata:profile Sample calculatePi') 21 | ->expectsConfirmation('Ready to run App\Challenges\A\Sample->calculatePi(1)?', 'Yes') 22 | ->expectsConfirmation('Ready to run App\Challenges\B\Sample->calculatePi(1)?', 'Yes') 23 | ->assertExitCode(Command::SUCCESS); 24 | }); 25 | 26 | it('can run single (no/s)', function () { 27 | $this->artisan('kata:profile Sample calculatePi') 28 | ->expectsConfirmation('Ready to run App\Challenges\A\Sample->calculatePi(1)?', 'No') 29 | ->expectsConfirmation('Ready to run App\Challenges\B\Sample->calculatePi(1)?', 'No') 30 | ->assertExitCode(Command::SUCCESS); 31 | }); 32 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | command->info('Database Seeder'); 20 | $this->command->table([ 21 | 'Variable', 22 | 'Value', 23 | ], [ 24 | [ 25 | 'Dummy data / Max users', 26 | config('laravel-kata.dummy-data.max-users'), 27 | ], 28 | [ 29 | 'Dummy data / Max blogs per user', 30 | config('laravel-kata.dummy-data.max-user-blogs'), 31 | ], 32 | ]); 33 | 34 | if (! Schema::hasTable('users')) { 35 | Artisan::call('migrate:fresh'); 36 | } 37 | 38 | $this->call(DefaultSeeder::class); 39 | 40 | match (app()->environment()) { 41 | 'local' => $this->call(LocalSeeder::class), 42 | 'testing' => $this->call(TestingSeeder::class), 43 | 'staging', 44 | 'production' => $this->call(ProductionSeeder::class), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Challenges/B/Eloquent.php: -------------------------------------------------------------------------------- 1 | avg('id'); 14 | } 15 | 16 | public function getCollectionUnique(int $iteration): string 17 | { 18 | return ExchangeRate::select('id') 19 | ->distinct() 20 | ->where('id', '<=', $iteration) 21 | ->pluck('id') 22 | ->join('|'); 23 | } 24 | 25 | public function getCollectionCount(int $iteration): int 26 | { 27 | return ExchangeRate::where('id', '<=', $iteration)->count(); 28 | } 29 | 30 | public function getCollectionRelatedCount(int $iteration): int 31 | { 32 | return User::where('id', '<=', $iteration) 33 | ->orderByDesc('id') 34 | ->first() 35 | ?->blogs()->count() ?? 0; 36 | } 37 | 38 | public function getMaxVersusOrder(int $iteration): float 39 | { 40 | return ExchangeRate::where('id', '<=', $iteration)->max('rate'); 41 | } 42 | 43 | public function eagerLoading(int $iteration): float 44 | { 45 | return User::with('blogs') 46 | ->where('id', '<=', $iteration) 47 | ->get() 48 | ->reduce(fn (int $total, User $user) => $total + $user->blogs->count(), 0); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Enums/CountryCode.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'code' => $this->value, 23 | 'name' => 'Dubai', 24 | 'currency_id' => Currency::firstWhere('code', CurrencyCode::AED)->id, 25 | ], 26 | self::NL => [ 27 | 'code' => $this->value, 28 | 'name' => 'The Netherlands', 29 | 'currency_id' => Currency::firstWhere('code', CurrencyCode::EUR)->id, 30 | ], 31 | self::UK => [ 32 | 'code' => $this->value, 33 | 'name' => 'United Kingdom', 34 | 'currency_id' => Currency::firstWhere('code', CurrencyCode::GBP)->id, 35 | ], 36 | self::US => [ 37 | 'code' => $this->value, 38 | 'name' => 'United States', 39 | 'currency_id' => Currency::firstWhere('code', CurrencyCode::USD)->id, 40 | ], 41 | self::ZA => [ 42 | 'code' => $this->value, 43 | 'name' => 'South Africa', 44 | 'currency_id' => Currency::firstWhere('code', CurrencyCode::ZAR)->id, 45 | ], 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Console/Commands/KataTestCommand.php: -------------------------------------------------------------------------------- 1 | testConnection()) { 19 | return self::FAILURE; 20 | } 21 | 22 | if (! $this->testConnection('testing')) { 23 | return self::FAILURE; 24 | } 25 | 26 | return self::SUCCESS; 27 | } 28 | 29 | protected function testConnection($connection = 'mysql'): bool 30 | { 31 | $success = true; 32 | $config = config(sprintf('database.connections.%s', $connection)); 33 | try { 34 | /** @var MySqlConnection $connection */ 35 | $connection = DB::connection($connection); 36 | $connection->getPDO(); 37 | $connection->getDatabaseName(); 38 | $database = $connection->getDatabaseName(); 39 | $success = $database === $config['database']; 40 | } catch (Exception $exception) { 41 | $this->warn(sprintf('Database: %s (not connected)', $config['database'])); 42 | $this->warn($exception->getMessage()); 43 | $success = false; 44 | } 45 | 46 | $this->info(sprintf('Database: %s (connected)', $config['database'])); 47 | 48 | return $success; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /routes/mock.php: -------------------------------------------------------------------------------- 1 | get('start_date'); 11 | $endDate = $request->get('end_date'); 12 | $source = $request->get('source'); 13 | $currencyCodes = explode(',', $request->get('currencies')); 14 | 15 | $sourceCurrency = Currency::where('code', $source)->firstOrFail(); 16 | $targetCurrencies = Currency::whereIn('code', $currencyCodes)->get(); 17 | 18 | $quotes = []; 19 | $carbonPeriod = CarbonPeriod::create($startDate, $endDate); 20 | foreach ($carbonPeriod as $date) { 21 | $day = $date->format('Y-m-d'); 22 | $quotes[$day] = []; 23 | foreach ($targetCurrencies as $targetCurrency) { 24 | $key = sprintf('%s%s', $sourceCurrency->code->value, $targetCurrency->code->value); 25 | 26 | $rate = $date->timestamp / ($sourceCurrency->id / $targetCurrency->id); 27 | $quotes[$day][$key] = round($rate / 3000000000, 10); 28 | } 29 | } 30 | 31 | return JsonResource::make([ 32 | 'success' => true, 33 | 'terms' => 'https://localhost/mock/exchangerate/terms', 34 | 'privacy' => 'https://localhost/mock/exchangerate/privacy', 35 | 'timeframe' => true, 36 | 'start_date' => $startDate, 37 | 'end_date' => $endDate, 38 | 'source' => $source, 39 | 'quotes' => $quotes, 40 | ]); 41 | }); 42 | -------------------------------------------------------------------------------- /app/Challenges/A/CleanCode.php: -------------------------------------------------------------------------------- 1 | make(ProductController::class); 18 | 19 | $sum = 0; 20 | for ($i = 0; $i <= $iteration; $i++) { 21 | $price = 10.99 * $i; 22 | $product = $productController->makeProduct([ 23 | 'name' => 'Test product '.$i, 24 | 'description' => 'Test product description', 25 | 'price' => $price, 26 | ]); 27 | 28 | $sum += $product->price; 29 | } 30 | 31 | return $sum; 32 | } 33 | 34 | public function shapes(int $iteration): float 35 | { 36 | $sum = 0; 37 | for ($i = 0; $i <= $iteration; $i++) { 38 | $shapeCircle = new ShapeCircle(5.0 * ($i + 1)); 39 | $shapeSquare = new ShapeSquare(4.0 * ($i + 1)); 40 | $shapeRectangle = new ShapeRectangle(3.0 * ($i + 1), 7.0 * ($i + 1)); 41 | 42 | $sum += 43 | $shapeCircle->areaPlusCircumference() + 44 | $shapeSquare->areaPlusCircumference() + 45 | $shapeRectangle->areaPlusCircumference(); 46 | } 47 | 48 | return $sum; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Challenges/A/CleanCodeDatabase.php: -------------------------------------------------------------------------------- 1 | 100 ? 100 : $iteration; 21 | 22 | /** @var \Domain\CleanCode\Controllers\ProductController $productController */ 23 | $productController = app()->make(ProductController::class); 24 | 25 | $sum = 0; 26 | for ($i = 0; $i <= $iteration; $i++) { 27 | $price = 10.99 * $i; 28 | $product = $productController->createProduct([ 29 | 'name' => 'Test product '.$i, 30 | 'description' => 'Test product description', 31 | 'price' => $price, 32 | ]); 33 | 34 | $sum += $product->price; 35 | } 36 | 37 | return $sum; 38 | } 39 | 40 | public function memoryAllocation(int $iteration): array 41 | { 42 | $largeArray = range(1, $iteration); 43 | $tempArray = []; 44 | 45 | foreach ($largeArray as $item) { 46 | $tempArray[] = str_repeat($item, 100); 47 | } 48 | 49 | $resultArray = []; 50 | foreach ($tempArray as $item) { 51 | $resultArray[] = strrev($item); 52 | } 53 | 54 | return $resultArray; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | ./tests/NoData 12 | 13 | 14 | ./tests/Data 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ./app 37 | 38 | 39 | ./app/Providers 40 | ./app/Http/Middleware 41 | app/Console/Kernel.php 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('mock') 37 | ->prefix('mock') 38 | ->group(base_path('routes/mock.php')); 39 | 40 | Route::middleware('web') 41 | ->group(base_path('routes/web.php')); 42 | }); 43 | } 44 | 45 | /** 46 | * Configure the rate limiters for the application. 47 | * 48 | * @return void 49 | */ 50 | protected function configureRateLimiting() 51 | { 52 | RateLimiter::for('api', function (Request $request) { 53 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /config/tinker.php: -------------------------------------------------------------------------------- 1 | [ 17 | // App\Console\Commands\ExampleCommand::class, 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Auto Aliased Classes 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Tinker will not automatically alias classes in your vendor namespaces 26 | | but you may explicitly allow a subset of classes to get aliased by 27 | | adding the names of each of those classes to the following list. 28 | | 29 | */ 30 | 31 | 'alias' => [ 32 | // 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Classes That Should Not Be Aliased 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Typically, Tinker automatically aliases classes as you require them in 41 | | Tinker. However, you may wish to never alias certain classes, which 42 | | you may accomplish by listing the classes in the following array. 43 | | 44 | */ 45 | 46 | 'dont_alias' => [ 47 | 'App\Nova', 48 | ], 49 | 50 | ]; 51 | -------------------------------------------------------------------------------- /app/KataChallenge.php: -------------------------------------------------------------------------------- 1 | maxSeconds = $this->request?->get('max-seconds') ?? config( 25 | 'laravel-kata.max-seconds', 26 | $this->maxSeconds 27 | ); 28 | 29 | $this->maxIterations = $this->request?->get('max-iterations') 30 | ?? config('laravel-kata.max-iterations', $this->maxIterations); 31 | 32 | if (! is_null(static::MAX_INTERATIONS) && $this->maxIterations > static::MAX_INTERATIONS) { 33 | $this->maxIterations = static::MAX_INTERATIONS; 34 | } 35 | 36 | $this->setUp(); 37 | } 38 | 39 | public function getMaxSeconds(): int 40 | { 41 | return $this->maxSeconds; 42 | } 43 | 44 | public function getMaxIterations(): int 45 | { 46 | return $this->maxIterations; 47 | } 48 | 49 | protected function setUp(): void 50 | { 51 | foreach (static::EXPECTED_MODELS as $expectedModelClass) { 52 | if ($expectedModelClass::count() === 0) { 53 | throw new KataChallengeException(sprintf( 54 | 'Expected records in %s, but found none', 55 | $expectedModelClass 56 | )); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Console/Commands/KataProfileCommand.php: -------------------------------------------------------------------------------- 1 | argument('challenge'); 19 | $method = $this->argument('method'); 20 | $iteration = (int) $this->argument('iteration'); 21 | 22 | $challengeA = str_replace('Sample', $challenge, Sample::class); 23 | $challengeB = str_replace('\\A\\', '\\B\\', $challengeA); 24 | 25 | $instanceA = app()->make($challengeA); 26 | $instanceB = app()->make($challengeB); 27 | 28 | if (confirm(sprintf('Ready to run %s->%s(%d)?', $challengeA, $method, $iteration))) { 29 | $responseA = $instanceA->{$method}($iteration); 30 | 31 | $this->info('Response of A'); 32 | $this->comment(json_encode($responseA, JSON_PRETTY_PRINT)); 33 | } else { 34 | $this->error(sprintf('Skipped %s->%s(%d)', $challengeA, $method, $iteration)); 35 | } 36 | 37 | if (confirm(sprintf('Ready to run %s->%s(%d)?', $challengeB, $method, $iteration))) { 38 | $responseB = $instanceB->{$method}($iteration); 39 | 40 | $this->info('Response of B'); 41 | $this->comment(json_encode($responseB, JSON_PRETTY_PRINT)); 42 | } else { 43 | $this->error(sprintf('Skipped %s->%s(%d)', $challengeB, $method, $iteration)); 44 | } 45 | 46 | return self::SUCCESS; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /tests/NoData/Utilities/Files/memory.xt: -------------------------------------------------------------------------------- 1 | Version: 3.2.1 2 | File format: 4 3 | TRACE START [2023-09-17 18:55:32.313909] 4 | 22 438545 0 1.607350 25400640 App\KataRunner->App\{closure:/var/www/html/app/KataRunner.php:659-659} 1 /var/www/html/app/Utilities/Benchmark.php 106 0 5 | 23 438546 0 1.607359 25400640 App\Challenges\A\Sample->memoryAllocation 1 /var/www/html/app/KataRunner.php 659 1 3 6 | 24 438547 0 1.607367 25400640 range 0 /var/www/html/app/Challenges/A/Sample.php 68 2 1 3 7 | 24 438547 1 1.607372 25400920 8 | 24 438548 0 1.607374 25400856 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 1 100 9 | 24 438548 1 1.607377 25401080 10 | 24 438549 0 1.607379 25401232 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 2 100 11 | 24 438549 1 1.607381 25401456 12 | 24 438550 0 1.607382 25401392 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 3 100 13 | 24 438550 1 1.607384 25401616 14 | 24 438551 0 1.607385 25401552 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' 15 | 24 438551 1 1.607389 25401712 16 | 24 438552 0 1.607390 25401896 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222' 17 | 24 438552 1 1.607392 25402056 18 | 24 438553 0 1.607393 25402024 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333' 19 | 24 438553 1 1.607395 25402184 20 | 23 438546 1 1.607399 25401240 21 | 22 438545 1 1.607403 25400640 22 | 22 438554 0 1.607404 25400640 xdebug_stop_trace 0 /var/www/html/app/Utilities/Benchmark.php 108 0 23 | 1.607406 25400688 24 | TRACE END [2023-09-17 18:55:32.314016] 25 | 26 | -------------------------------------------------------------------------------- /tests/NoData/Utilities/Files/sample.xt: -------------------------------------------------------------------------------- 1 | Version: 3.2.1 2 | File format: 4 3 | TRACE START [2023-09-17 18:08:26.147880] 4 | 22 442274 0 1.724884 25397288 App\KataRunner->App\{closure:/var/www/html/app/KataRunner.php:659-659} 1 /var/www/html/app/Utilities/Benchmark.php 77 0 5 | 23 442275 0 1.724897 25397288 App\Challenges\A\Sample->memoryAllocation 1 /var/www/html/app/KataRunner.php 659 1 3 6 | 24 442276 0 1.724911 25397288 range 0 /var/www/html/app/Challenges/A/Sample.php 68 2 1 3 7 | 24 442276 1 1.724916 25397568 8 | 24 442277 0 1.724917 25397504 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 1 100 9 | 24 442277 1 1.724921 25397728 10 | 24 442278 0 1.724922 25397880 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 2 100 11 | 24 442278 1 1.724924 25398104 12 | 24 442279 0 1.724925 25398040 str_repeat 0 /var/www/html/app/Challenges/A/Sample.php 72 2 3 100 13 | 24 442279 1 1.724927 25398264 14 | 24 442280 0 1.724928 25398200 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' 15 | 24 442280 1 1.724932 25398360 16 | 24 442281 0 1.724933 25398544 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222' 17 | 24 442281 1 1.724935 25398704 18 | 24 442282 0 1.724936 25398672 strrev 0 /var/www/html/app/Challenges/A/Sample.php 77 1 '3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333' 19 | 24 442282 1 1.724937 25398832 20 | 23 442275 1 1.724941 25397888 21 | 22 442274 1 1.724944 25397288 22 | 22 442283 0 1.724946 25397288 xdebug_stop_trace 0 /var/www/html/app/Utilities/Benchmark.php 79 0 23 | 1.724947 25397336 24 | TRACE END [2023-09-17 18:08:26.148000] 25 | 26 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /app/Challenges/A/Eloquent.php: -------------------------------------------------------------------------------- 1 | get() 22 | ->average('id'); 23 | } 24 | 25 | public function getCollectionUnique(int $iteration): string 26 | { 27 | return ExchangeRate::query() 28 | ->where('id', '<=', $iteration) 29 | ->get() 30 | ->pluck('id') 31 | ->unique() 32 | ->join('|'); 33 | } 34 | 35 | public function getCollectionCount(int $iteration): int 36 | { 37 | return ExchangeRate::query() 38 | ->where('id', '<=', $iteration) 39 | ->get() 40 | ->count(); 41 | } 42 | 43 | public function getCollectionRelatedCount(int $iteration): int 44 | { 45 | return User::all() 46 | ->where('id', '<=', $iteration) 47 | ->last() 48 | ?->blogs->count() ?? 0; 49 | } 50 | 51 | public function getMaxVersusOrder(int $iteration): float 52 | { 53 | return ExchangeRate::query() 54 | ->where('id', '<=', $iteration) 55 | ->orderByDesc('rate') 56 | ->first()?->rate; 57 | } 58 | 59 | public function eagerLoading(int $iteration): float 60 | { 61 | $value = 0; 62 | 63 | /** @var User $user */ 64 | foreach (User::where('id', '<=', $iteration)->get() as $user) { 65 | $value += $user->blogs()->count(); 66 | } 67 | 68 | return $value; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Challenges/A/Sample.php: -------------------------------------------------------------------------------- 1 | environment('testing')) { 20 | Carbon::setTestNow(Carbon::parse(self::SNAPSHOT_DATE)); 21 | $this->loadFxCache(); 22 | $this->exchangeRateService->syncExchangeRates(1); 23 | Carbon::setTestNow(); 24 | 25 | return; 26 | } 27 | 28 | $this->exchangeRateService->syncExchangeRates(); 29 | } 30 | 31 | protected function loadFxCache(): void 32 | { 33 | $pattern = sprintf('%s/Files/fx_*.json', __DIR__); 34 | 35 | foreach (glob($pattern) as $path) { 36 | $key = str_replace( 37 | ['_', '.json'], 38 | [':', ''], 39 | basename($path) 40 | ); 41 | Cache::set($key, json_decode(file_get_contents($path), true)); 42 | } 43 | } 44 | 45 | protected function saveFxCache(): void 46 | { 47 | $pattern = sprintf('%s/Files/fx_*.json', __DIR__); 48 | foreach (glob($pattern) as $path) { 49 | unlink($path); 50 | } 51 | 52 | $redis = Cache::connection('cache'); 53 | $keys = $redis->keys('cache:fx:*'); 54 | foreach ($keys as $key) { 55 | $key = str_replace('default_cache:', '', $key); 56 | $path = sprintf( 57 | '%s/Files/%s.json', 58 | __DIR__, 59 | str_replace(':', '_', $key) 60 | ); 61 | 62 | file_put_contents($path, json_encode(Cache::get($key))); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/larawell/inspectation/src/Inspectation.php: -------------------------------------------------------------------------------- 1 | 25 | * @mixin PendingArchExpectation 26 | */ 27 | final class Inspectation 28 | { 29 | public Expectation $expectation; 30 | 31 | /** 32 | * Creates a new inspectation. 33 | * 34 | * @param TValue $value 35 | */ 36 | public function __construct(public mixed $value) 37 | { 38 | $this->expectation = expect($this->value); 39 | } 40 | 41 | /** 42 | * Dynamically calls methods on the class or creates a new higher order expectation. 43 | * 44 | * @param array $parameters 45 | * @return Expectation|HigherOrderExpectation, TValue> 46 | */ 47 | public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation 48 | { 49 | return $this->expectation->__call($method, $parameters); 50 | } 51 | 52 | /** 53 | * Dynamically calls methods on the class without any arguments or creates a new higher order expectation. 54 | * 55 | * @return Expectation|OppositeExpectation|EachExpectation|HigherOrderExpectation, TValue|null>|TValue 56 | */ 57 | public function __get(string $name) 58 | { 59 | return $this->expectation->__get($name); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Data/Feature/KataFeatureTest.php: -------------------------------------------------------------------------------- 1 | get('/')->assertStatus(200); 21 | $this->assertTrue(true); 22 | }); 23 | 24 | test('get challenges', function () { 25 | $response = $this->get('/api/kata') 26 | ->assertStatus(200); 27 | 28 | $this->assertJsonResponseFormat($response, [ 29 | 'success' => 'boolean', 30 | 'data' => 'array', 31 | 'data.0' => 'string', 32 | ]); 33 | 34 | return array_values($response->json('data')); 35 | }); 36 | 37 | test('get challenge methods', function (array $challenges) { 38 | $challengeMethods = []; 39 | 40 | foreach ($challenges as $challenge) { 41 | $response = $this->get(sprintf('/api/kata/%s', $challenge)) 42 | ->assertStatus(200); 43 | 44 | $this->assertJsonResponseFormat($response, [ 45 | 'success' => 'boolean', 46 | 'data' => 'array', 47 | 'data.0' => 'string', 48 | ]); 49 | 50 | $challengeMethods[$challenge] = $response->json('data'); 51 | } 52 | 53 | return $challengeMethods; 54 | })->depends('get challenges'); 55 | 56 | test('run challenge', function () { 57 | $response = $this->get('/api/kata/Sample/run', [ 58 | 'iterations' => 1, 59 | ])->assertStatus(200); 60 | 61 | $this->assertJsonResponseFormat($response, [ 62 | 'success' => 'boolean', 63 | 'data' => 'array', 64 | 'data.report' => 'array', 65 | ]); 66 | }); 67 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Laravel Kata" 2 | APP_ENV=local 3 | APP_KEY=base64:kjyMYAaiixiQGLQFI04b/JIwqZ8Wje4jTg9cpick9ms= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | # Kata variables (see config) 12 | # CI_MODE=local 13 | # LK_RUN_MODE=debug 14 | 15 | # Xdebug 16 | SAIL_XDEBUG_MODE=profile,trace 17 | SAIL_XDEBUG_CONFIG="client_host=host.docker.internal output_dir=/var/www/html/storage/logs/xdebug" 18 | 19 | # Database / Main 20 | DB_CONNECTION=mysql 21 | DB_HOST=127.0.0.1 22 | DB_PORT=3306 23 | DB_DATABASE=laravel 24 | DB_USERNAME=sail 25 | DB_PASSWORD=password 26 | DB_ROOT_PASSWORD=password 27 | 28 | # Database / Testing 29 | DB_TEST_DATABASE=testing 30 | DB_TEST_USERNAME=root 31 | DB_TEST_PASSWORD=password 32 | 33 | BROADCAST_DRIVER=log 34 | CACHE_DRIVER=file 35 | FILESYSTEM_DISK=local 36 | QUEUE_CONNECTION=sync 37 | SESSION_DRIVER=file 38 | SESSION_LIFETIME=120 39 | 40 | MEMCACHED_HOST=127.0.0.1 41 | 42 | REDIS_HOST=127.0.0.1 43 | REDIS_PASSWORD=null 44 | REDIS_PORT=6379 45 | 46 | MAIL_MAILER=smtp 47 | MAIL_HOST=mailhog 48 | MAIL_PORT=1025 49 | MAIL_USERNAME=null 50 | MAIL_PASSWORD=null 51 | MAIL_ENCRYPTION=null 52 | MAIL_FROM_ADDRESS="hello@example.com" 53 | MAIL_FROM_NAME="${APP_NAME}" 54 | 55 | AWS_ACCESS_KEY_ID= 56 | AWS_SECRET_ACCESS_KEY= 57 | AWS_DEFAULT_REGION=us-east-1 58 | AWS_BUCKET= 59 | AWS_USE_PATH_STYLE_ENDPOINT=false 60 | 61 | PUSHER_APP_ID= 62 | PUSHER_APP_KEY= 63 | PUSHER_APP_SECRET= 64 | PUSHER_HOST= 65 | PUSHER_PORT=443 66 | PUSHER_SCHEME=https 67 | PUSHER_APP_CLUSTER=mt1 68 | 69 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 70 | VITE_PUSHER_HOST="${PUSHER_HOST}" 71 | VITE_PUSHER_PORT="${PUSHER_PORT}" 72 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 73 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 74 | 75 | # Clockwork 76 | CLOCKWORK_ENABLE=false 77 | CLOCKWORK_CACHE_ENABLED=true 78 | CLOCKWORK_ARTISAN_COLLECT=true 79 | CLOCKWORK_DATABASE_ENABLED=true 80 | CLOCKWORK_DATABASE_COLLECT_QUERIES=true 81 | CLOCKWORK_SERIALIZATION_DEPTH=1 82 | 83 | CODECOV_TOKEN= 84 | 85 | # Exchange Rate API 86 | EXCHANGE_RATE_API_HOST=http://127.0.0.1:8000/mock/exchangerate 87 | EXCHANGE_RATE_API_KEY=fake-key 88 | -------------------------------------------------------------------------------- /tests/NoData/HelpersTest.php: -------------------------------------------------------------------------------- 1 | getMethod('getCodeSnippet'); 8 | 9 | $path = help_me_code($reflectionMethod); 10 | 11 | expect($path) 12 | ->toEndWith('/Utilities/CodeUtility.php:10'); 13 | }); 14 | 15 | it('can get array subset by keys', function () { 16 | $array = [ 17 | 'key-1' => 1, 18 | 'key-2' => 2, 19 | 'key-3' => 3, 20 | 'key-4' => 4, 21 | ]; 22 | 23 | $subset = array_subset_by_keys($array, ['key-2', 'key-3']); 24 | 25 | expect($subset)->toBe([ 26 | 'key-2' => 2, 27 | 'key-3' => 3, 28 | ]); 29 | }); 30 | 31 | it('can wrap in format', function ( 32 | string $string, 33 | bool $success, 34 | bool $warn, 35 | string $expected 36 | ) { 37 | $string = wrap_in_format($string, $success, $warn); 38 | 39 | expect($string)->toBe($expected); 40 | })->with([ 41 | ['is true', true, false, 'is true'], 42 | ['is false', false, false, 'is false'], 43 | ['is true (warn)', true, true, 'is true (warn)'], 44 | ['is false (warn)', false, true, 'is false (warn)'], 45 | ]); 46 | 47 | it('can convert bytes to human', function (float $bytes) { 48 | $human = bytes_to_human($bytes); 49 | 50 | expect($human)->toMatchSnapshot(); 51 | })->with([ 52 | 0, 53 | 0.1, 54 | 1, 55 | 2, 56 | 9999999999999, 57 | ]); 58 | 59 | it('can convert time to human (digital)', function (float $bytes) { 60 | $human = time_to_human($bytes, digital: true); 61 | 62 | expect($human)->toMatchSnapshot(); 63 | })->with([ 64 | 0, 65 | 0.1, 66 | 1, 67 | 2, 68 | 9999999999999, 69 | ]); 70 | 71 | it('can convert time to human (false)', function (float $bytes) { 72 | $human = time_to_human($bytes, digital: false); 73 | 74 | expect($human)->toMatchSnapshot(); 75 | })->with([ 76 | 0, 77 | 0.1, 78 | 1, 79 | 2, 80 | 99999999, 81 | ]); 82 | 83 | it('will not redefine functions', function () { 84 | include 'app/helpers.php'; 85 | 86 | expect(time_to_human(0))->toBe('0.000000000 s'); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/Data/LaravelPlus/Collections/SmartCollectionTest.php: -------------------------------------------------------------------------------- 1 | make(); 10 | $users->map(function (User $user, int $index) { 11 | $user->email = sprintf('user-%d@test.com', $index); 12 | 13 | return $user; 14 | }); 15 | 16 | return $users; 17 | } 18 | 19 | it('can upsert new records', function (int $count) { 20 | $users = scopeSmartCollectionTestMakeUsers($count); 21 | $success = $users->upsert(); 22 | expect($success)->toBeTrue(); 23 | 24 | $userCount = User::where('email', 'LIKE', 'user-%@test.com')->count(); 25 | expect($userCount)->toBe($count); 26 | 27 | User::where('email', 'LIKE', 'user-%@test.com')->delete(); 28 | })->with([ 29 | 1, 100, 1000, 30 | ]); 31 | 32 | it('can upsert existing records', function (int $count) { 33 | $users = scopeSmartCollectionTestMakeUsers($count); 34 | $success = $users->upsert(); 35 | expect($success)->toBeTrue(); 36 | 37 | $userCount = User::where('email', 'LIKE', 'user-%@test.com')->count(); 38 | expect($userCount)->toBe($count); 39 | 40 | /** @var App\Collections\UserCollection $newUsers */ 41 | $newUsers = User::where('email', 'LIKE', 'user-%@test.com')->get(); 42 | 43 | // Update email address for all new 44 | $newUsers->map(function (User $user, int $index) { 45 | $user->email = sprintf('user-new-%d@test.com', $index); 46 | 47 | return $user; 48 | }); 49 | 50 | $newUsers->upsert(); 51 | 52 | // Only new users 53 | $userCount = User::where('email', 'LIKE', 'user-new-%@test.com')->count(); 54 | expect($userCount)->toBe($userCount); 55 | 56 | User::where('email', 'LIKE', 'user-new-%@test.com')->delete(); 57 | })->with([ 58 | 1, 100, 1000, 59 | ]); 60 | 61 | it('can delete records', function (int $count) { 62 | $users = scopeSmartCollectionTestMakeUsers($count); 63 | $success = $users->upsert(); 64 | expect($success)->toBeTrue(); 65 | 66 | $users = User::where('email', 'LIKE', 'user-%@test.com')->get(); 67 | $users->delete(); 68 | 69 | $userCount = User::where('email', 'LIKE', 'user-%@test.com')->count(); 70 | expect($userCount)->toBe(0); 71 | })->with([ 72 | 1, 100, 1000, 73 | ]); 74 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 40 | 'port' => env('PUSHER_PORT', 443), 41 | 'scheme' => env('PUSHER_SCHEME', 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env('ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /app/Challenges/A/MySql.php: -------------------------------------------------------------------------------- 1 | $iteration, 32 | ]; 33 | 34 | $rows = $this->select($sql, $params); 35 | 36 | return array_map(fn ($row) => (array) $row, $rows); 37 | } 38 | 39 | public function orVersusInAggregate(int $iteration): float 40 | { 41 | $sql = <<<'SQL' 42 | SELECT 43 | AVG(E.rate) AS `rate` 44 | FROM exchange_rates E 45 | WHERE 46 | E.id <= :limit AND 47 | ( 48 | E.target_currency_code = 'AED' OR 49 | E.target_currency_code = 'EUR' OR 50 | E.target_currency_code = 'GBP' OR 51 | E.target_currency_code = 'USD' OR 52 | E.target_currency_code = 'ZAR' 53 | ) 54 | SQL; 55 | 56 | $params = [ 57 | 'limit' => $iteration, 58 | ]; 59 | 60 | $value = $this->selectOne($sql, $params)->rate; 61 | 62 | return $value; 63 | } 64 | 65 | protected function selectOne(string $sql, array $params = []): mixed 66 | { 67 | $sql = Str::replaceFirst('SELECT', 'SELECT SQL_NO_CACHE', $sql); 68 | $expression = DB::raw($sql); 69 | $parsedSql = $expression->getValue(DB::connection()->getQueryGrammar()); 70 | 71 | return DB::selectOne($parsedSql, $params); 72 | } 73 | 74 | protected function select(string $sql, array $params = []): array 75 | { 76 | $sql = Str::replaceFirst('SELECT', 'SELECT SQL_NO_CACHE', $sql); 77 | $expression = DB::raw($sql); 78 | $parsedSql = $expression->getValue(DB::connection()->getQueryGrammar()); 79 | 80 | return DB::select($parsedSql, $params); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/Traits/HasExitHintsTrait.php: -------------------------------------------------------------------------------- 1 | showHints) { 19 | return; 20 | } 21 | 22 | if (! isset($this->command) || is_null($this->command)) { 23 | return; 24 | } 25 | 26 | if (is_null($this->exitHints) || empty($this->exitHints)) { 27 | $this->command->info('No hints found!'); 28 | 29 | return; 30 | } 31 | 32 | $this->command->warn($this->getRandomExitHint()); 33 | } 34 | 35 | protected function getExitHints(): Collection 36 | { 37 | return $this->exitHints; 38 | } 39 | 40 | protected function getRandomExitHint(): string 41 | { 42 | return (string) $this->exitHints->random(1)->first(); 43 | } 44 | 45 | protected function addExitHintsFromViolations(array $violations): void 46 | { 47 | if (is_null($this->showHints)) { 48 | $this->showHints = Config::get('laravel-kata.show-hints', false); 49 | } 50 | 51 | if (is_null($this->showHintsExtended)) { 52 | $this->showHintsExtended = Config::get('laravel-kata.show-hints-extended', false); 53 | } 54 | 55 | $hintKeys = [ 56 | 'class', 57 | 'method', 58 | 'function', 59 | 'beginLine', 60 | 'endLine', 61 | ]; 62 | 63 | foreach ($violations as $violation) { 64 | $hint = $this->showHintsExtended 65 | ? sprintf('(i) %s', json_encode(array_subset_by_keys($violation, $hintKeys))) 66 | : '(i) Check config to show extended hints'; 67 | 68 | $message = sprintf( 69 | "### %s (%s)\n%s\n\n%s\n\n%s", 70 | $violation['ruleSet'], 71 | $violation['rule'], 72 | $violation['description'], 73 | $violation['externalInfoUrl'] === '#' ? '' : $violation['externalInfoUrl'], 74 | $hint 75 | ); 76 | 77 | $this->addExitHint($message); 78 | } 79 | } 80 | 81 | private function addExitHint(string $message): void 82 | { 83 | if (is_null($this->exitHints)) { 84 | $this->exitHints = collect(); 85 | } 86 | 87 | $this->exitHints->push($message); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/helpers.php: -------------------------------------------------------------------------------- 1 | getFileName()), 12 | $reflectionMethod->getStartLine() 13 | ); 14 | } 15 | } 16 | 17 | if (! function_exists('array_subset_by_keys')) { 18 | /** 19 | * Returns a subset of the array by keys 20 | */ 21 | function array_subset_by_keys(array $array, array $keys): array 22 | { 23 | $return = []; 24 | 25 | foreach ($keys as $key) { 26 | $return[$key] = $array[$key] ?? null; 27 | } 28 | 29 | return $return; 30 | } 31 | } 32 | 33 | if (! function_exists('wrap_in_format')) { 34 | /** 35 | * Wrap in format for CLI 36 | */ 37 | function wrap_in_format(string $string, bool $success, bool $warn = false): string 38 | { 39 | return $success 40 | ? sprintf('%s', $string) 41 | : sprintf('%s', $warn ? 'yellow' : 'red', $string); 42 | } 43 | } 44 | 45 | if (! function_exists('bytes_to_human')) { 46 | function bytes_to_human(float $bytes): string 47 | { 48 | $bytes = intval($bytes); 49 | $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 50 | $factor = floor((strlen($bytes) - 1) / 3); 51 | 52 | return sprintf( 53 | '%s %s', 54 | round($bytes / (1024 ** $factor)), 55 | $units[$factor] 56 | ); 57 | } 58 | } 59 | 60 | if (! function_exists('time_to_human')) { 61 | function time_to_human(float $milliseconds, bool $digital = true): string 62 | { 63 | return $digital ? ms_to_time($milliseconds) : sprintf('%s s', number_format($milliseconds, 9)); 64 | } 65 | } 66 | 67 | if (! function_exists('ms_to_time')) { 68 | function ms_to_time(float $milliseconds) 69 | { 70 | $seconds = floor($milliseconds / 1000); 71 | $minutes = floor($seconds / 60); 72 | $hours = floor($minutes / 60); 73 | 74 | $minutes = $minutes - ($hours * 60); 75 | $seconds = $seconds - ($hours * 60 * 60) - ($minutes * 60); 76 | $ms = $milliseconds % 1000; 77 | 78 | $timeFormat = sprintf('%02d:%02d:%02d.%03d', $hours, $minutes, $seconds, $ms); 79 | if ($timeFormat === '00:00:00.000') { 80 | return time_to_human($milliseconds, false); 81 | } 82 | 83 | return $timeFormat; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. If this value is null, personal access tokens do 45 | | not expire. This won't tweak the lifetime of first-party sessions. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Sanctum Middleware 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When authenticating your first-party SPA with Sanctum you may need to 57 | | customize some of the middleware Sanctum uses while processing the 58 | | request. You may change the middleware listed below as required. 59 | | 60 | */ 61 | 'middleware' => [ 62 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 63 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 64 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /app/Console/Commands/KataRunCommand.php: -------------------------------------------------------------------------------- 1 | option('all')) { 23 | return $this->handleRun(); 24 | } 25 | 26 | $challenges = $this->option('challenge'); 27 | if (count($challenges) > 0) { 28 | $challenges = collect($challenges)->map( 29 | fn (string $challenge) => sprintf('App\\Challenges\\A\\%s', $challenge) 30 | )->toArray(); 31 | 32 | return $this->handleRun($challenges); 33 | } 34 | 35 | $classNames = collect(config('laravel-kata.challenges'))->map( 36 | fn ($namespace) => str_replace('App\\Challenges\\A\\', '', $namespace) 37 | )->toArray(); 38 | 39 | $classNames = $this->smartChoice('Challenges', $classNames); 40 | 41 | $challenges = collect($classNames)->map( 42 | fn ($className) => sprintf('App\\Challenges\\A\\%s', $className) 43 | )->toArray(); 44 | 45 | return $this->handleRun($challenges); 46 | } 47 | 48 | protected function handleRun(array $challenges = []): int 49 | { 50 | $configChallenges = config('laravel-kata.challenges'); 51 | 52 | $challenges = ! empty($challenges) ? $challenges : $configChallenges; 53 | $methods = $this->option('method'); 54 | 55 | $this->kataRunner = app()->makeWith(KataRunner::class, [ 56 | 'command' => $this, 57 | 'challenges' => $challenges, 58 | 'methods' => $methods, 59 | ]); 60 | 61 | try { 62 | $this->kataRunner->run(); 63 | } catch (KataChallengeScoreException $exception) { 64 | if (app()->runningUnitTests()) { 65 | throw $exception; 66 | } 67 | 68 | $warning = sprintf('%s', $exception->getMessage()); 69 | if (in_array($exception::class, config('laravel-kata.ignore-exceptions'))) { 70 | $this->warn($warning); 71 | 72 | return self::SUCCESS; 73 | } 74 | 75 | $this->error($warning); 76 | 77 | return self::FAILURE; 78 | } 79 | 80 | return self::SUCCESS; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/larawell/laravel-plus/src/Console/Commands/Traits/SmartChoice.php: -------------------------------------------------------------------------------- 1 | $choiceValue) { 21 | $newChoices[sprintf('#%s', $choiceKey)] = $choiceValue; 22 | } 23 | 24 | $choices = $this->_multiChoice($question, $newChoices, $default); 25 | 26 | Cache::set($key, $choices); 27 | 28 | $newChoices = []; 29 | foreach ($choices as $choiceKey => $choiceValue) { 30 | $newChoices[substr($choiceKey, 1)] = $choiceValue; 31 | } 32 | 33 | return $newChoices; 34 | } 35 | 36 | private function _multiChoice( 37 | string $question, 38 | array $choices, 39 | array $chosen 40 | ): array { 41 | $displayChoices = []; 42 | foreach ($choices as $choiceKey => $choiceTitle) { 43 | $displayChoices[$choiceKey] = in_array($choiceKey, array_keys($chosen)) 44 | ? sprintf('%s', $choiceTitle) 45 | : sprintf('%s', $choiceTitle); 46 | } 47 | 48 | $displayChoices = array_merge($displayChoices, [ 49 | '' => '- - - - - - - - - -', 50 | 't' => 'Toggle all', 51 | 'n' => 'Next', 52 | ]); 53 | 54 | $displayQuestion = empty($chosen) 55 | ? $question 56 | : sprintf('%s (%d)', $question, count($chosen)); 57 | 58 | /** @var \Illuminate\Console\Command $this */ 59 | $choice = $this->choice($displayQuestion, $displayChoices); 60 | if (empty($choice)) { 61 | $this->warn('Nothing selected'); 62 | 63 | return $this->_multiChoice($question, $choices, $chosen); 64 | } 65 | 66 | if ($choice === 't') { 67 | $chosen = count($chosen) === count($choices) ? [] : $choices; 68 | 69 | return $this->_multiChoice($question, $choices, $chosen); 70 | } 71 | 72 | if ($choice === 'n') { 73 | return $chosen; 74 | } 75 | 76 | if (isset($chosen[$choice])) { 77 | unset($chosen[$choice]); 78 | } else { 79 | $chosen[$choice] = $choices[$choice]; 80 | } 81 | 82 | return $this->_multiChoice($question, $choices, $chosen); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | wakatime 7 | 8 | 9 | 10 | 11 |

12 | 13 | # Laravel Kata 14 | The greatest collection of the worst code. 15 | 16 | ### Concepts 17 | - Practice the fundamentals 18 | - Expose common mistakes 19 | - Lab to theorycraft mechanics 20 | 21 | ### Dependencies 22 | - Docker 23 | - Composer 24 | - NPM 25 | 26 | ## Getting started 27 | ``` 28 | npm i 29 | npm run restart 30 | 31 | ./vendor/bin/sail kata:run 32 | ``` 33 | 34 | ## Sample challenge 35 | Calculate the value of pi. 36 | 37 | ### Solution A 38 | ```php 39 | namespace App\Challenges\A; 40 | 41 | class Sample 42 | { 43 | public function calculatePi(): float 44 | { 45 | $denominator = 1; 46 | $sum = 0; 47 | 48 | for ($i = 0; $i < 100000; $i++) { 49 | $sum = ($i % 2 === 0) 50 | ? $sum + (4 / $denominator) 51 | : $sum - (4 / $denominator); 52 | 53 | $denominator += 2; 54 | } 55 | 56 | return $this->return(round($sum, 2)); 57 | } 58 | } 59 | ``` 60 | 61 | ### Solution B 62 | ```php 63 | namespace App\Challenges\B; 64 | 65 | class Sample 66 | { 67 | public function calculatePi(): float 68 | { 69 | return $this->return(round(M_PI, 2)); 70 | } 71 | } 72 | ``` 73 | 74 | ### Report 75 |

76 | Sample report 77 |

78 | 79 | ## [What are katas?](https://www.youtube.com/watch?v=r_8Rw16uscg) 80 | Katas are code challenges focused on improving skill and technique. Some train programming fundamentals, while others focus on complex problem solving. Some are puzzles meant to test your creative problem solving, while others are based on real world coding scenarios. 81 | 82 | > The term was first coined by Dave Thomas, co-author of the book The Pragmatic Programmer as an acknowledgment to the Japanese concept of kata in the martial arts. Dave's version of the concept defines a code kata as an exercise in programming which helps a programmer sharpen their skills through practice and repetition. - [Codewars](https://docs.codewars.com/concepts/kata/) 83 | -------------------------------------------------------------------------------- /app/Challenges/A/FxConversion.php: -------------------------------------------------------------------------------- 1 | calculateTotalExchangeRate($iteration); 40 | } 41 | 42 | protected function calculateTotalExchangeRate(int $iteration, bool $useSingleton = false): float 43 | { 44 | $amount = 420.69; 45 | $total = 0; 46 | 47 | $dateFrom = Carbon::createFromFormat('Y-m-d', self::DATE_FROM); 48 | $dateTo = $dateFrom->copy()->addDays($iteration); 49 | 50 | $dateToMax = Carbon::createFromFormat('Y-m-d', self::DATE_TO); 51 | if ($dateTo > $dateToMax) { 52 | $dateTo = $dateToMax; 53 | } 54 | 55 | $fxConversionModule = $useSingleton ? FxConversionModule::make() : null; 56 | 57 | $dates = []; 58 | $carbonPeriod = CarbonPeriod::create($dateFrom, $dateTo); 59 | foreach ($carbonPeriod as $date) { 60 | $dates[] = $date; 61 | $total += $useSingleton 62 | ? $fxConversionModule->convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount) 63 | : FxConversionModule::convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount); 64 | } 65 | 66 | // No do it in reverse 67 | while (! empty($dates)) { 68 | $date = array_pop($dates); 69 | $total += $useSingleton 70 | ? $fxConversionModule->convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount) 71 | : FxConversionModule::convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount); 72 | } 73 | 74 | return $total; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Challenges/B/Concurrent.php: -------------------------------------------------------------------------------- 1 | run($this->makeFunctions($iteration)); 15 | 16 | return array_sum($results); 17 | } 18 | 19 | public function sequentialVsProcess(int $iteration): int 20 | { 21 | $results = Concurrency::driver('process')->run($this->makeFunctions($iteration)); 22 | 23 | return array_sum($results); 24 | } 25 | 26 | public function sequentialVsSync(int $iteration): int 27 | { 28 | $results = Concurrency::driver('sync')->run($this->makeFunctions($iteration)); 29 | 30 | return array_sum($results); 31 | } 32 | 33 | public function sequentialVsUpsertsProcess(int $iteration): string 34 | { 35 | return $this->sequentialVsForkUpsertsByDriver($iteration, 'process'); 36 | } 37 | 38 | public function sequentialVsUpsertsFork(int $iteration): string 39 | { 40 | return $this->sequentialVsForkUpsertsByDriver($iteration, 'fork'); 41 | } 42 | 43 | public function sequentialVsUpsertsSync(int $iteration): string 44 | { 45 | return $this->sequentialVsForkUpsertsByDriver($iteration, 'sync'); 46 | } 47 | 48 | private function sequentialVsForkUpsertsByDriver(int $iteration, string $driver): string 49 | { 50 | if (! in_array($driver, ['sync', 'fork', 'process'])) { 51 | throw new InvalidArgument(sprintf('Invalid driver: %s', $driver)); 52 | } 53 | 54 | ExperimentARecord::truncate(); 55 | $recordsChunked = $this->makeRecordsChunked($iteration, 'b'); 56 | 57 | $functions = []; 58 | foreach ($recordsChunked as $index => $chunk) { 59 | $functions[] = function () use ($chunk) { 60 | ExperimentARecord::upsertPrototype($chunk, [ 61 | 'unique_field_1', 'unique_field_2', 'unique_field_3', 62 | ], [ 63 | 'position', 'update_field_1', 'update_field_2', 'update_field_3', 64 | ]); 65 | }; 66 | } 67 | 68 | Concurrency::driver($driver)->run($functions); 69 | 70 | return $this->getExperimentARecordState(); 71 | } 72 | 73 | private function makeFunctions(int $iteration): array 74 | { 75 | $fns = []; 76 | for ($i = 0; $i < $iteration; $i++) { 77 | $fns[] = function () use ($iteration, $i) { 78 | usleep(1000); // 1ms 79 | 80 | return $iteration * $i; 81 | }; 82 | } 83 | 84 | return $fns; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Data/Traits/HasExitHintsTraitTest.php: -------------------------------------------------------------------------------- 1 | instance = new class 12 | { 13 | use HasExitHintsTrait; 14 | 15 | protected $command; 16 | 17 | public function __construct() 18 | { 19 | $this->command = new class 20 | { 21 | public function info(string $message): void {} 22 | 23 | public function warn(string $message): void {} 24 | }; 25 | } 26 | 27 | public function addHint(string $message): void 28 | { 29 | $this->addExitHint($message); 30 | } 31 | 32 | public function getHints(): Collection 33 | { 34 | return $this->getExitHints(); 35 | } 36 | 37 | public function getRandomHint(): string 38 | { 39 | return $this->getRandomExitHint(); 40 | } 41 | 42 | public function addViolations(array $violations): void 43 | { 44 | $this->addExitHintsFromViolations($violations); 45 | } 46 | }; 47 | }); 48 | 49 | it('can get hints', function () { 50 | $hint = sprintf('The hint at %s', now()->toDateTimeString()); 51 | $this->instance->addHint($hint); 52 | expect($this->instance) 53 | ->getHints() 54 | ->toBeInstanceOf(Collection::class) 55 | ->toContain($hint); 56 | }); 57 | 58 | it('can get random hint', function () { 59 | $hint = sprintf('The hint at %s', now()->toDateTimeString()); 60 | $this->instance->addHint($hint); 61 | expect($this->instance) 62 | ->getRandomHint()->toBe($hint); 63 | }); 64 | 65 | it('can add violations', function () { 66 | $violation = [ 67 | 'beginLine' => 11, 68 | 'endLine' => 11, 69 | 'package' => null, 70 | 'function' => null, 71 | 'class' => null, 72 | 'method' => null, 73 | 'description' => 'Avoid variables with short names like $a. Configured minimum length is 3.', 74 | 'rule' => 'ShortVariable', 75 | 'ruleSet' => 'Naming Rules', 76 | 'externalInfoUrl' => 'https://phpmd.org/rules/naming.html#shortvariable', 77 | 'priority' => 3, 78 | ]; 79 | 80 | $this->instance->addViolations([$violation]); 81 | 82 | $expected = <<<'STR' 83 | ### Naming Rules (ShortVariable) 84 | Avoid variables with short names like $a. Configured minimum length is 3. 85 | 86 | https://phpmd.org/rules/naming.html#shortvariable 87 | 88 | (i) {"class":null,"method":null,"function":null,"beginLine":11,"endLine":11} 89 | STR; 90 | expect($this->instance) 91 | ->getRandomHint()->toBe($expected); 92 | }); 93 | -------------------------------------------------------------------------------- /bin/restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | : ' 4 | # Init Script 5 | Convenient script to ensure environment is ready to go 6 | 7 | ## Objectives 8 | - Ensure dependencies are installed 9 | - Auto configure for new + existing environments 10 | - Ensure it is executable without interactions (for CI/CD) 11 | 12 | ## Wishlist 13 | - Identify and flag new dependency configs, maybe by comparing .env with .env.example 14 | - Optimise and introduce as a git hook 15 | - Auto spin up environment with sail 16 | - Ensure all dependencies are available, like `composer`, `php`, etc 17 | ' 18 | 19 | PATH_TO_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 20 | PATH_TO_REPO="$PATH_TO_SCRIPT_DIR/../" 21 | 22 | # Always load the example env 23 | if [ ! -f "$PATH_TO_REPO/.env" ]; then 24 | cp "$PATH_TO_REPO/.env.example" "$PATH_TO_REPO/.env" 25 | fi 26 | 27 | source $PATH_TO_REPO/.env 28 | 29 | if [ "${CI_MODE}" == "circleci" ]; then 30 | echo "Running in CircliCI" 31 | 32 | echo $'\n# CircleCI\n' >> $PATH_TO_REPO/.env.new 33 | cat $PATH_TO_REPO/.env.circleci >> $PATH_TO_REPO/.env.new 34 | 35 | echo $'\n# Default env\n' >> $PATH_TO_REPO/.env.new 36 | cat $PATH_TO_REPO/.env >> $PATH_TO_REPO/.env.new 37 | 38 | mv $PATH_TO_REPO/.env.new $PATH_TO_REPO/.env 39 | source $PATH_TO_REPO/.env 40 | 41 | composer install 42 | mysql -h127.0.0.1 -uroot -p$DB_ROOT_PASSWORD -e "DROP DATABASE IF EXISTS $DB_DATABASE; CREATE DATABASE $DB_DATABASE;" 43 | mysql -h127.0.0.1 -uroot -p$DB_ROOT_PASSWORD -e "DROP DATABASE IF EXISTS $DB_TEST_DATABASE; CREATE DATABASE $DB_TEST_DATABASE;" 44 | mysql -h127.0.0.1 -uroot -p$DB_ROOT_PASSWORD -e "GRANT ALL PRIVILEGES ON *.* TO 'sail'@'%'; FLUSH PRIVILEGES;" 45 | 46 | # experimenting 47 | echo "Serving Laravel..." 48 | nohup php artisan serve & 49 | 50 | php artisan migrate:fresh --seed --no-interaction --force 51 | php artisan migrate:fresh --env=testing --database=$DB_TEST_DATABASE --seed --force --no-interaction 52 | exit 0 53 | fi 54 | 55 | echo "Running in local" 56 | 57 | # Install dependencies 58 | ./vendor/bin/sail composer install 59 | 60 | # Launch sail environment 61 | ./vendor/bin/sail down --rmi local -v 62 | ./vendor/bin/sail up -d --build 63 | 64 | # Give mysql some time 65 | echo "Sleeping for 9 seconds to give MySQL some time..." 66 | sleep 9 67 | 68 | docker exec -it kata-mysql mysql -uroot -p$DB_ROOT_PASSWORD -e "DROP DATABASE IF EXISTS $DB_DATABASE; CREATE DATABASE $DB_DATABASE;" 69 | docker exec -it kata-mysql mysql -uroot -p$DB_ROOT_PASSWORD -e "DROP DATABASE IF EXISTS $DB_TEST_DATABASE; CREATE DATABASE $DB_TEST_DATABASE;" 70 | docker exec -it kata-mysql mysql -uroot -p$DB_ROOT_PASSWORD -e "GRANT ALL PRIVILEGES ON *.* TO '$DB_USERNAME'@'%'; FLUSH PRIVILEGES;" 71 | 72 | ./vendor/bin/sail artisan migrate:fresh --seed --force --no-interaction 73 | ./vendor/bin/sail artisan migrate:fresh --env=testing --database=$DB_TEST_DATABASE --seed --force --no-interaction 74 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class, 18 | // \App\Http\Middleware\TrustHosts::class, 19 | \App\Http\Middleware\TrustProxies::class, 20 | \Illuminate\Http\Middleware\HandleCors::class, 21 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 22 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 23 | \App\Http\Middleware\TrimStrings::class, 24 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 25 | ]; 26 | 27 | /** 28 | * The application's route middleware groups. 29 | * 30 | * @var array> 31 | */ 32 | protected $middlewareGroups = [ 33 | 'web' => [ 34 | \App\Http\Middleware\EncryptCookies::class, 35 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 36 | \Illuminate\Session\Middleware\StartSession::class, 37 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 38 | \App\Http\Middleware\VerifyCsrfToken::class, 39 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 40 | ], 41 | 42 | 'api' => [ 43 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 44 | 'throttle:api', 45 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 46 | ], 47 | 48 | 'mock' => [ 49 | // \Illuminate\Routing\Middleware\SubstituteBindings::class, 50 | ], 51 | ]; 52 | 53 | /** 54 | * The application's route middleware. 55 | * 56 | * These middleware may be assigned to groups or used individually. 57 | * 58 | * @var array 59 | */ 60 | protected $routeMiddleware = [ 61 | 'auth' => \App\Http\Middleware\Authenticate::class, 62 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 63 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 64 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 65 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 66 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 67 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 68 | 'signed' => \App\Http\Middleware\ValidateSignature::class, 69 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 70 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 20 | return $request->user(); 21 | }); 22 | 23 | Route::any('/', function (Request $request) { 24 | $message = "I'm a teapot!"; 25 | 26 | return response($message, 418); 27 | }); 28 | 29 | Route::get('health', function (Request $request) { 30 | return JsonResource::make([ 31 | 'success' => true, 32 | ]); 33 | }); 34 | 35 | /** 36 | * Get the list of challenges 37 | */ 38 | Route::get('kata', function (Request $request) { 39 | return JsonResource::make([ 40 | 'success' => true, 41 | 'data' => collect(config('laravel-kata.challenges', [])) 42 | ->map(function ($className) { 43 | $classNameParts = explode('\\', $className); 44 | 45 | return array_pop($classNameParts); 46 | }) 47 | ->toArray(), 48 | ]); 49 | }); 50 | 51 | /** 52 | * Get the list of challenge's methods 53 | */ 54 | Route::get('kata/{challenge}', function (Request $request, string $challenge) { 55 | $class = sprintf( 56 | 'App\\Challenges\\A\\%s', 57 | $challenge 58 | ); 59 | 60 | try { 61 | $reflectionClass = new ReflectionClass($class); 62 | } catch (ReflectionException $exception) { 63 | throw new Exception(sprintf( 64 | 'Something bad happened: %s', 65 | $exception->getMessage() 66 | )); 67 | } 68 | 69 | $data = collect($reflectionClass->getMethods()) 70 | ->filter(fn (ReflectionMethod $method) => $method->class === $class) 71 | ->filter(fn (ReflectionMethod $method) => $method->isPublic()) 72 | ->map(fn ($method) => $method->name) 73 | ->toArray(); 74 | 75 | return JsonResource::make([ 76 | 'success' => true, 77 | 'data' => $data, 78 | ]); 79 | }); 80 | 81 | /** 82 | * Run the challenge 83 | */ 84 | Route::get('kata/{challenge}/run', function (Request $request, string $challenge) { 85 | /** @var KataRunner $kataRunner */ 86 | $kataRunner = app()->makeWith(KataRunner::class, [ 87 | 'command' => null, 88 | 'challenges' => [ 89 | sprintf('App\\Challenges\\A\\%s', $challenge), 90 | ], 91 | ]); 92 | 93 | $data = $kataRunner->run(); 94 | 95 | return JsonResource::make([ 96 | 'success' => true, 97 | 'data' => $data, 98 | ]); 99 | }); 100 | --------------------------------------------------------------------------------