├── .editorconfig ├── .github └── workflows │ └── php.yml ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build ├── coverage │ └── .gitignore ├── logs │ └── .gitignore └── redis.ini ├── composer.json ├── composer.lock ├── phpcs.xml ├── phpunit.xml.dist ├── phpunit.xml.dist.bak ├── src ├── Lodash │ ├── Auth │ │ ├── Contracts │ │ │ ├── AuthServiceContract.php │ │ │ ├── ClientRepositoryContract.php │ │ │ ├── RefreshTokenBridgeRepositoryContract.php │ │ │ ├── RefreshTokenRepositoryContract.php │ │ │ ├── TokenRepositoryContract.php │ │ │ ├── UserContract.php │ │ │ └── UserRepositoryContract.php │ │ ├── Events │ │ │ ├── StartEmulateEvent.php │ │ │ └── StopEmulateEvent.php │ │ ├── InternalUserProvider.php │ │ ├── Passport │ │ │ ├── Grants │ │ │ │ ├── EmulateUserGrant.php │ │ │ │ ├── GoogleAccessTokenGrant.php │ │ │ │ ├── GoogleIdTokenGrant.php │ │ │ │ ├── Grant.php │ │ │ │ ├── InternalGrant.php │ │ │ │ └── InternalRefreshTokenGrant.php │ │ │ ├── Guards │ │ │ │ ├── RequestGuard.php │ │ │ │ └── TokenGuard.php │ │ │ └── PassportServiceProvider.php │ │ ├── Repositories │ │ │ ├── ClientRepository.php │ │ │ ├── RefreshTokenBridgeRepository.php │ │ │ ├── RefreshTokenRepository.php │ │ │ ├── TokenRepository.php │ │ │ └── UserRepository.php │ │ └── Services │ │ │ └── AuthService.php │ ├── Cache │ │ ├── CacheManager.php │ │ ├── CacheServiceProvider.php │ │ └── RedisStore.php │ ├── Commands │ │ ├── ClearAll.php │ │ ├── Compile.php │ │ ├── DbClear.php │ │ ├── DbDump.php │ │ ├── DbRestore.php │ │ ├── LogClear.php │ │ ├── UserAdd.php │ │ └── UserPassword.php │ ├── Composer │ │ ├── ComposerChecker.php │ │ └── ComposerScripts.php │ ├── Debug │ │ └── DebugServiceProvider.php │ ├── Elasticsearch │ │ ├── ElasticsearchException.php │ │ ├── ElasticsearchManager.php │ │ ├── ElasticsearchManagerContract.php │ │ ├── ElasticsearchQueryContract.php │ │ └── ElasticsearchServiceProvider.php │ ├── Eloquent │ │ ├── Casts │ │ │ └── BinaryUuid.php │ │ ├── ManyToManyPreload.php │ │ ├── UserIdentities.php │ │ ├── UsesUuidAsPrimary.php │ │ └── UuidAsPrimaryContract.php │ ├── Foundation │ │ └── Application.php │ ├── Http │ │ ├── Request.php │ │ ├── Requests │ │ │ └── RestrictsExtraAttributes.php │ │ └── Resources │ │ │ ├── ArrayResource.php │ │ │ ├── ErrorResource.php │ │ │ ├── JsonResource.php │ │ │ ├── JsonResourceCollection.php │ │ │ ├── Response │ │ │ ├── PaginatedResourceResponse.php │ │ │ └── ResourceResponse.php │ │ │ ├── SuccessResource.php │ │ │ ├── TransformableArrayResource.php │ │ │ ├── TransformableContract.php │ │ │ └── TransformsData.php │ ├── Log │ │ └── DateTimeFormatter.php │ ├── Middlewares │ │ ├── ApiLocale.php │ │ ├── SetRequestContext.php │ │ ├── SimpleBasicAuth.php │ │ └── XssSecurity.php │ ├── Queue │ │ ├── QueueServiceProvider.php │ │ └── SqsFifo │ │ │ ├── Connectors │ │ │ └── SqsFifoConnector.php │ │ │ └── SqsFifoQueue.php │ ├── Redis │ │ ├── Connections │ │ │ └── PhpRedisArrayConnection.php │ │ ├── Connectors │ │ │ └── PhpRedisConnector.php │ │ ├── RedisManager.php │ │ └── RedisServiceProvider.php │ ├── ServiceProvider.php │ ├── Support │ │ ├── Arr.php │ │ ├── Declensions.php │ │ ├── Str.php │ │ └── Uuid.php │ ├── Testing │ │ ├── Attributes.php │ │ ├── DataStructuresProvider.php │ │ ├── FakeDataProvider.php │ │ └── Response.php │ └── Validation │ │ └── StrictTypesValidator.php ├── config │ └── config.php └── helpers.php └── tests ├── Bootstrap.php └── Unit ├── Eloquent └── UsesUuidAsPrimaryTest.php ├── Http ├── Requests │ ├── CustomRequest.php │ └── RestrictsExtraAttributesTest.php └── Resources │ ├── ArrayResourceTest.php │ ├── ErrorResourceTest.php │ ├── JsonResourceCollectionTest.php │ ├── User.php │ ├── UserResource.php │ └── UserResourceWithHidden.php ├── Middleware └── SimpleBasicAuthTest.php ├── RedisTest.php ├── ServiceProviderTest.php ├── Support ├── DeclensionsTest.php └── StrTest.php ├── TestCase.php └── Testing ├── AttributesTest.php ├── DataStructuresBuilderTest.php └── ItemStructuresProvider.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | # Tab indentation (no size specified) 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v2 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install dependencies 30 | if: steps.composer-cache.outputs.cache-hit != 'true' 31 | run: composer install --prefer-dist --no-progress --no-suggest 32 | 33 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 34 | # Docs: https://getcomposer.org/doc/articles/scripts.md 35 | 36 | - name: Run code style check 37 | run: composer run-script phpcs 38 | 39 | - name: Run test suite 40 | run: composer run-script test 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor/ 3 | node_modules/ 4 | npm-debug.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | storage/*.key 14 | .env.*.php 15 | .env.php 16 | .env 17 | Homestead.yaml 18 | Homestead.json 19 | 20 | # Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer 21 | .rocketeer/ 22 | .phpunit* 23 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 600 4 | 5 | checks: 6 | php: 7 | code_rating: true 8 | duplication: true 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: php 3 | 4 | env: 5 | global: 6 | - SETUP=stable 7 | 8 | matrix: 9 | fast_finish: true 10 | include: 11 | - php: 7.4 12 | - php: 8.0 13 | #- php: 7.3 14 | # env: SETUP=lowest 15 | 16 | cache: 17 | directories: 18 | - $HOME/.composer/cache 19 | 20 | services: 21 | - redis-server 22 | - mysql 23 | 24 | before_install: 25 | #- phpenv config-rm xdebug.ini || true 26 | - printf "\n" | pecl install -f igbinary 27 | # Install & Build Redis 28 | - printf "\n" | pecl install -f --nobuild redis 29 | - cd "$(pecl config-get temp_dir)/redis" 30 | - phpize 31 | - ./configure --enable-redis-igbinary 32 | - make && make install 33 | - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 34 | - cd - 35 | - travis_retry composer self-update 36 | - mysql -e 'CREATE DATABASE forge;' 37 | 38 | install: 39 | - travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest 40 | 41 | script: 42 | - php --ri redis 43 | - composer phpcs 44 | - composer coverage-clover 45 | 46 | after_script: 47 | - wget https://scrutinizer-ci.com/ocular.phar 48 | - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 49 | 50 | notifications: 51 | on_success: never 52 | on_failure: always 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at akalongman@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------- 3 | 4 | Before you contribute code to this project, please make sure it conforms to the PSR-2 coding standard 5 | and that the project unit tests still pass. The easiest way to contribute is to work on a checkout of the repository, 6 | or your own fork. If you do this, you can run the following commands to check if everything is ready to submit: 7 | 8 | composer phpcs 9 | 10 | Which should give you no output, indicating that there are no coding standard errors. And then: 11 | 12 | composer test 13 | 14 | Which should give you no failures or errors. You can ignore any skipped tests as these are for external tools. 15 | 16 | Pushing 17 | ------- 18 | 19 | Development is based on the git flow branching model (see http://nvie.com/posts/a-successful-git-branching-model/ ) 20 | If you fix a bug please push in hotfix branch. 21 | If you develop a new feature please create a new branch. 22 | 23 | Version 24 | ------- 25 | Version number: 0.#version.#hotfix 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The [MIT License](http://opensource.org/licenses/mit-license.php) 2 | 3 | Copyright (c) 2016 [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman) 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 | 23 | -------------------------------------------------------------------------------- /build/coverage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /build/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /build/redis.ini: -------------------------------------------------------------------------------- 1 | extension="igbinary.so" 2 | extension="redis.so" 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "longman/laravel-lodash", 3 | "type": "library", 4 | "description": "Add more functional to Laravel", 5 | "keywords": [ 6 | "lodash", 7 | "laravel", 8 | "utilities", 9 | "igbinary", 10 | "redis", 11 | "phpredis", 12 | "aws", 13 | "sqs", 14 | "tools" 15 | ], 16 | "license": "MIT", 17 | "minimum-stability": "stable", 18 | "homepage": "https://github.com/akalongman/laravel-lodash", 19 | "support": { 20 | "issues": "https://github.com/akalongman/laravel-lodash/issues", 21 | "source": "https://github.com/akalongman/laravel-lodash" 22 | }, 23 | "authors": [ 24 | { 25 | "name": "Avtandil Kikabidze aka LONGMAN", 26 | "email": "akalongman@gmail.com", 27 | "homepage": "https://longman.me", 28 | "role": "Maintainer, Developer" 29 | } 30 | ], 31 | "require": { 32 | "php": "^8.3", 33 | "laravel/framework": "^12.0" 34 | }, 35 | "require-dev": { 36 | "google/apiclient": "^2.18", 37 | "aws/aws-sdk-php": "^3.306", 38 | "elasticsearch/elasticsearch": "^8.17", 39 | "jetbrains/phpstorm-attributes": "^1.0", 40 | "laravel/horizon": "^5.24", 41 | "laravel/passport": "^12.0", 42 | "longman/php-code-style": "^10.1", 43 | "mockery/mockery": "^1.6", 44 | "neitanod/forceutf8": "^2.0", 45 | "orchestra/testbench": "^10.1", 46 | "phpunit/phpunit": "^11.0" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "Longman\\LaravelLodash\\": "src/Lodash" 51 | }, 52 | "files": [ 53 | "src/helpers.php" 54 | ] 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Tests\\": "tests/" 59 | } 60 | }, 61 | "config": { 62 | "sort-packages": true, 63 | "process-timeout": 3600, 64 | "allow-plugins": { 65 | "dealerdirect/phpcodesniffer-composer-installer": true, 66 | "php-http/discovery": true 67 | } 68 | }, 69 | "extra": { 70 | "laravel": { 71 | "providers": [ 72 | "Longman\\LaravelLodash\\ServiceProvider" 73 | ] 74 | } 75 | }, 76 | "suggest": { 77 | "ext-json": "Needed for using resources", 78 | "ext-redis": "Needed for using phpredis", 79 | "ext-igbinary": "Needed for speed up Redis/Memcached data serialization and reduce serialized data size", 80 | "ext-msgpack": "Needed for speed up Redis/Memcached data serialization and reduce serialized data size", 81 | "ext-lzf": "Needed for serialization Redis traffic", 82 | "ext-lz4": "Needed for serialization Redis traffic", 83 | "longman/laravel-multilang": "Adds multilanguage functional to Laravel >=5.5", 84 | "ramsey/uuid": "Use UUID identifiers in the eloquent models", 85 | "elasticsearch/elasticsearch": "Use Elasticsearch service in the laravel", 86 | "aws/aws-sdk-php": "Use AWS SQS >=3", 87 | "barryvdh/laravel-debugbar": "Allow print compiled queries via function get_db_queries()", 88 | "neitanod/forceutf8": "Allow encoding/decoding utf-8 strings", 89 | "beyondcode/laravel-self-diagnosis": "Run system diagnosis with many custom checks" 90 | }, 91 | "scripts": { 92 | "phpcs": "./vendor/bin/phpcs --standard=phpcs.xml -spn --encoding=utf-8 src/ tests/ --report-width=150", 93 | "phpcbf": "./vendor/bin/phpcbf --standard=phpcs.xml -spn --encoding=utf-8 src/ tests/ --report-width=150", 94 | "test": "./vendor/bin/phpunit -c phpunit.xml.dist", 95 | "coverage-clover": "./vendor/bin/phpunit --stop-on-failure --coverage-clover build/logs/clover.xml", 96 | "coverage-html": "./vendor/bin/phpunit --stop-on-failure --coverage-html build/coverage" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | */tests/* 8 | 9 | 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | ./src/config 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | ./src/config 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Contracts/AuthServiceContract.php: -------------------------------------------------------------------------------- 1 | authService->retrieveUserById((int) $identifier); 21 | } 22 | 23 | public function retrieveByToken($identifier, $token): ?Authenticatable 24 | { 25 | return $this->authService->retrieveUserByToken((int) $identifier, $token); 26 | } 27 | 28 | public function updateRememberToken(Authenticatable $user, $token): void 29 | { 30 | $this->authService->updateRememberToken($user, $token); 31 | } 32 | 33 | public function retrieveByCredentials(array $credentials): ?Authenticatable 34 | { 35 | return $this->authService->retrieveByCredentials($credentials); 36 | } 37 | 38 | public function validateCredentials(Authenticatable $user, array $credentials): bool 39 | { 40 | $plain = $credentials['password']; 41 | 42 | return $this->authService->validateCredentials($user, $plain); 43 | } 44 | 45 | public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void 46 | { 47 | $this->authService->rehashPasswordIfRequired($user, $credentials, $force); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/EmulateUserGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 31 | $this->refreshTokenTTL = new DateInterval('P1M'); 32 | } 33 | 34 | public function respondToAccessTokenRequest( 35 | ServerRequestInterface $request, 36 | ResponseTypeInterface $responseType, 37 | DateInterval $accessTokenTtl, 38 | ): ResponseTypeInterface { 39 | $startEmulating = Arr::get($request->getAttributes(), 'startEmulating', false); 40 | 41 | $emulatedUser = $request->getAttribute('emulated_user'); 42 | 43 | // Validate request 44 | $client = $this->validateClient($request); 45 | $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); 46 | $emulatedUser = $this->validateEmulatedUser($emulatedUser, $request); 47 | 48 | // Finalize the requested scopes 49 | $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $emulatedUser->getIdentifier()); 50 | 51 | // Issue and persist access token 52 | $accessToken = $this->issueAccessToken($accessTokenTtl, $client, $emulatedUser->getIdentifier(), $scopes); 53 | $refreshToken = $this->issueRefreshToken($accessToken); 54 | 55 | if ($startEmulating) { 56 | $user = $request->getAttribute('user'); 57 | 58 | event(new StartEmulateEvent($user, $emulatedUser)); 59 | 60 | $user = $this->validateUser($user, $request, $emulatedUser); 61 | $this->authService->updateAccessToken($accessToken->getIdentifier(), $user->getId()); 62 | } else { 63 | event(new StopEmulateEvent($emulatedUser)); 64 | } 65 | 66 | // Inject access token into response type 67 | $responseType->setAccessToken($accessToken); 68 | $responseType->setRefreshToken($refreshToken); 69 | 70 | return $responseType; 71 | } 72 | 73 | public function getRefreshToken(AccessToken $token): void 74 | { 75 | $this->issueRefreshToken($token); 76 | } 77 | 78 | protected function validateUser( 79 | UserEntityInterface $user, 80 | ServerRequestInterface $request, 81 | UserEntityInterface $userEntity, 82 | ): UserEntityInterface { 83 | if (! $this->authService->canUserEmulateOtherUser($user, $userEntity)) { 84 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 85 | 86 | throw OAuthServerException::accessDenied(); 87 | } 88 | 89 | if (! $user instanceof UserEntityInterface) { 90 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 91 | 92 | throw OAuthServerException::invalidCredentials(); 93 | } 94 | 95 | return $user; 96 | } 97 | 98 | protected function validateEmulatedUser( 99 | UserEntityInterface $user, 100 | ServerRequestInterface $request, 101 | ): UserEntityInterface { 102 | return $user; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/GoogleAccessTokenGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 28 | $this->refreshTokenTTL = new DateInterval('P1M'); 29 | } 30 | 31 | public function respondToAccessTokenRequest( 32 | ServerRequestInterface $request, 33 | ResponseTypeInterface $responseType, 34 | DateInterval $accessTokenTtl, 35 | ): ResponseTypeInterface { 36 | // Validate request 37 | $client = $this->validateClient($request); 38 | $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); 39 | $user = $this->validateUser($request); 40 | 41 | // Finalize the requested scopes 42 | $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); 43 | 44 | // Issue and persist access token 45 | $accessToken = $this->issueAccessToken($accessTokenTtl, $client, $user->getIdentifier(), $scopes); 46 | $refreshToken = $this->issueRefreshToken($accessToken); 47 | 48 | // Inject access token into response type 49 | $responseType->setAccessToken($accessToken); 50 | $responseType->setRefreshToken($refreshToken); 51 | 52 | // Fire login event 53 | $this->authService->fireLoginEvent('api', $user); 54 | 55 | return $responseType; 56 | } 57 | 58 | protected function validateUser(ServerRequestInterface $request): UserEntityInterface 59 | { 60 | $googleToken = $this->getRequestParameter('token', $request); 61 | if (is_null($googleToken)) { 62 | throw OAuthServerException::invalidRequest('token'); 63 | } 64 | 65 | try { 66 | $user = $this->authService->getGoogleUserByAccessToken($googleToken); 67 | } catch (Throwable $e) { 68 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 69 | 70 | throw OAuthServerException::invalidRequest('token'); 71 | } 72 | 73 | if (! $user instanceof UserEntityInterface) { 74 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 75 | 76 | throw OAuthServerException::invalidCredentials(); 77 | } 78 | 79 | return $user; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/GoogleIdTokenGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 28 | $this->refreshTokenTTL = new DateInterval('P1M'); 29 | } 30 | 31 | public function respondToAccessTokenRequest( 32 | ServerRequestInterface $request, 33 | ResponseTypeInterface $responseType, 34 | DateInterval $accessTokenTtl, 35 | ): ResponseTypeInterface { 36 | // Validate request 37 | $client = $this->validateClient($request); 38 | $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); 39 | $user = $this->validateUser($request); 40 | 41 | // Finalize the requested scopes 42 | $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); 43 | 44 | // Issue and persist access token 45 | $accessToken = $this->issueAccessToken($accessTokenTtl, $client, $user->getIdentifier(), $scopes); 46 | $refreshToken = $this->issueRefreshToken($accessToken); 47 | 48 | // Inject access token into response type 49 | $responseType->setAccessToken($accessToken); 50 | $responseType->setRefreshToken($refreshToken); 51 | 52 | // Fire login event 53 | $this->authService->fireLoginEvent('api', $user); 54 | 55 | return $responseType; 56 | } 57 | 58 | protected function validateUser(ServerRequestInterface $request): UserEntityInterface 59 | { 60 | $googleToken = $this->getRequestParameter('token', $request); 61 | if (is_null($googleToken)) { 62 | throw OAuthServerException::invalidRequest('token'); 63 | } 64 | 65 | try { 66 | $user = $this->authService->getGoogleUserByIdToken($googleToken); 67 | } catch (Throwable $e) { 68 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 69 | 70 | throw OAuthServerException::invalidRequest('token'); 71 | } 72 | 73 | if (! $user instanceof UserEntityInterface) { 74 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 75 | 76 | throw OAuthServerException::invalidCredentials(); 77 | } 78 | 79 | return $user; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/Grant.php: -------------------------------------------------------------------------------- 1 | getRequestParameter('client_id', $request); 27 | if (is_null($clientId)) { 28 | throw OAuthServerException::invalidRequest('client_id'); 29 | } 30 | 31 | // Get client without validating secret 32 | $client = $this->clientRepository->getClientEntity($clientId); 33 | 34 | if (! $client instanceof ClientEntityInterface) { 35 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); 36 | throw OAuthServerException::invalidClient($request); 37 | } 38 | 39 | return $client; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/InternalGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 29 | $this->refreshTokenTTL = new DateInterval('P1M'); 30 | } 31 | 32 | public function respondToAccessTokenRequest( 33 | ServerRequestInterface $request, 34 | ResponseTypeInterface $responseType, 35 | DateInterval $accessTokenTtl, 36 | ): ResponseTypeInterface { 37 | // Validate request 38 | $client = $this->validateClient($request); 39 | $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); 40 | $user = $this->validateUser($request); 41 | 42 | // Finalize the requested scopes 43 | $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); 44 | 45 | // Issue and persist access token 46 | $accessToken = $this->issueAccessToken($accessTokenTtl, $client, $user->getIdentifier(), $scopes); 47 | $refreshToken = $this->issueRefreshToken($accessToken); 48 | 49 | // Inject access token into response type 50 | $responseType->setAccessToken($accessToken); 51 | $responseType->setRefreshToken($refreshToken); 52 | 53 | // Fire login event 54 | $this->authService->fireLoginEvent('api', $user); 55 | 56 | return $responseType; 57 | } 58 | 59 | public function getRefreshToken(AccessToken $token): void 60 | { 61 | $this->issueRefreshToken($token); 62 | } 63 | 64 | protected function validateUser(ServerRequestInterface $request): UserEntityInterface 65 | { 66 | $login = $this->getRequestParameter('login', $request); 67 | if (is_null($login)) { 68 | throw OAuthServerException::invalidRequest('login'); 69 | } 70 | 71 | $password = $this->getRequestParameter('password', $request); 72 | if (is_null($password)) { 73 | throw OAuthServerException::invalidRequest('password'); 74 | } 75 | 76 | try { 77 | $user = $this->authService->retrieveByCredentials(['login' => $login]); 78 | } catch (Throwable $e) { 79 | report($e); 80 | 81 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 82 | 83 | throw OAuthServerException::invalidCredentials(); 84 | } 85 | 86 | if (! $user instanceof UserEntityInterface) { 87 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 88 | 89 | throw OAuthServerException::invalidCredentials(); 90 | } 91 | 92 | if (! $this->authService->validateCredentials($user, $password)) { 93 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); 94 | 95 | throw OAuthServerException::invalidCredentials(); 96 | } 97 | 98 | return $user; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Grants/InternalRefreshTokenGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 35 | 36 | $this->refreshTokenTTL = new DateInterval('P1M'); 37 | } 38 | 39 | public function respondToAccessTokenRequest( 40 | ServerRequestInterface $request, 41 | ResponseTypeInterface $responseType, 42 | DateInterval $accessTokenTtl, 43 | ): ResponseTypeInterface { 44 | // Validate request 45 | $client = $this->validateClient($request); 46 | $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); 47 | $scopes = $this->validateScopes( 48 | $this->getRequestParameter( 49 | 'scope', 50 | $request, 51 | implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']), 52 | ), 53 | ); 54 | 55 | // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure 56 | // the request doesn't include any new scopes 57 | foreach ($scopes as $scope) { 58 | if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) { 59 | throw OAuthServerException::invalidScope($scope->getIdentifier()); 60 | } 61 | } 62 | 63 | // Expire old tokens 64 | $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); 65 | if ($this->revokeRefreshTokens) { 66 | $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); 67 | } 68 | 69 | // Issue and persist new access token 70 | $accessToken = $this->issueAccessToken($accessTokenTtl, $client, $oldRefreshToken['user_id'], $scopes); 71 | $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); 72 | $responseType->setAccessToken($accessToken); 73 | 74 | // Issue and persist new refresh token if given 75 | if ($this->revokeRefreshTokens) { 76 | $refreshToken = $this->issueRefreshToken($accessToken); 77 | 78 | if ($refreshToken !== null) { 79 | $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); 80 | $responseType->setRefreshToken($refreshToken); 81 | } 82 | } 83 | 84 | // when emulated user requests new access token. set emulator user id. 85 | $oldAccessToken = $this->tokenRepository->find($oldRefreshToken['access_token_id']); 86 | if ($oldAccessToken->emulator_user_id) { 87 | $this->authService->updateAccessToken($accessToken->getIdentifier(), $oldAccessToken->emulator_user_id); 88 | } 89 | 90 | return $responseType; 91 | } 92 | 93 | protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId) 94 | { 95 | $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request); 96 | if (! is_string($encryptedRefreshToken)) { 97 | throw OAuthServerException::invalidRequest('refresh_token'); 98 | } 99 | 100 | // Validate refresh token 101 | try { 102 | $refreshToken = $this->decrypt($encryptedRefreshToken); 103 | } catch (Throwable $e) { 104 | throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); 105 | } 106 | 107 | $refreshTokenData = json_decode($refreshToken, true); 108 | if ($refreshTokenData['client_id'] !== $clientId) { 109 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); 110 | throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); 111 | } 112 | 113 | if ($refreshTokenData['expire_time'] < time()) { 114 | throw OAuthServerException::invalidRefreshToken('Token has expired'); 115 | } 116 | 117 | if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { 118 | throw OAuthServerException::invalidRefreshToken('Token has been revoked'); 119 | } 120 | 121 | return $refreshTokenData; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Guards/RequestGuard.php: -------------------------------------------------------------------------------- 1 | user = null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/Guards/TokenGuard.php: -------------------------------------------------------------------------------- 1 | server = $server; 26 | $this->tokens = $tokens; 27 | $this->clients = $clients; 28 | $this->provider = $provider; 29 | $this->encrypter = $encrypter; 30 | $this->request = $request; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Passport/PassportServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerCustomRepositories(); 45 | } 46 | 47 | #[Override] 48 | protected function registerAuthorizationServer(): void 49 | { 50 | $this->app->singleton(AuthorizationServer::class, function () { 51 | return tap($this->makeAuthorizationServer(), function (AuthorizationServer $server) { 52 | $this->enableGrants($server); 53 | }); 54 | }); 55 | } 56 | 57 | protected function enableGrants(AuthorizationServer $server): void 58 | { 59 | $accessTokenTtl = new DateInterval('PT1H'); 60 | 61 | $server->setDefaultScope(Passport::$defaultScope); 62 | 63 | $server->enableGrantType($this->makeInternalGrant(), $accessTokenTtl); 64 | 65 | $server->enableGrantType($this->makeInternalRefreshTokenGrant(), $accessTokenTtl); 66 | 67 | $server->enableGrantType($this->makeGoogleAccessTokenGrant(), $accessTokenTtl); 68 | 69 | $server->enableGrantType($this->makeGoogleIdTokenGrant(), $accessTokenTtl); 70 | 71 | $server->enableGrantType($this->makeEmulateGrant(), $accessTokenTtl); 72 | } 73 | 74 | #[Override] 75 | protected function makeGuard(array $config): RequestGuard 76 | { 77 | return new RequestGuard(function (Request $request) use ($config) { 78 | /** @var \Illuminate\Auth\AuthManager $authManager */ 79 | $authManager = $this->app['auth']; 80 | 81 | return (new TokenGuard( 82 | $this->app->make(ResourceServer::class), 83 | new PassportUserProvider($authManager->createUserProvider($config['provider']), $config['provider']), 84 | $this->app->make(TokenRepositoryContract::class), 85 | $this->app->make(ClientRepositoryContract::class), 86 | $this->app->make('encrypter'), 87 | $request, 88 | ))->user(); 89 | }, $this->app['request']); 90 | } 91 | 92 | protected function registerCustomRepositories(): void 93 | { 94 | $this->app->bind(ClientRepositoryContract::class, ClientRepository::class); 95 | $this->app->bind(TokenRepositoryContract::class, TokenRepository::class); 96 | $this->app->bind(RefreshTokenBridgeRepositoryContract::class, RefreshTokenBridgeRepository::class); 97 | $this->app->bind(UserRepositoryContract::class, UserRepository::class); 98 | $this->app->bind(RefreshTokenRepositoryContract::class, RefreshTokenRepository::class); 99 | $this->app->bind(AuthServiceContract::class, AuthService::class); 100 | } 101 | 102 | protected function makeInternalGrant(): InternalGrant 103 | { 104 | $grant = new InternalGrant( 105 | $this->app->make(AuthServiceContract::class), 106 | $this->app->make(RefreshTokenBridgeRepositoryContract::class), 107 | ); 108 | 109 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 110 | 111 | return $grant; 112 | } 113 | 114 | protected function makeInternalRefreshTokenGrant(): InternalRefreshTokenGrant 115 | { 116 | $grant = new InternalRefreshTokenGrant( 117 | $this->app->make(RefreshTokenBridgeRepositoryContract::class), 118 | $this->app->make(TokenRepositoryContract::class), 119 | $this->app->make(AuthServiceContract::class), 120 | ); 121 | 122 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 123 | 124 | return $grant; 125 | } 126 | 127 | protected function makeGoogleAccessTokenGrant(): GoogleAccessTokenGrant 128 | { 129 | $grant = new GoogleAccessTokenGrant( 130 | $this->app->make(AuthServiceContract::class), 131 | $this->app->make(RefreshTokenBridgeRepositoryContract::class), 132 | ); 133 | 134 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 135 | 136 | return $grant; 137 | } 138 | 139 | protected function makeGoogleIdTokenGrant(): GoogleIdTokenGrant 140 | { 141 | $grant = new GoogleIdTokenGrant( 142 | $this->app->make(AuthServiceContract::class), 143 | $this->app->make(RefreshTokenBridgeRepositoryContract::class), 144 | ); 145 | 146 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 147 | 148 | return $grant; 149 | } 150 | 151 | protected function makeEmulateGrant(): EmulateUserGrant 152 | { 153 | $grant = new EmulateUserGrant( 154 | $this->app->make(AuthServiceContract::class), 155 | $this->app->make(RefreshTokenBridgeRepository::class), 156 | ); 157 | 158 | $grant->setRefreshTokenTTL(new DateInterval('P1Y')); 159 | 160 | return $grant; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Repositories/ClientRepository.php: -------------------------------------------------------------------------------- 1 | find($accessTokenIdentifier); 17 | $token->setAttribute('emulator_user_id', $userId); 18 | $this->save($token); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Lodash/Auth/Repositories/UserRepository.php: -------------------------------------------------------------------------------- 1 | database = $database; 27 | $this->googleOauthService = $googleOauthService; 28 | } 29 | 30 | public function findOneForAuth(int $id): ?UserContract 31 | { 32 | $item = $this->getModel() 33 | ->find($id); 34 | 35 | return $item; 36 | } 37 | 38 | public function getGoogleUserByAccessToken(string $googleToken): ?array 39 | { 40 | $this->googleOauthService 41 | ->getClient() 42 | ->setAccessToken($googleToken); 43 | 44 | $user = $this->googleOauthService->userinfo->get(); 45 | if (empty($user->getEmail())) { 46 | return null; 47 | } 48 | 49 | return ['email' => $user->getEmail()]; 50 | } 51 | 52 | public function getGoogleUserByIdToken(string $googleToken): ?array 53 | { 54 | try { 55 | $user = $this->googleOauthService 56 | ->getClient() 57 | ->verifyIdToken($googleToken); 58 | } catch (Throwable $e) { 59 | return null; 60 | } 61 | 62 | return $user; 63 | } 64 | 65 | public function getUserEntityByUserCredentials( 66 | $username, 67 | $password, 68 | $grantType, 69 | ClientEntityInterface $clientEntity, 70 | ) { 71 | throw new Exception('getUserEntityByUserCredentials is deprecated!'); 72 | } 73 | 74 | public function retrieveByCredentials(array $credentials): ?UserContract 75 | { 76 | if (empty($credentials)) { 77 | return null; 78 | } 79 | 80 | $login = $credentials['login'] ?? $credentials['email']; 81 | 82 | $query = $this->getModel()->newQuery(); 83 | 84 | $query->where('email', '=', $login); 85 | 86 | /** @var \App\Models\User $user */ 87 | $user = $query->first(); 88 | if (! $user) { 89 | return null; 90 | } 91 | 92 | return $user; 93 | } 94 | 95 | public function retrieveUserByToken(int $identifier, string $token): ?UserContract 96 | { 97 | throw new RuntimeException('Not implemented'); 98 | } 99 | 100 | public function updateRememberToken(UserContract $user, string $token): void 101 | { 102 | // Not used 103 | } 104 | 105 | protected function getModel(): User 106 | { 107 | return new User(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Lodash/Cache/CacheManager.php: -------------------------------------------------------------------------------- 1 | app['redis']; 20 | 21 | $connection = $config['connection'] ?? 'default'; 22 | 23 | return $this->repository(new RedisStore($redis, $this->getPrefix($config), $connection)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Lodash/Cache/CacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('cache', static function ($app) { 28 | return new CacheManager($app); 29 | }); 30 | 31 | $this->app->singleton('cache.store', static function ($app) { 32 | return $app['cache']->driver(); 33 | }); 34 | 35 | $this->app->singleton('memcached.connector', static function () { 36 | return new MemcachedConnector(); 37 | }); 38 | 39 | $this->app->singleton(RateLimiter::class); 40 | } 41 | 42 | /** 43 | * Get the services provided by the provider. 44 | * 45 | * @return array 46 | */ 47 | public function provides(): array 48 | { 49 | return [ 50 | 'cache', 51 | 'cache.store', 52 | 'memcached.connector', 53 | RateLimiter::class, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Lodash/Cache/RedisStore.php: -------------------------------------------------------------------------------- 1 | call('clear-compiled'); 33 | $this->call('cache:clear'); 34 | $this->call('config:clear'); 35 | $this->call('route:clear'); 36 | $this->call('view:clear'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lodash/Commands/Compile.php: -------------------------------------------------------------------------------- 1 | call('config:cache'); 33 | $this->call('route:cache'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Lodash/Commands/DbClear.php: -------------------------------------------------------------------------------- 1 | confirmToProceed('Application In Production! Will be dropped all tables!')) { 39 | return; 40 | } 41 | 42 | $dbConn = $this->getDatabase(); 43 | $connection = DB::connection($dbConn); 44 | 45 | $database = $connection->getDatabaseName(); 46 | $tables = $connection->select('SHOW TABLES'); 47 | 48 | if (empty($tables)) { 49 | $this->info('Tables not found in database "' . $database . '"'); 50 | 51 | return; 52 | } 53 | 54 | $pretend = $this->input->getOption('pretend'); 55 | $connection->transaction(function () use ($connection, $tables, $database, $pretend) { 56 | if (! $pretend) { 57 | $connection->statement('SET FOREIGN_KEY_CHECKS=0;'); 58 | } 59 | 60 | foreach ($tables as $table) { 61 | foreach ($table as $value) { 62 | $stm = 'DROP TABLE IF EXISTS `' . $value . '`'; 63 | if ($pretend) { 64 | $this->line($stm); 65 | } else { 66 | $connection->statement($stm); 67 | $this->comment('Table `' . $value . '` dropped'); 68 | } 69 | } 70 | } 71 | if ($pretend) { 72 | return; 73 | } 74 | 75 | $connection->statement('SET FOREIGN_KEY_CHECKS=1;'); 76 | $this->info('All tables dropped from database "' . $database . '"!'); 77 | }); 78 | } 79 | 80 | protected function getDatabase(): string 81 | { 82 | $database = $this->input->getOption('database'); 83 | 84 | return $database ?: $this->laravel['config']['database.default']; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Lodash/Commands/DbDump.php: -------------------------------------------------------------------------------- 1 | getDatabase(); 43 | $connection = DB::connection($dbConn); 44 | $dbName = $connection->getConfig('database'); 45 | $filename = $dbName . '_' . Carbon::now()->format('Ymd_His') . '.sql'; 46 | 47 | $path = $this->getPath($filename); 48 | 49 | $process = new Process('mysqldump --host=' . $connection->getConfig('host') . ' --user=' . $connection->getConfig('username') . ' --password=' . $connection->getConfig('password') . ' ' . $dbName . ' > ' . $path); 50 | $process->run(); 51 | 52 | // Executes after the command finishes 53 | if (! $process->isSuccessful()) { 54 | throw new ProcessFailedException($process); 55 | } 56 | 57 | $this->info('Database backup saved to: ' . $path); 58 | } 59 | 60 | protected function getPath(string $filename): string 61 | { 62 | $path = $this->input->getOption('path'); 63 | if ($path) { 64 | if (! is_dir(base_path($path))) { 65 | mkdir(base_path($path), 0777, true); 66 | } 67 | 68 | $path = base_path($path . DIRECTORY_SEPARATOR . $filename); 69 | } else { 70 | $path = storage_path($filename); 71 | } 72 | 73 | return $path; 74 | } 75 | 76 | protected function getDatabase(): string 77 | { 78 | $database = $this->input->getOption('database'); 79 | 80 | return $database ?: $this->laravel['config']['database.default']; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Lodash/Commands/DbRestore.php: -------------------------------------------------------------------------------- 1 | confirmToProceed('Application In Production! Will be imported sql file!')) { 42 | return; 43 | } 44 | 45 | $dbConn = $this->getDatabase(); 46 | $connection = DB::connection($dbConn); 47 | 48 | $path = $this->getFilePath(); 49 | if (! file_exists($path)) { 50 | $this->error('File ' . $path . ' not found!'); 51 | 52 | return; 53 | } 54 | 55 | $connection->unprepared(file_get_contents($path)); 56 | 57 | $this->info('Database backup restored successfully'); 58 | } 59 | 60 | protected function getDatabase(): string 61 | { 62 | $database = $this->input->getOption('database'); 63 | 64 | return $database ?: $this->laravel['config']['database.default']; 65 | } 66 | 67 | protected function getFilePath(): string 68 | { 69 | $file = $this->input->getArgument('file'); 70 | 71 | return base_path($file); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Lodash/Commands/LogClear.php: -------------------------------------------------------------------------------- 1 | confirmToProceed('Application In Production! Will be deleted all log files from storage/log folder!')) { 38 | return; 39 | } 40 | 41 | $logFiles = $filesystem->allFiles(storage_path('logs')); 42 | if (empty($logFiles)) { 43 | $this->comment('Log files does not found in path ' . storage_path('logs')); 44 | 45 | return; 46 | } 47 | 48 | foreach ($logFiles as $file) { 49 | if ($file->getExtension() !== 'log') { 50 | continue; 51 | } 52 | 53 | $status = $filesystem->delete($file->getRealPath()); 54 | if (! $status) { 55 | continue; 56 | } 57 | 58 | $this->info('Successfully deleted: ' . $file); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Lodash/Commands/UserAdd.php: -------------------------------------------------------------------------------- 1 | getGuard(); 34 | $config = config('auth'); 35 | 36 | if (! isset($config['guards'][$guard]['provider'])) { 37 | $this->error('Provider not found for guard "' . $guard . '"!'); 38 | 39 | return; 40 | } 41 | $provider = $config['guards'][$guard]['provider']; 42 | if (! isset($config['providers'][$provider]['model'])) { 43 | $this->error('Model not found for provider "' . $provider . '"!'); 44 | 45 | return; 46 | } 47 | $model = $config['providers'][$provider]['model']; 48 | $user = new $model(); 49 | 50 | $email = $this->argument('email'); 51 | $password = $this->argument('password'); 52 | if (! $password) { 53 | if ($this->confirm('Let system generate password?', true)) { 54 | $password = str_random(16); 55 | } else { 56 | $password = $this->secret('Please enter new password'); 57 | } 58 | } 59 | $cryptedPassword = bcrypt($password); 60 | $user->create([ 61 | 'email' => $email, 62 | 'password' => $cryptedPassword, 63 | ]); 64 | 65 | $this->comment('User successfully created'); 66 | $this->info('E-Mail: ' . $email); 67 | $this->info('Password: ' . $password); 68 | } 69 | 70 | protected function getGuard(): string 71 | { 72 | $guard = $this->input->getOption('guard'); 73 | 74 | return $guard ?? config('auth.defaults.guard'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Lodash/Commands/UserPassword.php: -------------------------------------------------------------------------------- 1 | getGuard(); 36 | $config = config('auth'); 37 | 38 | if (! isset($config['guards'][$guard]['provider'])) { 39 | $this->error('Provider not found for guard "' . $guard . '"!'); 40 | 41 | return; 42 | } 43 | $provider = $config['guards'][$guard]['provider']; 44 | if (! isset($config['providers'][$provider]['model'])) { 45 | $this->error('Model not found for provider "' . $provider . '"!'); 46 | 47 | return; 48 | } 49 | $model = $config['providers'][$provider]['model']; 50 | $user = new $model(); 51 | 52 | $email = $this->argument('email'); 53 | $user = $user->where(compact('email'))->first(); 54 | if (empty($user)) { 55 | $this->error('User with email "' . $email . '" not found'); 56 | 57 | return; 58 | } 59 | 60 | $password = $this->argument('password'); 61 | if (! $password) { 62 | if ($this->confirm('Let system generate password?', true)) { 63 | $password = str_random(16); 64 | } else { 65 | $password = $this->secret('Please enter new password'); 66 | } 67 | } 68 | $cryptedPassword = bcrypt($password); 69 | $user->update([ 70 | 'password' => $cryptedPassword, 71 | ]); 72 | 73 | $this->comment('User "' . $email . '" successfully updated'); 74 | $this->info('New Password: ' . $password); 75 | } 76 | 77 | protected function getGuard(): string 78 | { 79 | $guard = $this->input->getOption('guard'); 80 | 81 | return $guard ?? config('auth.defaults.guard'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Lodash/Composer/ComposerChecker.php: -------------------------------------------------------------------------------- 1 | getHashPath(), $this->getHash($this->getContent($this->getJsonPath()))); 31 | chmod($this->getHashPath(), 0777); 32 | } 33 | 34 | public function checkHash(): void 35 | { 36 | if (! $this->validateHash()) { 37 | throw new RuntimeException('The vendor folder is not in sync with the composer.lock file, it is recommended that you run `composer install`.'); 38 | } 39 | } 40 | 41 | public function validateHash(): bool 42 | { 43 | if (! file_exists($this->getHashPath())) { 44 | return false; 45 | } 46 | 47 | $hash = $this->getContent($this->getHashPath()); 48 | $currentHash = $this->getHash($this->getContent($this->getJsonPath())); 49 | 50 | return $hash === $currentHash; 51 | } 52 | 53 | private function getJsonPath(): string 54 | { 55 | return $this->baseDir . DIRECTORY_SEPARATOR . $this->lockPath; 56 | } 57 | 58 | private function getHashPath(): string 59 | { 60 | return $this->baseDir . DIRECTORY_SEPARATOR . $this->hashPath; 61 | } 62 | 63 | private function getHash(string $content): string 64 | { 65 | return md5($content); 66 | } 67 | 68 | private function getContent(string $file): string 69 | { 70 | if (! file_exists($file)) { 71 | throw new InvalidArgumentException('File ' . $file . ' does not found!'); 72 | } 73 | 74 | return file_get_contents($file); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Lodash/Composer/ComposerScripts.php: -------------------------------------------------------------------------------- 1 | getComposer()->getConfig()->get('vendor-dir')); 16 | 17 | $composer = new ComposerChecker($baseDir); 18 | $composer->createHash(); 19 | $event->getIO()->write('The composer.lock hash saved in the "vendor/composer.hash" file.'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Lodash/Debug/DebugServiceProvider.php: -------------------------------------------------------------------------------- 1 | getClientIp(); 18 | if (! in_array($ip, $ips)) { 19 | return; 20 | } 21 | 22 | config(['app.debug' => true]); 23 | } 24 | 25 | public function register(): void 26 | { 27 | // 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Lodash/Elasticsearch/ElasticsearchException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 18 | } 19 | 20 | public function getErrors(): array 21 | { 22 | return $this->errors; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lodash/Elasticsearch/ElasticsearchManagerContract.php: -------------------------------------------------------------------------------- 1 | app->singleton(Client::class, static function (Application $app) { 23 | // Logger instance 24 | $config = $app['config']->get('services.elastic_search'); 25 | 26 | $params = [ 27 | 'hosts' => $config['hosts'], 28 | ]; 29 | 30 | if (! empty($config['connectionParams'])) { 31 | $params['connectionParams'] = $config['connectionParams']; 32 | } 33 | 34 | $logger = ! empty($config['log_channel']) ? $app['log']->stack($config['log_channel']) : null; 35 | if ($logger) { 36 | $params['logger'] = $logger; 37 | } 38 | 39 | $client = ClientBuilder::fromConfig($params); 40 | 41 | return $client; 42 | }); 43 | 44 | $this->app->singleton(ElasticsearchManagerContract::class, static function (Application $app) { 45 | $client = $app->make(Client::class); 46 | $enabled = (bool) $app['config']->get('services.elastic_search.enabled', false); 47 | 48 | return new ElasticsearchManager($client, $enabled); 49 | }); 50 | } 51 | 52 | public function provides(): array 53 | { 54 | return [Client::class, ElasticsearchManagerContract::class]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Lodash/Eloquent/Casts/BinaryUuid.php: -------------------------------------------------------------------------------- 1 | Uuid::toBinary($value), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lodash/Eloquent/ManyToManyPreload.php: -------------------------------------------------------------------------------- 1 | getTable(); 46 | $queryKeyColumn = $query->getQuery()->wheres[0]['column']; 47 | $join = $query->getQuery()->joins; 48 | $newQuery = $this->newQueryWithoutScopes(); 49 | $connection = $this->getConnection(); 50 | 51 | // Initialize MySQL variables inline 52 | $newQuery->from($connection->raw('(select @num:=0, @group:=0) as `vars`, ' . $this->quoteColumn($table))); 53 | 54 | // If no columns already selected, let's select * 55 | if (! $query->getQuery()->columns) { 56 | $newQuery->select($table . '.*'); 57 | } 58 | 59 | // Make sure column aliases are unique 60 | $groupAlias = $table . '_grp'; 61 | $numAlias = $table . '_rn'; 62 | 63 | // Apply mysql variables 64 | $newQuery->addSelect( 65 | $connection->raw( 66 | '@num := if(@group = ' . $this->quoteColumn($queryKeyColumn) . ', @num+1, 1) as `' . $numAlias . '`, @group := ' . $this->quoteColumn($queryKeyColumn) . ' as `' . $groupAlias . '`', 67 | ), 68 | ); 69 | 70 | // Make sure first order clause is the group order 71 | $newQuery->getQuery()->orders = (array) $query->getQuery()->orders; 72 | array_unshift($newQuery->getQuery()->orders, [ 73 | 'column' => $queryKeyColumn, 74 | 'direction' => 'asc', 75 | ]); 76 | 77 | if ($join) { 78 | $leftKey = explode('.', $queryKeyColumn)[1]; 79 | $leftKeyColumn = '`' . $table . '`.`' . $leftKey . '`'; 80 | $newQuery->addSelect($queryKeyColumn); 81 | $newQuery->mergeBindings($query->getQuery()); 82 | $newQuery->getQuery()->joins = (array) $query->getQuery()->joins; 83 | $query->whereRaw($leftKeyColumn . ' = ' . $this->quoteColumn($queryKeyColumn)); 84 | } 85 | 86 | $query->from($connection->raw('(' . $newQuery->toSql() . ') as `' . $table . '`')) 87 | ->where($numAlias, '<=', $limit); 88 | 89 | return $this; 90 | } 91 | 92 | public function scopeLimitPerGroupViaUnion(Builder $query, int $limit = 10, array $pivotColumns = []): Model 93 | { 94 | $table = $this->getTable(); 95 | $queryKeyColumn = $query->getQuery()->wheres[0]['column']; 96 | $joins = $query->getQuery()->joins; 97 | $connection = $this->getConnection(); 98 | 99 | $queryKeyValues = $query->getQuery()->wheres[0]['values']; 100 | $pivotTable = explode('.', $queryKeyColumn)[0]; 101 | 102 | $joinLeftColumn = $joins[0]->wheres[0]['first']; 103 | $joinRightColumn = $joins[0]->wheres[0]['second']; 104 | $joinOperator = $joins[0]->wheres[0]['operator']; 105 | 106 | // Remove extra wheres 107 | $wheres = $query->getQuery()->wheres; 108 | $bindings = $query->getQuery()->bindings; 109 | foreach ($wheres as $key => $where) { 110 | if (! isset($where['column']) || $where['column'] !== $queryKeyColumn) { 111 | continue; 112 | } 113 | 114 | //$count = count($where['values']); 115 | unset($wheres[$key]); 116 | } 117 | $groups = $query->getQuery()->groups; 118 | $orders = $query->getQuery()->orders; 119 | 120 | foreach ($queryKeyValues as $value) { 121 | if (! isset($unionQuery1)) { 122 | $unionQuery1 = $connection->table($pivotTable) 123 | ->select([$table . '.*']) 124 | ->join($table, $joinLeftColumn, $joinOperator, $joinRightColumn) 125 | ->where($queryKeyColumn, '=', $value) 126 | ->limit($limit); 127 | if (! empty($groups)) { 128 | foreach ($groups as $group) { 129 | $unionQuery1->groupBy($group); 130 | } 131 | } 132 | 133 | if (! empty($orders)) { 134 | foreach ($orders as $order) { 135 | $unionQuery1->orderBy($order['column'], $order['direction']); 136 | } 137 | } 138 | 139 | // Merge wheres 140 | $unionQuery1->mergeWheres($wheres, $bindings); 141 | } else { 142 | $select = [ 143 | $table . '.*', 144 | ]; 145 | 146 | foreach ($pivotColumns as $pivotColumn) { 147 | $select[] = $pivotTable . '.' . $pivotColumn . ' as pivot_' . $pivotColumn; 148 | } 149 | 150 | $unionQuery2 = $connection->table($pivotTable) 151 | ->select($select) 152 | ->join($table, $joinLeftColumn, $joinOperator, $joinRightColumn) 153 | ->where($queryKeyColumn, '=', $value) 154 | ->limit($limit); 155 | if (! empty($groups)) { 156 | foreach ($groups as $group) { 157 | $unionQuery2->groupBy($group); 158 | } 159 | } 160 | 161 | if (! empty($orders)) { 162 | foreach ($orders as $order) { 163 | $unionQuery2->orderBy($order['column'], $order['direction']); 164 | } 165 | } 166 | 167 | // Merge wheres 168 | $unionQuery2->mergeWheres($wheres, $bindings); 169 | 170 | $unionQuery1->unionAll($unionQuery2); 171 | } 172 | } 173 | 174 | if (! isset($unionQuery1)) { 175 | throw new InvalidArgumentException('Union query does not found'); 176 | } 177 | 178 | $query->setQuery($unionQuery1); 179 | 180 | return $this; 181 | } 182 | 183 | private function quoteColumn(string $column): string 184 | { 185 | return '`' . str_replace('.', '`.`', $column) . '`'; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Lodash/Eloquent/UserIdentities.php: -------------------------------------------------------------------------------- 1 | belongsTo($this->getUserClass(), $this->columnCreatedBy); 57 | } 58 | 59 | public function editor(): BelongsTo 60 | { 61 | return $this->belongsTo($this->getUserClass(), $this->columnUpdatedBy); 62 | } 63 | 64 | public function destroyer(): BelongsTo 65 | { 66 | return $this->belongsTo($this->getUserClass(), $this->columnDeletedBy); 67 | } 68 | 69 | public function usesUserIdentities(): bool 70 | { 71 | return $this->userIdentities; 72 | } 73 | 74 | public function stopUserIdentities(): self 75 | { 76 | $this->userIdentities = false; 77 | 78 | return $this; 79 | } 80 | 81 | public function startUserIdentities(): self 82 | { 83 | $this->userIdentities = true; 84 | 85 | return $this; 86 | } 87 | 88 | protected static function bootUserIdentities(): void 89 | { 90 | // Creating 91 | static::creating(static function (Model $model) { 92 | if (! $model->usesUserIdentities()) { 93 | return; 94 | } 95 | 96 | if (is_null($model->{$model->columnCreatedBy})) { 97 | $model->{$model->columnCreatedBy} = auth()->id(); 98 | } 99 | 100 | if (! is_null($model->{$model->columnUpdatedBy})) { 101 | return; 102 | } 103 | 104 | $model->{$model->columnUpdatedBy} = auth()->id(); 105 | }); 106 | 107 | // Updating 108 | static::updating(static function (Model $model) { 109 | if (! $model->usesUserIdentities()) { 110 | return; 111 | } 112 | 113 | $model->{$model->columnUpdatedBy} = auth()->id(); 114 | }); 115 | 116 | // Deleting/Restoring 117 | if (! static::usingSoftDeletes()) { 118 | return; 119 | } 120 | 121 | static::deleting(static function (Model $model) { 122 | if (! $model->usesUserIdentities()) { 123 | return; 124 | } 125 | 126 | if (is_null($model->{$model->columnDeletedBy})) { 127 | $model->{$model->columnDeletedBy} = auth()->id(); 128 | } 129 | 130 | $model->save(); 131 | }); 132 | 133 | static::restoring(static function (Model $model) { 134 | if (! $model->usesUserIdentities()) { 135 | return; 136 | } 137 | 138 | $model->{$model->columnDeletedBy} = null; 139 | }); 140 | } 141 | 142 | protected function getUserClass(): string 143 | { 144 | $provider = auth()->guard()->getProvider(); 145 | if ($provider) { 146 | return $provider->getModel(); 147 | } 148 | 149 | $user = auth()->guard()->user(); 150 | if ($user) { 151 | return $user::class; 152 | } 153 | 154 | if (class_exists(User::class)) { 155 | return User::class; 156 | } 157 | 158 | throw new InvalidArgumentException('User class can not detected'); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Lodash/Eloquent/UsesUuidAsPrimary.php: -------------------------------------------------------------------------------- 1 | getKeyName(); 34 | 35 | if (! empty($model->{$keyName})) { 36 | // Do some validation 37 | $model->isUuidBinary($model->{$keyName}) 38 | ? Uuid::fromBytes($model->{$keyName}) 39 | : Uuid::fromString($model->{$keyName}); 40 | } elseif (! empty($model->attributes[$keyName])) { 41 | // Do some validation 42 | $model->isUuidBinary($model->attributes[$keyName]) 43 | ? Uuid::fromBytes($model->attributes[$keyName]) 44 | : Uuid::fromString($model->attributes[$keyName]); 45 | } else { 46 | $model->{$keyName} = Uuid::uuid1()->getBytes(); 47 | } 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Lodash/Eloquent/UuidAsPrimaryContract.php: -------------------------------------------------------------------------------- 1 | getRevision(); 30 | $version = 'v' . self::APP_VERSION; 31 | if (! empty($revision)) { 32 | $version .= '.' . substr($revision, 0, 8); 33 | } 34 | 35 | return $version; 36 | } 37 | 38 | public function getUrl(): string 39 | { 40 | return (string) config('app.url', 'http://localhost'); 41 | } 42 | 43 | public function isDebug(): bool 44 | { 45 | return (bool) config('app.debug', false); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Lodash/Http/Request.php: -------------------------------------------------------------------------------- 1 | requestId)) { 31 | return $this->requestId; 32 | } 33 | $this->requestId = (string) Str::uuid(); 34 | 35 | return $this->requestId; 36 | } 37 | 38 | public function getClientPlatform(): string 39 | { 40 | if (! is_null($this->requestPlatform)) { 41 | return $this->requestPlatform; 42 | } 43 | 44 | $ua = Str::lower($this->userAgent()); 45 | 46 | if (str_contains($ua, 'okhttp')) { 47 | $this->requestPlatform = self::CLIENT_PLATFORM_ANDROID; 48 | } elseif (str_contains($ua, 'darwin')) { 49 | $this->requestPlatform = self::CLIENT_PLATFORM_IOS; 50 | } else { 51 | $this->requestPlatform = self::CLIENT_PLATFORM_WEB; 52 | } 53 | 54 | return $this->requestPlatform; 55 | } 56 | 57 | public function getReferrerWithoutDomain(): string 58 | { 59 | return str_replace(config('app.url'), '', (string) $this->server('HTTP_REFERER')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Lodash/Http/Requests/RestrictsExtraAttributes.php: -------------------------------------------------------------------------------- 1 | checkForNotAllowedProperties(); 33 | 34 | $this->checkForEmptyPayload(); 35 | 36 | parent::prepareForValidation(); 37 | } 38 | 39 | private function checkForEmptyPayload(): void 40 | { 41 | if (! $this->checkForEmptyPayload) { 42 | return; 43 | } 44 | 45 | if (in_array($this->method(), $this->methodsForEmptyPayload, true)) { 46 | if (empty($this->input())) { 47 | throw ValidationException::withMessages(['general' => [__('validation.payload_is_empty')]]); 48 | } 49 | } 50 | } 51 | 52 | private function checkForNotAllowedProperties(): void 53 | { 54 | if (! $this->checkForExtraProperties) { 55 | return; 56 | } 57 | 58 | $validationData = $this->getValidationData(); 59 | 60 | // Ignore marked properties 61 | if (! empty($this->ignoreExtraProperties)) { 62 | foreach ($this->ignoreExtraProperties as $deleleValue) { 63 | if (($key = array_search($deleleValue, $validationData)) !== false) { 64 | unset($validationData[$key]); 65 | } 66 | } 67 | } 68 | 69 | $extraAttributes = array_diff( 70 | $validationData, 71 | $this->getAllowedAttributes(), 72 | ); 73 | 74 | if (! empty($extraAttributes)) { 75 | $messages = []; 76 | foreach ($extraAttributes as $attribute) { 77 | $message = __('validation.restrict_extra_attributes', ['attribute' => $attribute]); 78 | if (config('app.debug')) { 79 | $message .= ' Request Class: ' . static::class; 80 | } 81 | 82 | $messages[$attribute] = $message; 83 | } 84 | 85 | throw ValidationException::withMessages($messages); 86 | } 87 | } 88 | 89 | private function getValidationData(): array 90 | { 91 | $data = Arr::dot($this->validationData()); 92 | 93 | $return = []; 94 | foreach ($data as $key => $value) { 95 | $key = preg_replace('#\.\d+#', '.*', (string) $key); 96 | $return[] = $key; 97 | } 98 | 99 | return $return; 100 | } 101 | 102 | private function getAllowedAttributes(): array 103 | { 104 | if (method_exists($this, 'possibleAttributes')) { 105 | return $this->possibleAttributes(); 106 | } 107 | 108 | $rules = app()->call([$this, 'rules']); 109 | 110 | return array_keys($rules); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/ArrayResource.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 12 | } 13 | 14 | public function getTransformed(): array 15 | { 16 | return $this->resource; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/ErrorResource.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 16 | 17 | $this->setDataWrapper(''); 18 | } 19 | 20 | public function toArray($request): array 21 | { 22 | /** 23 | * Merge additional info and unset it, due to it causing 24 | * `errors` array to be wrapped in unnecessary `data` property 25 | */ 26 | $result = array_merge( 27 | $this->resource, 28 | $this->additional ?? [], 29 | ); 30 | 31 | $this->additional([]); 32 | 33 | return $result; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/JsonResource.php: -------------------------------------------------------------------------------- 1 | resource instanceof TransformableContract) { 39 | return $this->transformToApi($this->resource); 40 | } 41 | 42 | return []; 43 | } 44 | 45 | public function toArray($request): array 46 | { 47 | $data = $this->getResourceData(); 48 | 49 | $relationsData = $this->getRelationsData(); 50 | 51 | if (! empty($relationsData)) { 52 | $data['relationships'] = $relationsData; 53 | } 54 | 55 | if (! empty($this->getDataWrapper())) { 56 | return [$this->getDataWrapper() => $data]; 57 | } 58 | 59 | return $data; 60 | } 61 | 62 | public function getDataWrapper(): string 63 | { 64 | return $this->dataWrapper; 65 | } 66 | 67 | public function setDataWrapper(string $dataWrapper): void 68 | { 69 | $this->dataWrapper = $dataWrapper; 70 | } 71 | 72 | public function withRelations(array $relations): self 73 | { 74 | $this->includedRelations = array_merge($this->includedRelations, $relations); 75 | 76 | return $this; 77 | } 78 | 79 | public function toResponse($request): JsonResponse 80 | { 81 | return (new ResourceResponse($this))->toResponse($request); 82 | } 83 | 84 | public function withResourceType(string $resourceType): self 85 | { 86 | $this->resourceType = $resourceType; 87 | 88 | return $this; 89 | } 90 | 91 | public function appendAdditional(array $data): self 92 | { 93 | $this->additional = array_merge_recursive($this->additional, $data); 94 | 95 | return $this; 96 | } 97 | 98 | public function jsonOptions(): int 99 | { 100 | return JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; 101 | } 102 | 103 | protected function getResourceData(): array 104 | { 105 | if ($this->resource instanceof TransformableContract) { 106 | $data = [ 107 | 'id' => $this->getResourceId(), 108 | 'type' => $this->getResourceType(), 109 | 'attributes' => $this->getTransformed(), 110 | ]; 111 | } elseif (is_null($this->resource)) { 112 | $data = [ 113 | 'id' => null, 114 | 'type' => null, 115 | 'attributes' => [], 116 | ]; 117 | } else { 118 | $data = [ 119 | 'id' => $this->getResourceId(), 120 | 'type' => $this->getResourceType(), 121 | 'attributes' => $this->getTransformed(), 122 | ]; 123 | } 124 | 125 | return $data; 126 | } 127 | 128 | protected function getRelationsData(): array 129 | { 130 | if (is_null($this->resource)) { 131 | return []; 132 | } 133 | 134 | $relations = []; 135 | 136 | $relationsChain = self::undot($this->includedRelations); 137 | foreach ($relationsChain as $currentRelation => $remainingRelationChain) { 138 | $methodName = 'include' . ucfirst($currentRelation); 139 | 140 | if (! method_exists($this, $methodName)) { 141 | throw new Exception('Relation method does not exist: ' . $methodName); 142 | } 143 | 144 | $remainingRelations = []; 145 | if (! empty($remainingRelationChain)) { 146 | $remainingRelations = array_keys(Arr::dot($remainingRelationChain)); 147 | } 148 | 149 | /** @var \Longman\LaravelLodash\Http\Resources\JsonResource $resource */ 150 | $resource = $this->$methodName(); 151 | if (is_null($resource)) { 152 | continue; 153 | } 154 | 155 | $resource->withRelations($remainingRelations); 156 | 157 | $relations[$currentRelation] = $resource; 158 | } 159 | 160 | return $relations; 161 | } 162 | 163 | protected function getResourceType(): string 164 | { 165 | if (! $this->resource instanceof TransformableContract) { 166 | return $this->resourceType; 167 | } 168 | 169 | $reflection = new ReflectionClass($this->resource->getModel()); 170 | 171 | return $reflection->getShortName(); 172 | } 173 | 174 | protected function getResourceId(): ?string 175 | { 176 | if ($this->resource instanceof UuidAsPrimaryContract) { 177 | return $this->resource->getUidString(); 178 | } 179 | 180 | if ($this->resource instanceof TransformableContract) { 181 | return (string) $this->resource->getKey(); 182 | } 183 | 184 | return null; 185 | } 186 | 187 | private static function undot(array $array): array 188 | { 189 | $result = []; 190 | foreach ($array as $item) { 191 | Arr::set($result, $item, []); 192 | } 193 | 194 | return $result; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/JsonResourceCollection.php: -------------------------------------------------------------------------------- 1 | collection as $item) { 22 | $item->setDataWrapper(''); 23 | } 24 | 25 | return ['data' => $this->collection]; 26 | } 27 | 28 | public function withRelations(array $relations = []): self 29 | { 30 | /** @var \Longman\LaravelLodash\Http\Resources\JsonResource $item */ 31 | foreach ($this->collection as $item) { 32 | $item->withRelations($relations); 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | public function appendAdditional(array $data): self 39 | { 40 | $this->additional = array_merge_recursive($this->additional, $data); 41 | 42 | return $this; 43 | } 44 | 45 | public function jsonOptions(): int 46 | { 47 | return JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; 48 | } 49 | 50 | protected function preparePaginatedResponse($request) 51 | { 52 | if ($this->preserveAllQueryParameters) { 53 | $this->resource->appends($request->query()); 54 | } elseif (! is_null($this->queryParameters)) { 55 | $this->resource->appends($this->queryParameters); 56 | } 57 | 58 | return (new PaginatedResourceResponse($this))->toResponse($request); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/Response/PaginatedResourceResponse.php: -------------------------------------------------------------------------------- 1 | json( 31 | $this->wrap( 32 | $this->resource->resolve($request), 33 | array_merge_recursive( 34 | $this->paginationInformation($request), 35 | $this->resource->with($request), 36 | $this->resource->additional, 37 | ), 38 | ), 39 | $this->calculateStatus(), 40 | [], 41 | $jsonOptions, 42 | ), function ($response) use ($request) { 43 | $response->original = $this->resource->resource->map(static function ($item) { 44 | return is_array($item) ? Arr::get($item, 'resource') : $item->resource; 45 | }); 46 | 47 | $this->resource->withResponse($request, $response); 48 | }); 49 | } 50 | 51 | protected function paginationInformation($request): array 52 | { 53 | $paginated = $this->resource->resource->toArray(); 54 | 55 | return [ 56 | 'meta' => $this->meta($paginated), 57 | ]; 58 | } 59 | 60 | protected function meta($paginated): array 61 | { 62 | return [ 63 | 'pagination' => [ 64 | 'total' => $paginated['total'] ?? null, 65 | 'count' => count($paginated['data']) ?? null, 66 | 'perPage' => $paginated['per_page'], 67 | 'currentPage' => $paginated['current_page'] ?? null, 68 | 'totalPages' => $paginated['last_page'] ?? null, 69 | 'links' => [ 70 | 'next' => $paginated['next_page_url'] ?? null, 71 | 'previous' => $paginated['prev_page_url'] ?? null, 72 | ], 73 | ], 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/Response/ResourceResponse.php: -------------------------------------------------------------------------------- 1 | json( 27 | $this->wrap( 28 | $this->resource->resolve($request), 29 | $this->resource->with($request), 30 | $this->resource->additional, 31 | ), 32 | $this->calculateStatus(), 33 | [], 34 | $jsonOptions, 35 | ), function ($response) use ($request) { 36 | $response->original = $this->resource->resource; 37 | 38 | $this->resource->withResponse($request, $response); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/SuccessResource.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 16 | 17 | $this->setDataWrapper(''); 18 | } 19 | 20 | public function toArray($request): array 21 | { 22 | /** 23 | * Merge additional info and unset it 24 | */ 25 | $result = array_merge( 26 | $this->resource, 27 | $this->additional ?? [], 28 | ); 29 | 30 | $this->additional([]); 31 | 32 | return $result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/TransformableArrayResource.php: -------------------------------------------------------------------------------- 1 | getTransformFields() as $from => $to) { 13 | $data[$to] = $this->resource[$from]; 14 | } 15 | 16 | return $data; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lodash/Http/Resources/TransformableContract.php: -------------------------------------------------------------------------------- 1 | getterMethod]). 23 | * If static getterMethod is defined in the resource class, it will be called and as a first argument will be passed TransformableContract $model, 24 | * Otherwise, the model's method will be used. 25 | */ 26 | protected static array $transformMapping = []; 27 | 28 | /** 29 | * Fields list for merging with general mapping during transformation o internal. Used for updating some fields. 30 | * Array values should be a column name from the database. 31 | */ 32 | protected static array $internalMapping = []; 33 | 34 | /** 35 | * Fields list for hiding in output. 36 | * Array values should be a column name from the database. 37 | */ 38 | protected static array $hideInOutput = []; 39 | 40 | public static function getTransformFields(): array 41 | { 42 | return static::$transformMapping; 43 | } 44 | 45 | public static function getInternalFields(): array 46 | { 47 | return static::$internalMapping; 48 | } 49 | 50 | public static function getHideInOutput(): array 51 | { 52 | return static::$hideInOutput; 53 | } 54 | 55 | public static function transformToApi(TransformableContract $model): array 56 | { 57 | $fields = static::getTransformFields(); 58 | $hiddenProperties = $model->getHidden(); 59 | $hideInOutput = static::getHideInOutput(); 60 | $transformed = []; 61 | foreach ($fields as $internalField => $transformValue) { 62 | if (in_array($internalField, $hiddenProperties, true)) { 63 | continue; 64 | } 65 | 66 | if (in_array($internalField, $hideInOutput, true)) { 67 | continue; 68 | } 69 | 70 | [$key, $value] = self::parseKeyValue($internalField, $transformValue, $model); 71 | 72 | $transformed[$key] = $value; 73 | } 74 | 75 | return $transformed; 76 | } 77 | 78 | public static function transformToInternal(array $fields): array 79 | { 80 | $transformFields = static::getTransformFields(); 81 | $transformFields = array_replace($transformFields, static::getInternalFields()); 82 | 83 | $modelTransformedFields = []; 84 | foreach ($transformFields as $key => $transformField) { 85 | if (is_array($transformField)) { 86 | $modelTransformedFields[key($transformField)] = $key; 87 | } else { 88 | $modelTransformedFields[$transformField] = $key; 89 | } 90 | } 91 | 92 | $transformed = []; 93 | foreach ($fields as $fieldKey => $postValue) { 94 | if (isset($modelTransformedFields[$fieldKey])) { 95 | $transformed[$modelTransformedFields[$fieldKey]] = $postValue; 96 | } 97 | } 98 | 99 | return $transformed; 100 | } 101 | 102 | private static function parseKeyValue(string $internalField, $transformValue, TransformableContract $model): array 103 | { 104 | if (is_array($transformValue)) { 105 | $key = key($transformValue); 106 | $method = $transformValue[$key]; 107 | if (method_exists(static::class, $method)) { // Check if getter exists in the resource class 108 | $value = call_user_func_array([static::class, $method], ['model' => $model]); 109 | } elseif (method_exists($model, $method)) { // Check if getter exists in the model class 110 | $value = $model->$method(); 111 | } else { 112 | throw new LogicException('Method ' . $method . ' does not available not for resource ' . static::class . ', not for model ' . $model::class); 113 | } 114 | } else { 115 | // Try to find getter for external field 116 | $method = 'get' . Str::snakeCaseToPascalCase($transformValue); 117 | if (method_exists($model, $method)) { 118 | $key = $transformValue; 119 | $value = $model->$method(); 120 | } else { 121 | // Call getter for internal field 122 | $method = 'get' . Str::snakeCaseToPascalCase($internalField); 123 | if (! method_exists($model, $method)) { 124 | throw new LogicException('Field ' . $internalField . ' getter (' . $method . ') does not available for model ' . $model::class); 125 | } 126 | $key = $transformValue; 127 | $value = $model->$method(); 128 | } 129 | } 130 | 131 | return [$key, $value]; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Lodash/Log/DateTimeFormatter.php: -------------------------------------------------------------------------------- 1 | getLogger()->getHandlers() as $handler) { 18 | $handler->setFormatter( 19 | new LineFormatter( 20 | LineFormatter::SIMPLE_FORMAT, 21 | 'Y-m-d H:i:s.u', 22 | ), 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Lodash/Middlewares/ApiLocale.php: -------------------------------------------------------------------------------- 1 | wantsJson()) { 23 | $locales = config('lodash.locales', []); 24 | $locale = $request->getPreferredLanguage(array_keys($locales)); 25 | app()->setLocale($locale); 26 | } 27 | 28 | /** @var \Symfony\Component\HttpFoundation\Response $response */ 29 | $response = $next($request); 30 | $locale = app()->getLocale(); 31 | $response->headers->set('Content-Language', $locale, true); 32 | 33 | if (! empty($locales[$locale])) { 34 | setlocale(LC_ALL, $locales[$locale]['full_locale']); 35 | } 36 | 37 | return $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lodash/Middlewares/SetRequestContext.php: -------------------------------------------------------------------------------- 1 | getRequestId(); 18 | $requestPlatform = $request->getClientPlatform(); 19 | 20 | logger()->withContext([ 21 | 'request-id' => $requestId, 22 | 'request-platform' => $requestPlatform, 23 | ]); 24 | 25 | app()->instance('request-id', $requestId); 26 | app()->instance('request-platform', $requestPlatform); 27 | 28 | /** @var \Illuminate\Http\Response $response */ 29 | $response = $next($request); 30 | $response->headers->set('Request-Id', $requestId, true); 31 | 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Lodash/Middlewares/SimpleBasicAuth.php: -------------------------------------------------------------------------------- 1 | getUser() !== $config['user'] || $request->getPassword() !== $config['password']) { 21 | $header = ['WWW-Authenticate' => 'Basic']; 22 | 23 | return response('You have to supply your credentials to access this resource.', Response::HTTP_UNAUTHORIZED, $header); 24 | } 25 | } 26 | 27 | return $next($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Lodash/Middlewares/XssSecurity.php: -------------------------------------------------------------------------------- 1 | getUri(); 21 | $excluded = config('lodash.xss.exclude_uris'); 22 | if (! empty($excluded)) { 23 | foreach ($excluded as $uri) { 24 | if (str_contains($requestUri, $uri)) { 25 | return $response; 26 | } 27 | } 28 | } 29 | 30 | /** @see http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx */ 31 | $response->headers->set('X-Frame-Options', config('lodash.xss.x_frame_options'), true); 32 | 33 | /** @see http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx */ 34 | $response->headers->set('X-Content-Type-Options', config('lodash.xss.x_content_type_options'), true); 35 | 36 | /** @see http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx */ 37 | $response->headers->set('X-XSS-Protection', config('lodash.xss.x_xss_protection'), true); 38 | 39 | return $response; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lodash/Queue/QueueServiceProvider.php: -------------------------------------------------------------------------------- 1 | {'register' . $connector . 'Connector'}($manager); 23 | } 24 | } 25 | 26 | protected function registerSqsFifoConnector(QueueManager $manager): void 27 | { 28 | $manager->addConnector('sqs.fifo', static function (): SqsFifoConnector { 29 | return new SqsFifoConnector(); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lodash/Queue/SqsFifo/Connectors/SqsFifoConnector.php: -------------------------------------------------------------------------------- 1 | getDefaultConfiguration($config); 24 | 25 | if ($config['key'] && $config['secret']) { 26 | $config['credentials'] = Arr::only($config, ['key', 'secret']); 27 | } 28 | 29 | $options = $config['options'] ?? []; 30 | unset($config['options']); 31 | 32 | $queue = Arr::get($options, 'type') === 'fifo' ? new SqsFifoQueue( 33 | new SqsClient($config), 34 | $config['queue'], 35 | $config['prefix'] ?? '', 36 | $options, 37 | ) : new SqsQueue( 38 | new SqsClient($config), 39 | $config['queue'], 40 | $config['prefix'] ?? '', 41 | $options, 42 | ); 43 | 44 | return $queue; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lodash/Queue/SqsFifo/SqsFifoQueue.php: -------------------------------------------------------------------------------- 1 | sqs = $sqs; 32 | $this->prefix = $prefix; 33 | $this->default = $default; 34 | $this->options = $options; 35 | 36 | if (Arr::get($this->options, 'polling') !== 'long') { 37 | return; 38 | } 39 | 40 | $this->sqs->setQueueAttributes([ 41 | 'Attributes' => [ 42 | 'ReceiveMessageWaitTimeSeconds' => Arr::get($this->options, 'wait_time', 20), 43 | ], 44 | 'QueueUrl' => $this->getQueue($default), 45 | ]); 46 | } 47 | 48 | /** 49 | * Push a raw payload onto the queue. 50 | * 51 | * @see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/general-recommendations.html 52 | * @see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queue-recommendations.html 53 | * 54 | * @param string $payload 55 | * @param string $queue 56 | * @param array $options 57 | * @return mixed 58 | */ 59 | public function pushRaw($payload, $queue = null, array $options = []) 60 | { 61 | $messageGroupId = $this->getMessageGroupId(); 62 | $messageDeduplicationId = $this->getMessageDeduplicationId($payload); 63 | 64 | $messageId = $this->sqs->sendMessage([ 65 | 'QueueUrl' => $this->getQueue($queue), 66 | 'MessageBody' => $payload, 67 | 'MessageGroupId' => $messageGroupId, 68 | 'MessageDeduplicationId' => $messageDeduplicationId, 69 | ])->get('MessageId'); 70 | 71 | return $messageId; 72 | } 73 | 74 | /** 75 | * FIFO queues don't support per-message delays, only per-queue delays 76 | * 77 | * @see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html 78 | */ 79 | public function later($delay, $job, $data = '', $queue = null) 80 | { 81 | throw new BadMethodCallException('FIFO queues don\'t support per-message delays, only per-queue delays'); 82 | } 83 | 84 | /** 85 | * Pop the next job off of the queue. 86 | * 87 | * @param string $queue 88 | * @return \Illuminate\Contracts\Queue\Job|null 89 | */ 90 | public function pop($queue = null) 91 | { 92 | $response = $this->sqs->receiveMessage([ 93 | 'QueueUrl' => $queue = $this->getQueue($queue), 94 | 'AttributeNames' => ['ApproximateReceiveCount'], 95 | 'MaxNumberOfMessages' => 1, 96 | 'WaitTimeSeconds' => Arr::get($this->options, 'wait_time', 20), 97 | ]); 98 | 99 | if (! is_null($response['Messages']) && count($response['Messages']) > 0) { 100 | return new SqsJob( 101 | $this->container, 102 | $this->sqs, 103 | $response['Messages'][0], 104 | $this->connectionName, 105 | $queue, 106 | ); 107 | } 108 | } 109 | 110 | protected function getMessageGroupId(): string 111 | { 112 | $messageGroupId = session()->getId(); 113 | if (empty($messageGroupId)) { 114 | $messageGroupId = str_random(40); 115 | } 116 | 117 | return $messageGroupId; 118 | } 119 | 120 | protected function getMessageDeduplicationId(string $payload): string 121 | { 122 | return config('app.debug') ? str_random(40) : sha1($payload); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Lodash/Redis/Connections/PhpRedisArrayConnection.php: -------------------------------------------------------------------------------- 1 | client->_hosts() as $master) { 26 | $async 27 | ? $this->command('rawCommand', [$master, 'flushdb', 'async']) 28 | : $this->command('flushdb', [$master]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Lodash/Redis/Connectors/PhpRedisConnector.php: -------------------------------------------------------------------------------- 1 | createRedisClusterInstance( 31 | array_map([$this, 'buildClusterConnectionString'], $config), 32 | $options, 33 | )); 34 | } 35 | 36 | // Use client-side sharding 37 | return new PhpRedisArrayConnection($this->createRedisArrayInstance( 38 | array_map([$this, 'buildRedisArrayConnectionString'], $config), 39 | $options, 40 | )); 41 | } 42 | 43 | protected function createClient(array $config): Redis 44 | { 45 | return tap(new Redis(), function (Redis $client) use ($config) { 46 | if ($client instanceof RedisFacade) { 47 | throw new LogicException( 48 | 'Please remove or rename the Redis facade alias in your "app" configuration file in order to avoid collision with the PHP Redis extension.', 49 | ); 50 | } 51 | 52 | $this->establishConnection($client, $config); 53 | 54 | if (! empty($config['password'])) { 55 | $client->auth((string) $config['password']); 56 | } 57 | 58 | if (! empty($config['database'])) { 59 | $client->select((int) $config['database']); 60 | } 61 | 62 | if (! empty($config['prefix'])) { 63 | $client->setOption(Redis::OPT_PREFIX, (string) $config['prefix']); 64 | } 65 | 66 | if (! empty($config['read_timeout'])) { 67 | $client->setOption(Redis::OPT_READ_TIMEOUT, (string) $config['read_timeout']); 68 | } 69 | 70 | if (array_key_exists('serializer', $config)) { 71 | $client->setOption(Redis::OPT_SERIALIZER, (string) $config['serializer']); 72 | } 73 | 74 | if (array_key_exists('compression', $config)) { 75 | $client->setOption(Redis::OPT_COMPRESSION, (string) $config['compression']); 76 | } 77 | 78 | if (array_key_exists('compression_level', $config)) { 79 | $client->setOption(Redis::OPT_COMPRESSION_LEVEL, (string) $config['compression_level']); 80 | } 81 | 82 | if (empty($config['scan'])) { 83 | $client->setOption(Redis::OPT_SCAN, (string) Redis::SCAN_RETRY); 84 | } 85 | }); 86 | } 87 | 88 | protected function buildRedisArrayConnectionString(array $server): string 89 | { 90 | return $server['host'] . ':' . $server['port']; 91 | } 92 | 93 | protected function createRedisArrayInstance(array $servers, array $options): RedisArray 94 | { 95 | $client = new RedisArray($servers, Arr::only($options, [ 96 | 'function', 97 | 'previous', 98 | 'retry_interval', 99 | 'lazy_connect', 100 | 'connect_timeout', 101 | 'read_timeout', 102 | 'algorithm', 103 | 'consistent', 104 | 'distributor', 105 | ])); 106 | 107 | if (! empty($options['password'])) { 108 | // @TODO: Remove after this will be implemented 109 | // https://github.com/phpredis/phpredis/issues/1508 110 | throw new InvalidArgumentException('RedisArray does not support authorization'); 111 | //$client->auth((string) $options['password']); 112 | } 113 | 114 | if (! empty($options['database'])) { 115 | $client->select((int) $options['database']); 116 | } 117 | 118 | if (! empty($options['prefix'])) { 119 | $client->setOption(Redis::OPT_PREFIX, (string) $options['prefix']); 120 | } 121 | 122 | if (array_key_exists('serializer', $options)) { 123 | $client->setOption(Redis::OPT_SERIALIZER, (string) $options['serializer']); 124 | } 125 | 126 | if (array_key_exists('compression', $options)) { 127 | $client->setOption(Redis::OPT_COMPRESSION, (string) $options['compression']); 128 | } 129 | 130 | if (array_key_exists('compression_level', $options)) { 131 | $client->setOption(Redis::OPT_COMPRESSION_LEVEL, (string) $options['compression_level']); 132 | } 133 | 134 | return $client; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Lodash/Redis/RedisManager.php: -------------------------------------------------------------------------------- 1 | driver) { 22 | 'predis' => new PredisConnector(), 23 | 'phpredis' => new PhpRedisConnector(), 24 | default => throw new InvalidArgumentException('Redis driver ' . $this->driver . ' does not exists'), 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Lodash/Redis/RedisServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('redis', static function ($app) { 15 | $config = $app->make('config')->get('database.redis'); 16 | 17 | return new RedisManager($app, Arr::pull($config, 'client', 'phpredis'), $config); 18 | }); 19 | 20 | $this->app->bind('redis.connection', static function ($app) { 21 | return $app['redis']->connection(); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lodash/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | ClearAll::class, 33 | 'command.lodash.db.clear' => DbClear::class, 34 | 'command.lodash.db.dump' => DbDump::class, 35 | 'command.lodash.db.restore' => DbRestore::class, 36 | 'command.lodash.log.clear' => LogClear::class, 37 | 'command.lodash.user.add' => UserAdd::class, 38 | 'command.lodash.user.password' => UserPassword::class, 39 | ]; 40 | 41 | public function boot(): void 42 | { 43 | $this->publishes([ 44 | __DIR__ . '/../config/config.php' => config_path('lodash.php'), 45 | ]); 46 | 47 | $this->registerBladeDirectives(); 48 | 49 | $this->loadTranslations(); 50 | 51 | $this->loadValidations(); 52 | } 53 | 54 | public function register(): void 55 | { 56 | $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'lodash'); 57 | 58 | $this->registerCommands(); 59 | 60 | $this->registerRequestMacros(); 61 | } 62 | 63 | protected function registerCommands(): void 64 | { 65 | if (! config('lodash.register.commands')) { 66 | return; 67 | } 68 | 69 | foreach ($this->commands as $name => $class) { 70 | $this->app->singleton($name, $class); 71 | } 72 | 73 | $this->commands(array_keys($this->commands)); 74 | } 75 | 76 | protected function registerBladeDirectives(): void 77 | { 78 | if (! config('lodash.register.blade_directives')) { 79 | return; 80 | } 81 | 82 | // Display relative time 83 | app('blade.compiler')->directive('datetime', static function ($expression) { 84 | return "toIso8601String() 85 | . '\' title=\'' . $expression . '\'>' 86 | . with($expression)->diffForHumans() . '' ?>"; 87 | }); 88 | 89 | // Pluralization helper 90 | app('blade.compiler')->directive('plural', static function ($expression) { 91 | $expression = trim($expression, '()'); 92 | [$count, $str, $spacer] = array_pad(preg_split('/,\s*/', $expression), 3, "' '"); 93 | 94 | return ""; 95 | }); 96 | } 97 | 98 | protected function registerRequestMacros(): void 99 | { 100 | if (! config('lodash.register.request_macros')) { 101 | return; 102 | } 103 | 104 | Request::macro('getInt', function (string $name, int $default = 0): int { 105 | return (int) $this->get($name, $default); 106 | }); 107 | 108 | Request::macro('getBool', function (string $name, bool $default = false): bool { 109 | return (bool) $this->get($name, $default); 110 | }); 111 | 112 | Request::macro('getFloat', function (string $name, float $default = 0): float { 113 | return (float) $this->get($name, $default); 114 | }); 115 | 116 | Request::macro('getString', function (string $name, string $default = ''): string { 117 | return (string) $this->get($name, $default); 118 | }); 119 | } 120 | 121 | protected function loadTranslations(): void 122 | { 123 | if (! config('lodash.register.translations')) { 124 | return; 125 | } 126 | 127 | $this->loadTranslationsFrom(__DIR__ . '/../translations', 'lodash'); 128 | 129 | $this->publishes([ 130 | __DIR__ . '/../translations' => resource_path('lang/vendor/lodash'), 131 | ]); 132 | } 133 | 134 | protected function loadValidations(): void 135 | { 136 | if (! config('lodash.register.validation_rules')) { 137 | return; 138 | } 139 | 140 | Validator::extend('type', function ($attribute, $value, $parameters, $validator): bool { 141 | $validator->addReplacer('type', static function ($message, $attribute, $rule, $parameters): string { 142 | return str_replace(':type', $parameters[0], $message); 143 | }); 144 | 145 | /** @var \Longman\LaravelLodash\Validation\StrictTypesValidator $validator */ 146 | $customValidator = $this->app->make(StrictTypesValidator::class); 147 | 148 | return $customValidator->validate($attribute, $value, $parameters); 149 | }, 'The :attribute must be of type :type'); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Lodash/Support/Arr.php: -------------------------------------------------------------------------------- 1 | [ // სახელობითი 25 | '*' => 'ი', 26 | ], 27 | self::DECLENSION_2 => [ // მოთხრობითი 28 | '*' => 'მა', 29 | ], 30 | self::DECLENSION_3 => [ // მიცემითი 31 | 'ადმინისტრაცია' => 'ადმინისტრაციას', 32 | 'თანამშრომელი' => 'თანამშრომელს', 33 | 'ინჟინერი' => 'ინჟინერს', 34 | 'ოფიცერი' => 'ოფიცერს', 35 | 'ბიბლიოთეკა' => 'ბიბლიოთეკას', 36 | 'დამლაგებელი' => 'დამლაგებელს', 37 | 'ბუღალტერი' => 'ბუღალტერს', 38 | 'ად' => 'ადს', 39 | 'ან' => 'ანს', 40 | 'ამ' => 'ამს', 41 | 'ბა' => 'ბას', 42 | 'გე' => 'გეს', 43 | 'დე' => 'დეს', 44 | 'დი' => 'დის', 45 | 'ვა' => 'ვას', 46 | 'ვი' => 'ვს', 47 | 'ია' => 'იას', 48 | 'კა' => 'კას', 49 | 'კი' => 'კს', 50 | 'კო' => 'კოს', 51 | 'ლა' => 'ლას', 52 | 'ლი' => 'ლს', 53 | 'ლე' => 'ლეს', 54 | 'ნი' => 'ნს', 55 | 'რი' => 'რს', 56 | 'სი' => 'სს', 57 | 'ტი' => 'ტს', 58 | 'უა' => 'უას', 59 | 'ში' => 'შს', 60 | 'ცი' => 'ცს', 61 | 'ძე' => 'ძეს', 62 | 'წე' => 'წეს', 63 | 'ხი' => 'ხს', 64 | '*' => 'ს', 65 | ], 66 | self::DECLENSION_4 => [ // ნათესაობითი 67 | 'ადმინისტრაცია' => 'ადმინისტრაციის', 68 | 'თანამშრომელი' => 'თანამშრომლის', 69 | 'ინჟინერი' => 'ინჟინრის', 70 | 'ოფიცერი' => 'ოფიცრის', 71 | 'ბიბლიოთეკა' => 'ბიბლიოთეკის', 72 | 'დამლაგებელი' => 'დამლაგებლის', 73 | 'ბუღალტერი' => 'ბუღალტრის', 74 | 'ად' => 'ადის', 75 | 'ან' => 'ანის', 76 | 'ამ' => 'ამის', 77 | 'ბა' => 'ბის', 78 | 'გე' => 'გის', 79 | 'დე' => 'დის', 80 | 'დი' => 'დის', 81 | 'ვა' => 'ვას', 82 | 'ვი' => 'ვის', 83 | 'ია' => 'იას', 84 | 'კა' => 'კას', 85 | 'კი' => 'კის', 86 | 'კო' => 'კოს', 87 | 'ლა' => 'ლის', 88 | 'ლი' => 'ლის', 89 | 'ლე' => 'ლის', 90 | 'ნი' => 'ნის', 91 | 'რი' => 'რის', 92 | 'სი' => 'სის', 93 | 'ტი' => 'ტის', 94 | 'უა' => 'უას', 95 | 'ში' => 'შის', 96 | 'ცი' => 'ცის', 97 | 'ძე' => 'ძის', 98 | 'წე' => 'წის', 99 | 'ხი' => 'ხის', 100 | '*' => 'ს', // ის 101 | ], 102 | self::DECLENSION_5 => [ // მოქმედებითი 103 | '*' => 'ით', 104 | ], 105 | self::DECLENSION_6 => [ // ვითარებითი 106 | '*' => 'ად', 107 | ], 108 | self::DECLENSION_7 => [ // წოდებითი 109 | '*' => 'ო', 110 | ], 111 | ]; 112 | 113 | public static function getAvailableDeclensions(): array 114 | { 115 | return [self::DECLENSION_1, self::DECLENSION_2, self::DECLENSION_3, self::DECLENSION_4, self::DECLENSION_5, self::DECLENSION_6, self::DECLENSION_7]; 116 | } 117 | 118 | public static function applyDeclension(string $word, int $declension): string 119 | { 120 | if (! in_array($declension, self::getAvailableDeclensions(), true)) { 121 | throw new InvalidArgumentException('Declension "' . $declension . '" is invalid'); 122 | } 123 | 124 | $rules = self::$declensionRules[$declension]; 125 | $turnedWord = null; 126 | foreach ($rules as $suffix1 => $suffix2) { 127 | if (Str::substr($word, -Str::length($suffix1)) === $suffix1) { 128 | $turnedWord = Str::substr($word, 0, -Str::length($suffix1)) . $suffix2; 129 | break; 130 | } 131 | } 132 | 133 | if (is_null($turnedWord)) { 134 | $turnedWord = $word . $rules['*']; 135 | } 136 | 137 | return $turnedWord; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Lodash/Support/Str.php: -------------------------------------------------------------------------------- 1 | = $finalLength) { 39 | return $value; 40 | } 41 | $diff = $finalLength - $length; 42 | $value = $dir === 'left' ? str_repeat('0', $diff) . $value : $value . str_repeat('0', $diff); 43 | 44 | return $value; 45 | } 46 | 47 | public static function formatBalance(?int $amount = 0, int $d = 2): string 48 | { 49 | //$amount = str_replace([',', ' '], ['.', ''], $amount); 50 | $amount = (float) $amount; 51 | $amount /= 100; 52 | $amount = number_format($amount, $d, '.', ''); 53 | 54 | return $amount; 55 | } 56 | 57 | public static function snakeCaseToPascalCase(string $string): string 58 | { 59 | $str = str_replace('_', '', ucwords($string, '_')); 60 | 61 | return $str; 62 | } 63 | 64 | public static function pascalCaseToSnakeCase(string $string): string 65 | { 66 | $string = strtolower(preg_replace('/(? $strLength) { 99 | $start = $strLength; 100 | } 101 | 102 | if ($length < 0) { 103 | $length = max(0, $strLength - $start + $length); 104 | } elseif ((is_null($length) === true) || ($length > $strLength)) { 105 | $length = $strLength; 106 | } 107 | 108 | if (($start + $length) > $strLength) { 109 | $length = $strLength - $start; 110 | } 111 | 112 | return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $strLength - $start - $length); 113 | } 114 | 115 | public static function limitMiddle(string $value, int $limit = 100, string $separator = '...'): string 116 | { 117 | $length = self::length($value); 118 | 119 | if ($length <= $limit) { 120 | return $value; 121 | } 122 | 123 | return self::substrReplaceUnicode($value, $separator, $limit / 2, $length - $limit); 124 | } 125 | 126 | public static function toDotNotation(string $value): string 127 | { 128 | $value = trim($value); 129 | $value = preg_replace('/\[(.+)\]/U', '.$1', $value); 130 | 131 | return $value; 132 | } 133 | 134 | /** 135 | * @param mixed $data 136 | * @return string 137 | */ 138 | public static function hash($data): string 139 | { 140 | if (is_array($data)) { 141 | sort($data); 142 | $data = serialize($data); 143 | } 144 | 145 | if (is_object($data)) { 146 | $data = serialize($data); 147 | } 148 | 149 | if (is_null($data)) { 150 | $data = 'NULL'; 151 | } 152 | 153 | return sha1((string) $data); 154 | } 155 | 156 | public static function convertToUtf8(string $string): string 157 | { 158 | if (! class_exists(Encoding::class)) { 159 | throw new RuntimeException('To use this method, package "neitanod/forceutf8" should be installed!'); 160 | } 161 | 162 | /** @link https://github.com/neitanod/forceutf8 */ 163 | $string = Encoding::toUTF8($string); 164 | 165 | $string = stripslashes(trim($string)); 166 | 167 | $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); 168 | 169 | return $string; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Lodash/Support/Uuid.php: -------------------------------------------------------------------------------- 1 | toString(); 30 | } 31 | 32 | public static function toBinary(string $uuid): string 33 | { 34 | $uuid = UuidFactory::fromString(strtolower($uuid)); 35 | 36 | return $uuid->getBytes(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lodash/Testing/Attributes.php: -------------------------------------------------------------------------------- 1 | */ 23 | private array $relations = []; 24 | private string $relationName; 25 | private int $count; 26 | 27 | public function __construct(array $params, string $relationName = 'root', int $count = 1) 28 | { 29 | $this->params = $params; 30 | $this->relationName = $relationName; 31 | $this->count = $count; 32 | $this->parseParameters($params); 33 | } 34 | 35 | public function getAttributes(array $extraAttrs = [], array $except = []): array 36 | { 37 | $array = array_replace_recursive($this->attributes, $extraAttrs); 38 | 39 | return Arr::except($array, $except); 40 | } 41 | 42 | public function hasAttribute(string $name): bool 43 | { 44 | return array_key_exists($name, $this->attributes); 45 | } 46 | 47 | public function getAttribute(string $name, mixed $default = null): mixed 48 | { 49 | return $this->attributes[$name] ?? $default; 50 | } 51 | 52 | public function setAttribute(string $name, $value): void 53 | { 54 | $this->attributes[$name] = $value; 55 | } 56 | 57 | public function getCount(): int 58 | { 59 | return $this->count; 60 | } 61 | 62 | public function getRelationName(): string 63 | { 64 | return $this->relationName; 65 | } 66 | 67 | public function getRelations(): array 68 | { 69 | return $this->relations; 70 | } 71 | 72 | public function hasRelation(string $name): bool 73 | { 74 | return array_key_exists($name, $this->getRelations()); 75 | } 76 | 77 | public function getRelation(string $name): self 78 | { 79 | if (! $this->hasRelation($name)) { 80 | throw new InvalidArgumentException('Relation "' . $name . '" does not found'); 81 | } 82 | 83 | return $this->getRelations()[$name]; 84 | } 85 | 86 | public function addRelation(string $name, int $count = 1, array $data = []): void 87 | { 88 | $this->params = array_replace_recursive($this->params, [self::RELATION_MARKER . $name . ':' . $count => $data]); 89 | } 90 | 91 | public function toArray(array $extraParams = [], array $except = []): array 92 | { 93 | $array = array_replace_recursive($this->params, $extraParams); 94 | 95 | return Arr::except($array, $except); 96 | } 97 | 98 | private function parseParameters(array $params): void 99 | { 100 | $attributes = []; 101 | $relations = []; 102 | foreach ($params as $key => $data) { 103 | if (str_starts_with((string) $key, self::RELATION_MARKER)) { 104 | $ex = explode(':', $key); 105 | if (! isset($ex[1])) { 106 | throw new InvalidArgumentException('Relation is empty'); 107 | } 108 | 109 | $relName = $ex[1]; 110 | $count = 1; 111 | if (isset($ex[2])) { 112 | $count = $ex[2] === self::DYNAMIC_COUNT_MARKER ? count($data) : (int) $ex[2]; 113 | } 114 | $attrs = $data; 115 | 116 | $relations[$relName] = new self($attrs, $relName, $count); 117 | } else { 118 | $attributes[$key] = $data; 119 | } 120 | } 121 | 122 | $this->attributes = $attributes; 123 | $this->relations = $relations; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Lodash/Testing/DataStructuresProvider.php: -------------------------------------------------------------------------------- 1 | connection($name); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Lodash/Testing/Response.php: -------------------------------------------------------------------------------- 1 | getDecodedContent(); 41 | 42 | PHPUnit::assertCount($count, $response['data'] ?? []); 43 | 44 | return $this; 45 | } 46 | 47 | public function assertJsonDataPagination(array $data): self 48 | { 49 | $response = $this->getDecodedContent(); 50 | 51 | PHPUnit::assertEquals($data['currentPage'], $response['meta']['pagination']['currentPage']); 52 | PHPUnit::assertEquals($data['perPage'], $response['meta']['pagination']['perPage']); 53 | PHPUnit::assertEquals($data['count'], $response['meta']['pagination']['count']); 54 | PHPUnit::assertEquals($data['total'], $response['meta']['pagination']['total']); 55 | 56 | return $this; 57 | } 58 | 59 | public function assertJsonDataCollectionStructure(array $data, bool $includePagerMeta = true): self 60 | { 61 | $struct = self::$successResponseStructure; 62 | $struct['data'] = [$data]; 63 | 64 | if ($includePagerMeta) { 65 | $struct['meta'] = [ 66 | 'pagination' => self::$pagerMetaStructure, 67 | ]; 68 | } 69 | 70 | PHPUnit::assertNotEmpty($this->getDecodedContent()['data'], 'Data collection is empty.'); 71 | 72 | $this->assertJsonStructure($struct); 73 | 74 | return $this; 75 | } 76 | 77 | public function assertJsonDataItemStructure(array $data): self 78 | { 79 | $struct = ['data' => $data]; 80 | 81 | $this->assertJsonStructure($struct); 82 | 83 | return $this; 84 | } 85 | 86 | public function assertJsonErrorStructure(?string $message = null, bool $includeMeta = false): self 87 | { 88 | $structure = self::$errorResponseStructure; 89 | if (! $includeMeta) { 90 | $structure = Arr::except($structure, 'meta'); 91 | } 92 | $this->assertJsonStructure($structure); 93 | $this->assertJson(['status' => 'error']); 94 | if ($message) { 95 | $this->assertJson(['message' => $message]); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | public function assertJsonValidationErrorStructure(array $errors = [], bool $includeMeta = false): self 102 | { 103 | $structure = self::$errorResponseStructure; 104 | if (! $includeMeta) { 105 | $structure = Arr::except($structure, 'meta'); 106 | } 107 | $structure = array_merge($structure, ['errors']); 108 | $this->assertJsonStructure($structure); 109 | $this->assertJson(['message' => __('validation.error'), 'status' => 'error']); 110 | if ($errors) { 111 | $this->assertJsonValidationErrors($errors); 112 | } 113 | 114 | return $this; 115 | } 116 | 117 | public function assertJsonSuccessStructure(?string $message = null, bool $includeMeta = false): self 118 | { 119 | $structure = self::$successResponseStructure; 120 | if (! $includeMeta) { 121 | $structure = Arr::except($structure, 'meta'); 122 | } 123 | $this->assertJsonStructure($structure); 124 | $this->assertJson(['status' => 'ok']); 125 | if ($message) { 126 | $this->assertJson(['message' => $message]); 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | public function getDecodedContent(): array 133 | { 134 | $content = $this->getContent(); 135 | 136 | return json_decode($content, true); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Lodash/Validation/StrictTypesValidator.php: -------------------------------------------------------------------------------- 1 | self::NATIVE_TYPE_INT, 18 | 'integer' => self::NATIVE_TYPE_INT, 19 | 'float' => self::NATIVE_TYPE_FLOAT, 20 | 'double' => self::NATIVE_TYPE_FLOAT, 21 | 'bool' => self::NATIVE_TYPE_BOOL, 22 | 'boolean' => self::NATIVE_TYPE_BOOL, 23 | ]; 24 | 25 | public function validate($attribute, $value, $parameters): bool 26 | { 27 | if (empty($parameters)) { 28 | return false; 29 | } 30 | 31 | $valueType = gettype($value); 32 | $requiredType = $parameters[0]; 33 | 34 | if (empty(static::TYPE_MAP[$requiredType]) || $this->isNativeTypeString($requiredType)) { 35 | return $valueType === $requiredType; 36 | } 37 | 38 | return $valueType === static::TYPE_MAP[$requiredType]; 39 | } 40 | 41 | protected function isNativeTypeString(string $type): bool 42 | { 43 | return in_array($type, [ 44 | static::NATIVE_TYPE_INT, 45 | static::NATIVE_TYPE_FLOAT, 46 | static::NATIVE_TYPE_BOOL, 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'en' => [ 8 | 'name' => 'English', 9 | 'native_name' => 'English', 10 | 'flag' => 'gb', 11 | 'locale' => 'en', 12 | 'canonical_locale' => 'en_GB', 13 | 'full_locale' => 'en_GB.UTF-8', 14 | ], 15 | 'ka' => [ 16 | 'name' => 'Georgian', 17 | 'native_name' => 'ქართული', 18 | 'flag' => 'ge', 19 | 'locale' => 'ka', 20 | 'canonical_locale' => 'ka_GE', 21 | 'full_locale' => 'ka_GE.UTF-8', 22 | ], 23 | ], 24 | 25 | 'debug' => [ 26 | 'ips' => explode(',', env('DEBUG_IP_LIST', '')), // IP list for enabling debug mode 27 | ], 28 | 29 | 'xss' => [ 30 | 'exclude_uris' => [], // Excluded URI's for Xss middleware 31 | 'x_frame_options' => 'DENY', // X-Frame-Options header value 32 | 'x_content_type_options' => 'nosniff', // X-Content-Type-Options header value 33 | 'x_xss_protection' => '1; mode=block', // X-XSS-Protection header value 34 | ], 35 | 36 | 'register' => [ 37 | 'blade_directives' => false, 38 | 'request_macros' => false, 39 | 'translations' => true, 40 | 'validation_rules' => true, 41 | 'commands' => true, 42 | ], 43 | ]; 44 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | addMessage($value, 'debug'); 12 | } 13 | } 14 | } 15 | 16 | if (! function_exists('get_db_query')) { 17 | function get_db_query(): string 18 | { 19 | if (! app()->bound('debugbar')) { 20 | return ''; 21 | } 22 | 23 | /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugbar */ 24 | $debugbar = app('debugbar'); 25 | 26 | try { 27 | $collector = $debugbar->getCollector('queries'); 28 | } catch (Throwable $e) { 29 | return ''; 30 | } 31 | 32 | $queries = $collector->collect(); 33 | if (empty($queries['statements'])) { 34 | return ''; 35 | } 36 | 37 | $query = end($queries['statements']); 38 | 39 | return $query['sql']; 40 | } 41 | } 42 | 43 | if (! function_exists('get_db_queries')) { 44 | function get_db_queries(): array 45 | { 46 | if (! app()->bound('debugbar')) { 47 | return []; 48 | } 49 | /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugbar */ 50 | $debugbar = app('debugbar'); 51 | 52 | try { 53 | $collector = $debugbar->getCollector('queries'); 54 | } catch (Throwable $e) { 55 | return []; 56 | } 57 | 58 | $queries = $collector->collect(); 59 | if (empty($queries['statements'])) { 60 | return []; 61 | } 62 | 63 | $list = []; 64 | foreach ($queries['statements'] as $query) { 65 | $list[] = $query['sql']; 66 | } 67 | 68 | return $list; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | getMockForTrait(UsesUuidAsPrimary::class); 18 | 19 | $uuidString = '055a40ec-94a1-4cd7-891f-11604409055e'; 20 | $uuid = Uuid::fromString($uuidString); 21 | $uuidBinary = $uuid->getBytes(); 22 | 23 | $this->assertTrue($mock->isUuidBinary($uuidBinary)); 24 | } 25 | 26 | #[Test] 27 | public function it_should_check_uuid_is_invalid_binary(): void 28 | { 29 | $mock = $this->getMockForTrait(UsesUuidAsPrimary::class); 30 | 31 | $uuidString = '055a40ec-94a1-4cd7-891f-11604409055e'; 32 | $uuid = Uuid::fromString($uuidString); 33 | $uuidBinary = $uuid->toString(); // Not binary 34 | 35 | $this->assertFalse($mock->isUuidBinary($uuidBinary)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Unit/Http/Requests/CustomRequest.php: -------------------------------------------------------------------------------- 1 | createValidator($rules, $attributes); 28 | 29 | try { 30 | $formRequest->validateResolved(); 31 | } catch (ValidationException $e) { 32 | if (! empty($errorAttributes)) { 33 | $errors = Arr::dot($e->errors()); 34 | 35 | foreach ($errorAttributes as $errorAttr) { 36 | $found = false; 37 | foreach ($errors as $error) { 38 | if (strpos($error, $errorAttr) !== false) { 39 | $found = true; 40 | break; 41 | } 42 | } 43 | $this->assertTrue($found, 'Failed asserting that validation errors contains "' . $errorAttr . '" string'); 44 | } 45 | } 46 | } 47 | 48 | $this->assertInstanceOf(CustomRequest::class, $formRequest); 49 | } 50 | 51 | public static function provideData(): array 52 | { 53 | return [ 54 | [ 55 | [ 56 | 'field1' => 'required', 57 | 'field2' => 'required', 58 | ], 59 | [ 60 | 'field1' => 'Some Data 1', 61 | 'field2' => 'Some Data 2', 62 | 'field3' => 'Some Data 3', 63 | ], 64 | [ 65 | 'field3', 66 | ], 67 | ], 68 | [ 69 | [ 70 | 'field1' => 'required', 71 | 'field2.subfield1' => 'required', 72 | 'field2.subfield2' => 'required', 73 | 'field2.subfield3' => 'required', 74 | ], 75 | [ 76 | 'field1' => 'Some Data 1', 77 | 'field2' => [ 78 | 'subfield1' => 'Some Data 2-1', 79 | 'subfield2' => 'Some Data 2-2', 80 | 'subfield3' => 'Some Data 2-3', 81 | 'subfield4' => 'Some Data 2-3', 82 | ], 83 | 'field3' => 'Some Data 3', 84 | ], 85 | [ 86 | 'field2.subfield4', 87 | 'field3', 88 | ], 89 | ], 90 | [ 91 | [ 92 | 'field1' => 'required', 93 | 'field2.*.subfield1' => 'required', 94 | 'field2.*.subfield2' => 'required', 95 | 'field2.*.subfield3' => 'required', 96 | 'field3' => 'required', 97 | 'field4' => 'required', 98 | ], 99 | [ 100 | 'field1' => 'Some Data 1', 101 | 'field2' => [ 102 | [ 103 | 'subfield1' => 'Some Data 2-1', 104 | 'subfield2' => 'Some Data 2-2', 105 | ], 106 | [ 107 | 'subfield3' => 'Some Data 2-3', 108 | 'subfield4' => 'Some Data 2-3', 109 | ], 110 | ], 111 | 'field3' => 'Some Data 3', 112 | 'field4' => 'Some Data 4', 113 | 'field5' => 'Some Data 3', 114 | ], 115 | [ 116 | 'field2.*.subfield4', 117 | 'field5', 118 | ], 119 | ], 120 | [ 121 | [ 122 | 'field1' => 'required', 123 | 'field2' => 'required', 124 | 'field2.*' => 'required', 125 | ], 126 | [ 127 | 'field1' => 'Some Data 1', 128 | 'field2' => [ 129 | [ 130 | 'subfield1' => 'Some Data 2-1', 131 | 'subfield2' => 'Some Data 2-2', 132 | ], 133 | [ 134 | 'subfield3' => 'Some Data 2-3', 135 | 'subfield4' => 'Some Data 2-3', 136 | ], 137 | ], 138 | 'field3' => 'Some Data 3', 139 | ], 140 | [ 141 | 'field3', 142 | ], 143 | ], 144 | ]; 145 | } 146 | 147 | private function createValidator(array $rules, array $attributes): CustomRequest 148 | { 149 | /** @var \Mockery\MockInterface|\Tests\Unit\Http\Requests\CustomRequest $formRequest */ 150 | $formRequest = Mockery::mock(CustomRequest::class)->makePartial(); 151 | $formRequest->shouldReceive('rules')->andReturn($rules); 152 | $formRequest->setContainer($this->app); 153 | $formRequest->initialize($attributes); 154 | $formRequest->setValidator(Validator::make($formRequest->all(), $rules)); 155 | 156 | app(Translator::class)->addLines([ 157 | 'validation.restrict_extra_attributes' => 'The :attribute key is not allowed in the request body.', 158 | ], 'en'); 159 | 160 | return $formRequest; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tests/Unit/Http/Resources/ArrayResourceTest.php: -------------------------------------------------------------------------------- 1 | 1, 22 | 'aa2' => 2, 23 | 'aa3' => 3, 24 | ]); 25 | 26 | $request = app(Request::class); 27 | $response = $resource->additional(['custom' => '1'])->withResourceType('customType')->toResponse($request); 28 | 29 | $expected = '{"data":{"id":null,"type":"customType","attributes":{"aa":1,"aa2":2,"aa3":3}},"custom":"1"}'; 30 | 31 | $this->assertInstanceOf(JsonResponse::class, $response); 32 | $this->assertSame($expected, $response->content()); 33 | } 34 | 35 | #[Test] 36 | public function it_should_return_unecaped_json(): void 37 | { 38 | $resource = new ArrayResource([ 39 | 'aa' => 'ერთი', 40 | 'aa2' => 'ორი', 41 | 'aa3' => 'სამი', 42 | ]); 43 | 44 | $request = app(Request::class); 45 | $response = $resource->additional(['custom' => '1'])->withResourceType('customType')->toResponse($request); 46 | 47 | $expected = '{"data":{"id":null,"type":"customType","attributes":{"aa":"ერთი","aa2":"ორი","aa3":"სამი"}},"custom":"1"}'; 48 | 49 | $this->assertInstanceOf(JsonResponse::class, $response); 50 | $this->assertSame($expected, $response->content()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/Http/Resources/ErrorResourceTest.php: -------------------------------------------------------------------------------- 1 | 1, 22 | 'aa2' => 2, 23 | 'aa3' => 3, 24 | ]); 25 | 26 | $request = app(Request::class); 27 | $response = $resource->additional(['custom' => '1'])->toResponse($request); 28 | 29 | $expected = '{"errors":{"general":{"aa":1,"aa2":2,"aa3":3}},"custom":"1"}'; 30 | 31 | $this->assertInstanceOf(JsonResponse::class, $response); 32 | $this->assertSame($expected, $response->content()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/Http/Resources/User.php: -------------------------------------------------------------------------------- 1 | id; 17 | } 18 | 19 | public function getName(): string 20 | { 21 | return $this->name; 22 | } 23 | 24 | public function getMail(): string 25 | { 26 | return $this->mail; 27 | } 28 | 29 | public function getHomeAddress(): string 30 | { 31 | return $this->home_address; 32 | } 33 | 34 | public function getCalculatedField(): int 35 | { 36 | return 7; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | 'name', 13 | 'mail' => 'mail', 14 | 'home_address' => 'homeAddress', 15 | 'calculated_field' => ['calculatedField' => 'getCalculatedField'], 16 | ]; 17 | 18 | public function __construct(User $resource) 19 | { 20 | $this->resource = $resource; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unit/Http/Resources/UserResourceWithHidden.php: -------------------------------------------------------------------------------- 1 | set('auth.simple', [ 19 | 'enabled' => true, 20 | 'user' => 'testuser', 21 | 'password' => 'testpass', 22 | ]); 23 | 24 | $response = $this->call('GET', 'url1', [], [], [], []); 25 | $response->assertStatus(401); 26 | } 27 | 28 | #[Test] 29 | public function it_should_return_access_denied_on_wrong_credentials() 30 | { 31 | config()->set('auth.simple', [ 32 | 'enabled' => true, 33 | 'user' => 'testuser', 34 | 'password' => 'testpass', 35 | ]); 36 | 37 | $response = $this->call('GET', 'url1', [], [], [], ['PHP_AUTH_USER' => 'testuser', 'PHP_AUTH_PW' => 'wrongpass']); 38 | $response->assertStatus(401); 39 | } 40 | 41 | #[Test] 42 | public function it_should_return_ok_on_disabled_auth() 43 | { 44 | config()->set('auth.simple', [ 45 | 'enabled' => false, 46 | 'user' => 'testuser', 47 | 'password' => 'testpass', 48 | ]); 49 | 50 | $response = $this->call('GET', 'url1', [], [], [], []); 51 | $response->assertStatus(200); 52 | $response->assertSeeText('ok'); 53 | } 54 | 55 | #[Test] 56 | public function it_should_return_ok_with_credentials() 57 | { 58 | config()->set('auth.simple', [ 59 | 'enabled' => true, 60 | 'user' => 'testuser', 61 | 'password' => 'testpass', 62 | ]); 63 | $response = $this->call('GET', 'url1', [], [], [], ['PHP_AUTH_USER' => 'testuser', 'PHP_AUTH_PW' => 'testpass']); 64 | $response->assertStatus(200); 65 | $response->assertSeeText('ok'); 66 | } 67 | 68 | protected function getEnvironmentSetUp($app) 69 | { 70 | /** @var \Illuminate\Routing\Router $router */ 71 | $router = $app['router']; 72 | 73 | $router->get('url1', static function () { 74 | return 'ok'; 75 | })->middleware(SimpleBasicAuth::class); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Unit/RedisTest.php: -------------------------------------------------------------------------------- 1 | createConnection('phpredis', [ 21 | 'cluster' => false, 22 | 'default' => [ 23 | 'host' => getenv('REDIS_HOST') ?: '127.0.0.1', 24 | 'port' => getenv('REDIS_PORT') ?: 6379, 25 | 'database' => 5, 26 | 'options' => [ 27 | 'prefix' => 'lodash:', 28 | 'serializer' => REDIS::SERIALIZER_IGBINARY, 29 | ], 30 | 'timeout' => 0.5, 31 | 'read_timeout' => 1.5, 32 | 'serializer' => 'igbinary', 33 | ], 34 | ]); 35 | /** @var \Redis $client */ 36 | $client = $redis->connection()->client(); 37 | 38 | $data = ['name' => 'Georgia']; 39 | $redis->set('country', $data, null, 60); 40 | 41 | $this->assertInstanceOf(Redis::class, $client); 42 | $this->assertEquals($client->getOption(Redis::OPT_SERIALIZER), Redis::SERIALIZER_IGBINARY); 43 | $this->assertEquals($redis->get('country'), $data); 44 | } 45 | 46 | #[Test] 47 | public function it_should_set_custom_serializer_for_cluster() 48 | { 49 | $redis = $this->createConnection('phpredis', [ 50 | 'clusters' => [ 51 | 'options' => [ 52 | 'lazy_connect' => true, 53 | 'connect_timeout' => 1, 54 | 'read_timeout' => 3, 55 | 'password' => getenv('REDIS_PASSWORD') ?: null, 56 | 'database' => 5, 57 | 'prefix' => 'lodash:', 58 | 'serializer' => REDIS::SERIALIZER_IGBINARY, 59 | ], 60 | 61 | 'default' => [ 62 | [ 63 | 'host' => getenv('REDIS_HOST') ?: '127.0.0.1', 64 | 'port' => getenv('REDIS_PORT') ?: 6379, 65 | ], 66 | ], 67 | ], 68 | ]); 69 | /** @var \RedisArray $client */ 70 | $client = $redis->connection()->client(); 71 | 72 | $data = ['name' => 'Georgia']; 73 | $redis->set('country2', $data, null, 60); 74 | 75 | $this->assertInstanceOf(RedisArray::class, $client); 76 | $this->assertEquals($redis->get('country2'), $data); 77 | foreach ($client->getOption(Redis::OPT_SERIALIZER) as $serializer) { 78 | $this->assertEquals($serializer, Redis::SERIALIZER_IGBINARY); 79 | } 80 | } 81 | 82 | private function createConnection(string $driver, array $config = []): RedisManager 83 | { 84 | if (version_compare($this->app->version(), '5.7', '<')) { 85 | $redis = new RedisManager($driver, $config); 86 | } else { 87 | $redis = new RedisManager($this->app, $driver, $config); 88 | } 89 | 90 | return $redis; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Unit/ServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 'clear-all', 20 | 'command.lodash.db.clear' => 'db:clear', 21 | 'command.lodash.db.dump' => 'db:dump', 22 | 'command.lodash.db.restore' => 'db:restore', 23 | 'command.lodash.log.clear' => 'log:clear', 24 | 'command.lodash.user.add' => 'user:add', 25 | 'command.lodash.user.password' => 'user:password', 26 | ]; 27 | 28 | $registered = $this->app[Kernel::class]->all(); 29 | foreach ($commands as $command => $name) { 30 | $this->assertTrue($this->app->bound($command)); 31 | $this->assertContains($name, array_keys($registered)); 32 | } 33 | } 34 | 35 | #[Test] 36 | public function check_if_request_has_macros() 37 | { 38 | $this->assertTrue(Request::hasMacro('getInt')); 39 | $this->assertTrue(Request::hasMacro('getBool')); 40 | $this->assertTrue(Request::hasMacro('getFloat')); 41 | $this->assertTrue(Request::hasMacro('getString')); 42 | } 43 | 44 | #[Test] 45 | public function check_blade_directives() 46 | { 47 | $directives = [ 48 | 'datetime', 49 | 'plural', 50 | ]; 51 | 52 | $registered = app('blade.compiler')->getCustomDirectives(); 53 | foreach ($directives as $directive) { 54 | $this->assertContains($directive, array_keys($registered)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/StrTest.php: -------------------------------------------------------------------------------- 1 | assertSame('0012345678', Str::addZeros($string, 10, 'left')); 22 | $this->assertSame('1234567800', Str::addZeros($string, 10, 'right')); 23 | } 24 | 25 | #[Test] 26 | public function format_balance(): void 27 | { 28 | $int = 12345; 29 | 30 | $this->assertSame('123.45', Str::formatBalance($int, 2)); 31 | $this->assertSame('123.450', Str::formatBalance($int, 3)); 32 | } 33 | 34 | #[Test] 35 | public function snake_case_to_pascal_case(): void 36 | { 37 | $string = 'Lorem_ipsum_dolores'; 38 | $this->assertSame('LoremIpsumDolores', Str::snakeCaseToPascalCase($string)); 39 | } 40 | 41 | #[Test] 42 | public function pascal_case_to_snake_case(): void 43 | { 44 | $string = 'LoremIpsumDolores'; 45 | $this->assertSame('lorem_ipsum_dolores', Str::camelCaseToSnakeCase($string)); 46 | 47 | $string = 'არჩევანისგარემოსუზრუნველყოფისსისტემა'; 48 | $this->assertSame('არჩევანისგარემოსუზრუნველყოფისსისტემა', Str::camelCaseToSnakeCase($string)); 49 | } 50 | 51 | #[Test] 52 | public function snake_case_to_camel_case(): void 53 | { 54 | $string = 'Lorem_ipsum_dolores'; 55 | $this->assertSame('loremIpsumDolores', Str::snakeCaseToCamelCase($string)); 56 | } 57 | 58 | #[Test] 59 | public function camel_case_to_snake_case(): void 60 | { 61 | $string = 'loremIpsumDolores'; 62 | $this->assertSame('lorem_ipsum_dolores', Str::camelCaseToSnakeCase($string)); 63 | 64 | $string = 'არჩევანისგარემოსუზრუნველყოფისსისტემა'; 65 | $this->assertSame('არჩევანისგარემოსუზრუნველყოფისსისტემა', Str::camelCaseToSnakeCase($string)); 66 | } 67 | 68 | #[Test] 69 | public function convert_spaces_to_dashes(): void 70 | { 71 | $string = 'Lorem Ipsum Dolores'; 72 | $this->assertSame('Lorem-Ipsum-Dolores', Str::convertSpacesToDashes($string)); 73 | 74 | $string = 'არჩევანის გარემოს უზრუნველყოფის სისტემა'; 75 | $this->assertSame('არჩევანის-გარემოს-უზრუნველყოფის-სისტემა', Str::convertSpacesToDashes($string)); 76 | } 77 | 78 | #[Test] 79 | public function limit_middle(): void 80 | { 81 | $string = 'არჩევანის გარემოს უზრუნველყოფის სისტემა'; 82 | 83 | $this->assertSame('არჩევანის ...ის სისტემა', Str::limitMiddle($string, 20, '...')); 84 | $this->assertSame(23, mb_strlen(Str::limitMiddle($string, 20, '...'))); 85 | } 86 | 87 | #[Test] 88 | public function hash(): void 89 | { 90 | $data = ['aa' => 1, 'bb' => 2, 'cc' => 3]; 91 | $this->assertSame('899a999da95e9f021fc63c6af006933fd4dc3aa1', Str::hash($data)); 92 | 93 | $data = new stdClass(); 94 | $data->aaa = 1; 95 | $data->bbb = 2; 96 | $data->ccc = 3; 97 | $this->assertSame('41d162b72eab4e7cfb6bb853d651fbaa2ae0573b', Str::hash($data)); 98 | 99 | $data = null; 100 | $this->assertSame('eef19c54306daa69eda49c0272623bdb5e2b341f', Str::hash($data)); 101 | } 102 | 103 | #[Test] 104 | public function to_dot_notation(): void 105 | { 106 | $string = 'data[first][]'; 107 | $this->assertSame('data.first[]', Str::toDotNotation($string)); 108 | 109 | $string = 'data[first][second]'; 110 | $this->assertSame('data.first.second', Str::toDotNotation($string)); 111 | 112 | $string = 'data[first][second]third'; 113 | $this->assertSame('data.first.secondthird', Str::toDotNotation($string)); 114 | 115 | $string = 'data[first][second][0]'; 116 | $this->assertSame('data.first.second.0', Str::toDotNotation($string)); 117 | } 118 | 119 | #[Test] 120 | public function convert_to_utf8(): void 121 | { 122 | $data = 'hello žš, გამარჯობა'; 123 | $this->assertSame('hello žš, გამარჯობა', Str::convertToUtf8($data)); 124 | 125 | $data = 'Hírek'; 126 | $this->assertSame('Hírek', Str::convertToUtf8($data)); 127 | 128 | $data = 'H�rek'; 129 | $this->assertSame('H�rek', Str::convertToUtf8($data)); 130 | 131 | $data = "Fédération Camerounaise de Football\n"; 132 | $this->assertSame('Fédération Camerounaise de Football', Str::convertToUtf8($data)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Unit/TestCase.php: -------------------------------------------------------------------------------- 1 | 'User Name', 18 | 'name2' => 'User Name 2', 19 | 'relations:choices:5' => [ 20 | 'choice_name' => 'Choice Name', 21 | 'choice_name2' => 'Choice Name 2', 22 | 'relations:groups:1' => [ 23 | 'group_name' => 'Group Name', 24 | 'group_name2' => 'Group Name 2', 25 | 'relations:lecturers:2' => [ 26 | 'lecturer_name' => 'Lecturer Name', 27 | 'lecturer_name2' => 'Lecturer Name 2', 28 | 'relations:settings:1' => [ 29 | 'settings_name' => 'Settings Name', 30 | 'settings_name2' => 'Settings Name 2', 31 | ], 32 | ], 33 | ], 34 | ], 35 | 'relations:profiles:3' => [ 36 | 'profile_name' => 'Profile Name', 37 | 'profile_name2' => 'Profile Name 2', 38 | 'relations:settings:1' => [ 39 | 'settings_name' => 'Settings Name', 40 | 'settings_name2' => 'Settings Name 2', 41 | ], 42 | ], 43 | ]; 44 | 45 | $parser = new Attributes($attributes); 46 | 47 | $this->assertEquals( 48 | [ 49 | 'name' => 'User Name', 50 | 'name2' => 'User Name 2', 51 | ], 52 | $parser->getAttributes(), 53 | ); 54 | $this->assertEquals( 55 | [ 56 | 'name' => 'User Name', 57 | 'name2' => 'User Name 2', 58 | 'custom_attr' => 'Custom Attr for Merging', 59 | ], 60 | $parser->getAttributes(['custom_attr' => 'Custom Attr for Merging']), 61 | ); 62 | 63 | $this->assertEquals(false, $parser->hasRelation('missing_relation')); 64 | $this->assertEquals(true, $parser->hasRelation('choices')); 65 | 66 | // Choices 67 | $relation = $parser->getRelation('choices'); 68 | $this->assertEquals( 69 | [ 70 | 'choice_name' => 'Choice Name', 71 | 'choice_name2' => 'Choice Name 2', 72 | ], 73 | $relation->getAttributes(), 74 | ); 75 | $this->assertEquals(5, $relation->getCount()); 76 | 77 | // Groups 78 | $relation = $relation->getRelation('groups'); 79 | $this->assertEquals( 80 | [ 81 | 'group_name' => 'Group Name', 82 | 'group_name2' => 'Group Name 2', 83 | ], 84 | $relation->getAttributes(), 85 | ); 86 | $this->assertEquals(1, $relation->getCount()); 87 | 88 | // Lecturers 89 | $relation = $relation->getRelation('lecturers'); 90 | $this->assertEquals( 91 | [ 92 | 'lecturer_name' => 'Lecturer Name', 93 | 'lecturer_name2' => 'Lecturer Name 2', 94 | ], 95 | $relation->getAttributes(), 96 | ); 97 | $this->assertEquals(2, $relation->getCount()); 98 | 99 | // Settings 100 | $relation = $relation->getRelation('settings'); 101 | $this->assertEquals( 102 | [ 103 | 'settings_name' => 'Settings Name', 104 | 'settings_name2' => 'Settings Name 2', 105 | ], 106 | $relation->getAttributes(), 107 | ); 108 | $this->assertEquals(1, $relation->getCount()); 109 | 110 | // Profiles 111 | $relations2 = $parser->getRelations(); 112 | $this->assertEquals( 113 | [ 114 | 'profile_name' => 'Profile Name', 115 | 'profile_name2' => 'Profile Name 2', 116 | ], 117 | $relations2['profiles']->getAttributes(), 118 | ); 119 | $this->assertEquals(3, $relations2['profiles']->getCount()); 120 | 121 | // Profiles 122 | $relations2 = $relations2['profiles']->getRelations(); 123 | $this->assertEquals( 124 | [ 125 | 'settings_name' => 'Settings Name', 126 | 'settings_name2' => 'Settings Name 2', 127 | ], 128 | $relations2['settings']->getAttributes(), 129 | ); 130 | $this->assertEquals(1, $relations2['settings']->getCount()); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Unit/Testing/ItemStructuresProvider.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'uid', 17 | 'firstName', 18 | 'lastName', 19 | 'fullName', 20 | 'email', 21 | 'avatar', 22 | 'photoUrl', 23 | ], 24 | ]; 25 | 26 | private array $user_profile_structure = [ 27 | 'type', 28 | 'id', 29 | 'attributes' => [ 30 | 'type', 31 | 'degree', 32 | ], 33 | ]; 34 | 35 | private array $profile_status_structure = [ 36 | 'type', 37 | 'id', 38 | 'attributes' => [ 39 | 'status', 40 | 'message', 41 | ], 42 | ]; 43 | 44 | // phpcs:enable 45 | 46 | public function getUserStructure(array $relations = []): array 47 | { 48 | $structure = $this->user_structure; 49 | 50 | DataStructuresBuilder::includeNestedRelations($this, $structure, $relations); 51 | 52 | return $structure; 53 | } 54 | 55 | public function getUserProfileStructure(array $relations = []): array 56 | { 57 | $structure = $this->user_profile_structure; 58 | 59 | DataStructuresBuilder::includeNestedRelations($this, $structure, $relations); 60 | 61 | return $structure; 62 | } 63 | 64 | public function getProfileStatusStructure(array $relations = []): array 65 | { 66 | $structure = $this->profile_status_structure; 67 | 68 | DataStructuresBuilder::includeNestedRelations($this, $structure, $relations); 69 | 70 | return $structure; 71 | } 72 | } 73 | --------------------------------------------------------------------------------