├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── test-commands.sh ├── composer.json ├── lucid └── src ├── Bus ├── Marshal.php ├── ServesFeatures.php └── UnitDispatcher.php ├── Console ├── Command.php └── Commands │ ├── ChangeSourceNamespaceCommand.php │ ├── ControllerMakeCommand.php │ ├── FeatureDeleteCommand.php │ ├── FeatureDescribeCommand.php │ ├── FeatureMakeCommand.php │ ├── FeaturesListCommand.php │ ├── GeneratorCommand.php │ ├── InitCommandTrait.php │ ├── InitMicroCommand.php │ ├── InitMonolithCommand.php │ ├── JobDeleteCommand.php │ ├── JobMakeCommand.php │ ├── MigrationMakeCommand.php │ ├── ModelDeleteCommand.php │ ├── ModelMakeCommand.php │ ├── NewCommand.php │ ├── OperationDeleteCommand.php │ ├── OperationMakeCommand.php │ ├── PolicyDeleteCommand.php │ ├── PolicyMakeCommand.php │ ├── RequestDeleteCommand.php │ ├── RequestMakeCommand.php │ ├── ServiceDeleteCommand.php │ ├── ServiceMakeCommand.php │ └── ServicesListCommand.php ├── Domains └── Http │ └── Jobs │ ├── RespondWithJsonErrorJob.php │ ├── RespondWithJsonJob.php │ └── RespondWithViewJob.php ├── Entities ├── Domain.php ├── Entity.php ├── Feature.php ├── Job.php ├── Model.php ├── Operation.php ├── Policy.php ├── Request.php └── Service.php ├── Events ├── FeatureStarted.php ├── JobStarted.php └── OperationStarted.php ├── Exceptions └── InvalidInputException.php ├── Filesystem.php ├── Finder.php ├── Generators ├── ControllerGenerator.php ├── DirectoriesGeneratorTrait.php ├── FeatureGenerator.php ├── Generator.php ├── JobGenerator.php ├── MicroGenerator.php ├── ModelGenerator.php ├── MonolithGenerator.php ├── OperationGenerator.php ├── PolicyGenerator.php ├── RequestGenerator.php ├── ServiceGenerator.php └── stubs │ ├── broadcastserviceprovider.stub │ ├── controller.plain.stub │ ├── controller.resource.stub │ ├── feature-test.stub │ ├── feature.stub │ ├── foundation.serviceprovider.stub │ ├── job-queueable.stub │ ├── job-test.stub │ ├── job.stub │ ├── model-8.stub │ ├── model.stub │ ├── operation-queueable.stub │ ├── operation-test.stub │ ├── operation.stub │ ├── policy.stub │ ├── request.stub │ ├── routes-api.stub │ ├── routes-channels.stub │ ├── routes-console.stub │ ├── routes-web.stub │ ├── routeserviceprovider.stub │ ├── serviceprovider-8.stub │ ├── serviceprovider.stub │ └── welcome.blade.stub ├── Parser.php ├── Providers └── RouteServiceProvider.php ├── Str.php ├── Testing ├── MockMe.php ├── UnitMock.php └── UnitMockRegistry.php ├── Units ├── Controller.php ├── Feature.php ├── Job.php ├── Model.php ├── Operation.php ├── QueueableJob.php └── QueueableOperation.php └── Validation ├── Validation.php └── Validator.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: mulkave 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: tests 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | php: [7.0.21, 7.1, 7.2, 7.3, 7.4, 8.0] 22 | stability: [prefer-lowest, prefer-stable] 23 | laravel: [5.5, 6.x, 7.x, 8.x] 24 | exclude: 25 | - laravel: 5.5 26 | php: 8.0 27 | - laravel: 6.x 28 | php: 7.0.21 29 | - laravel: 6.x 30 | php: 7.1 31 | - laravel: 7.x 32 | php: 7.0.21 33 | - laravel: 7.x 34 | php: 7.1 35 | - laravel: 8.x 36 | php: 7.0.21 37 | - laravel: 8.x 38 | php: 7.1 39 | - laravel: 8.x 40 | php: 7.2 41 | 42 | name: L${{ matrix.laravel }} - PHP ${{ matrix.php }} - ${{ matrix.stability }} 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | 47 | - name: Setup PHP 48 | uses: shivammathur/setup-php@v2 49 | with: 50 | php-version: ${{ matrix.php }} 51 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd 52 | tools: composer:v2 53 | coverage: none 54 | 55 | - name: Install Laravel ${{ matrix.laravel }} 56 | run: | 57 | composer config -g --no-plugins allow-plugins.kylekatarnls/update-helper true 58 | composer create-project --prefer-dist laravel/laravel=${{ matrix.laravel }} laravel-${{ matrix.laravel }} --no-interaction 59 | 60 | - name: Require Lucid 61 | run: | 62 | cd laravel-${{ matrix.laravel }} 63 | composer config prefer-stable false 64 | composer config minimum-stability dev 65 | composer config repositories.lucid '{"type": "path", "url": "$GITHUB_WORKSPACE", "options": {"symlink": true}}' 66 | composer require lucidarch/lucid 67 | 68 | - name: Run Tests 69 | run: | 70 | chmod +x $GITHUB_WORKSPACE/bin/test-commands.sh 71 | cp $GITHUB_WORKSPACE/bin/test-commands.sh laravel-${{ matrix.laravel }} 72 | cd laravel-${{ matrix.laravel }} 73 | ./test-commands.sh 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mulkave@pm.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Bug & Issue Reports 2 | 3 | To encourage active collaboration, Lucid strongly encourages contribution through [pull requests](#which-branch-and-how-to-contribute). 4 | "Bug reports" may be searched or created in [issues](https://github.com/lucidarch/lucid/issues) or sent in the form of a [pull request](#which-branch-and-how-to-contribute) containing a failing test or steps to reproduce the bug. 5 | 6 | If you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. 7 | 8 | ⏱ PRs and issues are usually checked about three times a week so there is a high chance yours will be picked up soon. 9 | 10 | The Lucid Architecture source code is on GitHub as [lucidarch/lucid](https://github.com/lucidarch/lucid). 11 | 12 | ## Support Questions 13 | 14 | Lucid Architecture's GitHub issue trackers are not intended to provide help or support. Instead, use one of the following channels: 15 | 16 | - [Discussions](https://github.com/lucidarch/lucid/discussions) is where most conversations takes place 17 | - For a chat hit us on our official [Slack workspace](https://lucid-slack.herokuapp.com/) in the `#support` channel 18 | - If you prefer StackOverflow to post your questions you may use [#lucidarch](https://stackoverflow.com/questions/tagged/lucidarch) to tag them 19 | 20 | ## Core Development Discussion 21 | 22 | You may propose new features or improvements of existing Lucid Architecture behaviour in the [Lucid Discussins](https://github.com/lucidarch/lucid/discussions). 23 | If you propose a new feature, please be willing to implement at least some of the code that would be needed to complete the feature, or collaborate on active ideation in the meantime. 24 | 25 | Informal discussion regarding bugs, new features, and implementation of existing features takes place in the `#internals` channel of the [Lucid Slack workspace](https://lucid-slack.herokuapp.com/). 26 | Abed Halawi, the maintainer of Lucid, is typically present in the channel on weekdays from 8am-5pm EEST (Eastern European Summer Time), and sporadically present in the channel at other times. 27 | 28 | ## Which Branch? And How To Contribute 29 | 30 | The `main` branch is what contains the latest live version and is the one that gets released. 31 | 32 | - Fork this repository 33 | - Clone the forked repository to where you'll edit your code 34 | - Create a branch for your edits (e.g. `feature/queueable-units`, `fix/issue-31`) 35 | - Commit your changes and their tests (if applicable) with meaningful short messages 36 | - Push your branch `git push origin feature/queueable-units` 37 | - Open a [PR](https://github.com/lucidarch/lucid/compare) to the `main` branch, which will run tests for your edits 38 | 39 | ⏱ PRs and issues are usually checked about three times a week. 40 | 41 | 42 | ### Setup for Development 43 | 44 | Following are the steps to setup for development on Lucid: 45 | 46 | > Assuming we're in `~/dev` directory... 47 | 48 | - Clone the forked repository `[your username]/lucid` which will create a `lucid` folder at `~/dev/lucid` 49 | - Create a Laravel project to test your implementation in it `composer create-project laravel/laravel myproject` 50 | - Connect the created Laravel project to the local Lucid installation; in the Laravel project's `composer.json` 51 | ```json 52 | "require": { 53 | "...": "", 54 | "lucidarch/lucid": "@dev" 55 | }, 56 | "repositories": [ 57 | { 58 | "type": "path", 59 | "url": "~/dev/lucid", 60 | "options": { 61 | "symlink": true 62 | } 63 | } 64 | ], 65 | "minimum-stability": "dev", 66 | ``` 67 | > Make sure you change the `url` to the absolute path of your directory 68 | 69 | - Run `composer update` to create the symlink 70 | 71 | Now all your changes in the lucid directory will take effect automatically in the project. 72 | 73 | ## Security Vulnerabilities 74 | 75 | If you discover a security vulnerability within Lucid, please send an email to Abed Halawi at [halawi.abed@gmail.com](mailto:halawi.abed@gmail.com). 76 | All security vulnerabilities will be promptly addressed. 77 | 78 | ## Coding Style 79 | 80 | Lucid Architecture follows the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard and the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading standard. 81 | 82 | ### PHPDoc 83 | 84 | Below is an example of a valid Lucid Architecture documentation block. Note that the `@param` attribute is followed by two spaces, the argument type, two more spaces, and finally the variable name: 85 | 86 | ```php 87 | /** 88 | * Register a binding with the container. 89 | * 90 | * @param string|array $abstract 91 | * @param \Closure|string|null $concrete 92 | * @param bool $shared 93 | * @return void 94 | * 95 | * @throws \Exception 96 | */ 97 | public function bind($abstract, $concrete = null, $shared = false) 98 | { 99 | // 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-alpine 2 | 3 | RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ 4 | && pecl install xdebug \ 5 | && docker-php-ext-enable xdebug \ 6 | && apk del -f .build-deps 7 | 8 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Abed Halawi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Documentation 5 | Slack Chat 6 | Build Status 7 | Latest Stable Version 8 | License 9 |

10 | 11 | --- 12 | 13 | - Website: https://lucidarch.dev 14 | - Documentation: https://docs.lucidarch.dev 15 | - Social: we share updates & interesting content from the web 16 | - Twitter: [@lucid_arch](https://twitter.com/lucid_arch) & [#lucidarch](https://twitter.com/search?q=%23lucidarch) 17 | - Reddit: [/r/lucidarch](https://www.reddit.com/r/lucidarch/) 18 | 19 | ## Table of Contents 20 | 21 | * [About Lucid](#about-lucid) 22 | * [Concept](#concept) 23 | * [Table of Contents](#table-of-contents) 24 | * [Position](#position) 25 | * [The Stack](#the-stack) 26 | * [Framework](#framework) 27 | * [Foundation](#foundation) 28 | * [Domains](#domains) 29 | * [Services](#services) 30 | * [Features](#features) 31 | * [Operations](#operations) 32 | * [Data](#data) 33 | * [Benefits](#benefits) 34 | * [Organization](#organization) 35 | * [Reuse & Replace](#reuse--replace) 36 | * [Boundaries](#boundaries) 37 | * [Multitenancy](#multitenancy) 38 | * [Contribute](#contribute) 39 | * [Bug & Issue Reports](#bug--issue-reports) 40 | * [Support Questions](#support-questions) 41 | * [Core Development Discussion](#core-development-discussion) 42 | * [Which Branch? And How To Contribute](#which-branch-and-how-to-contribute) 43 | * [Setup for Development](#setup-for-development) 44 | * [Security Vulnerabilities](#security-vulnerabilities) 45 | * [Coding Style](#coding-style) 46 | * [PHPDoc](#phpdoc) 47 | * [Code of Conduct](#code-of-conduct) 48 | 49 | 50 | ## About Lucid 51 | Lucid is a software architecture to build scalable Laravel projects. It incorporates **Command Bus** and **Domain Driven Design** 52 | at the core, upon which it builds a stack of directories and classes to organize business logic. 53 | It also derives from **SOA (Service Oriented Architecture)** the notion of encapsulating functionality 54 | within a service and enriches the concept with more than the service being a class. 55 | 56 | **Use Lucid to:** 57 | 58 | - Write clean code effortlessly 59 | - Protect your code from deterioriting over time 60 | - Review code in fractions of the time typically required 61 | - Incorporate proven practices and patterns in your applications 62 | - Navigate code and move between codebases without feeling astranged 63 | 64 | ## Concept 65 | 66 | This architecture is in an amalgamation of best practices, design patterns and proven methods. 67 | 68 | 69 | - **Command Bus**: to dispatch units of work. In Lucid terminology these units will be a `Feature`, `Job` or `Operation`. 70 | - **Domain Driven Design**: to organize the units of work by categorizing them according to the topic they belong to. 71 | - **Service Oriented Architecture**: to encapsulate and manage functionalities of the same purpose with their required resources (routes, controllers, views, datatbase migrations etc.) 72 | 73 | If you prefer a video, watch the announcement at Laracon EU 2016: 74 | 75 |

76 | 77 | 78 | 79 |

80 | 81 | --- 82 | 83 | ### Table of Contents 84 | 85 | - [Position](#position) 86 | - [The Stack](#the-stack) 87 | - [Framework](#framework) 88 | - [Foundation](#foundation) 89 | - [Domains](#domains) 90 | - [Services](#services) 91 | - [Features](#features) 92 | - [Data](#data) 93 | - [Benefits](#benefits) 94 | - [Organization](#organization) 95 | - [Reuse & Replace](#reuse--replace) 96 | - [Boundaries](#boundaries) 97 | - [Multitenancy](#multitenancy) 98 | 99 | ## Position 100 | 101 | In a typical MVC application, Lucid will be the bond between the application's entrypoints and the units that do the work, 102 | securing code form meandring in drastic directions: 103 | 104 | ![Lucid MVC Position](https://raw.githubusercontent.com/lucidarch/artwork/main/material/concept/mvc-position.png) 105 | 106 | ## The Stack 107 | 108 | At a glance... 109 | 110 | ![Lucid Stack](https://raw.githubusercontent.com/lucidarch/artwork/main/material/concept/stack.png) 111 | 112 | ### Framework 113 | 114 | Provides the "kernel" to do the heavy lifting of the tedious stuff such as request/response lifecycle, dependency 115 | injection, and other core functionalities. 116 | 117 | ### Foundation 118 | 119 | Extends the framework to provide higher level abstractions that are custom to the application and can be shared 120 | across the entire stack rather than being case-specific. 121 | 122 | Examples of what could go into foundation are: 123 | - `DateTime` a support class for common date and time functions 124 | - `JsonSerializableInterface` that is used to identify an object to be serializable from and to JSON format 125 | 126 | ### Domains 127 | 128 | Provide separation to categorize jobs and corresponding classes that belong to the same topic. A domain operates in isolation 129 | from other domains and exposes its functionalities to features and operations through Lucid jobs only. 130 | 131 | Consider the structure below for an example on what a domain may look like: 132 | 133 | ``` 134 | app/Domains/GitHub 135 | ├── GitHubClient 136 | ├── Jobs 137 | │ ├── FetchGitHubRepoInfoJob 138 | │ └── LoginWithGitHubJob 139 | ├── Exceptions 140 | │ ├── InvalidTokenException 141 | │ └── RepositoryNotFoundException 142 | └── Tests 143 | └── GitHubClientTest 144 | └── Jobs 145 | ├── FetchGitHubReposJobTest 146 | └── LoginWithGitHubJobTest 147 | ``` 148 | 149 | [documentation](https://docs.lucidarch.dev/domains/) contains more details on working with domains. 150 | 151 | ### Services 152 | 153 | Are directories rich in functionality, used to separate a [Monolith]({{}}) into 154 | areas of focus in a multi-purpose application. 155 | 156 | Consider the example of an application where we enter food recipes and would want our members to have discussions in a forum, 157 | we would have two services: *1) Kitchen, 2) Forum* where the kitchen would manage all that's related to recipes, and forum is obvious: 158 | 159 | ``` 160 | app/Services 161 | ├── Forum 162 | └── Kitchen 163 | ``` 164 | 165 | and following is a single service's structure, highlighted are the Lucid specific directories: 166 | 167 |
168 | app/Services/Forum
169 | ├── Console
170 | │   └── Commands
171 | ├── Features
172 | ├── Operations
173 | ├── Http
174 | │   ├── Controllers
175 | │   └── Middleware
176 | ├── Providers
177 | │   ├── KitchenServiceProvider
178 | │   ├── BroadcastServiceProvider
179 | │   └── RouteServiceProvider
180 | ├── Tests
181 | │   └── Features
182 | │   └── Operations
183 | ├── database
184 | │   ├── factories
185 | │   ├── migrations
186 | │   └── seeds
187 | ├── resources
188 | │   ├── lang
189 | │   └── views
190 | └── routes
191 |     ├── api
192 |     ├── channels
193 |     ├── console
194 |     └── web
195 | 
196 | 197 | [documentation](https://docs.lucidarch.dev/services/) has more examples of services and their contents. 198 | 199 | ### Features 200 | 201 | Represent a human-readable application feature in a class. It contains the logic that implements the feature but with the least 202 | amount of detail, by running jobs from domains and operations at the application or service level. 203 | 204 | Serving the Feature class will be the only line in a controller's method (in MVC), consequently achieving the thinnest form of controllers. 205 | 206 | ```php 207 | class AddRecipeFeature extends Feature 208 | { 209 | public function handle(AddRecipe $request) 210 | { 211 | $price = $this->run(CalculateRecipePriceOperation::class, [ 212 | 'ingredients' => $request->input('ingredients'), 213 | ]); 214 | 215 | $this->run(SaveRecipeJob::class, [ 216 | 'price' => $price, 217 | 'user' => Auth::user(), 218 | 'title' => $request->input('title'), 219 | 'ingredients' => $request->input('ingredients'), 220 | 'instructions' => $request->input('instructions'), 221 | ]); 222 | 223 | return $this->run(RedirectBackJob::class); 224 | } 225 | } 226 | ``` 227 | 228 | [documentation](https://docs.lucidarch.dev/features/) about features expands on how to serve them as classes from anywhere. 229 | 230 | ### Operations 231 | 232 | Their purpose is to increase the degree of code reusability by piecing jobs together to provide composite functionalities from across domains. 233 | 234 | ```php 235 | class NotifySubscribersOperation extends Operation 236 | { 237 | private int $authorId; 238 | 239 | public function __construct(int $authorId) 240 | { 241 | $this->authorId = $authorId; 242 | } 243 | 244 | /** 245 | * Sends notifications to subscribers. 246 | * 247 | * @return int Number of notification jobs enqueued. 248 | */ 249 | public function handle(): int 250 | { 251 | $author = $this->run(GetAuthorByIDJob::class, [ 252 | 'id' => $this->authorId, 253 | ]); 254 | 255 | do { 256 | 257 | $result = $this->run(PaginateSubscribersJob::class, [ 258 | 'authorId' => $this->authorId, 259 | ]); 260 | 261 | if ($result->subscribers->isNotEmpty()) { 262 | // it's a queueable job so it will be enqueued, no waiting time 263 | $this->run(SendNotificationJob::class, [ 264 | 'from' => $author, 265 | 'to' => $result->subscribers, 266 | 'notification' => 'article.published', 267 | ]); 268 | } 269 | 270 | } while ($result->hasMorePages()); 271 | 272 | return $result->total; 273 | } 274 | } 275 | ``` 276 | 277 | [documentation](https://docs.lucidarch.dev/operations/) goes over this simple yet powerful concept. 278 | 279 | ### Data 280 | 281 | For a scalable set of interconnected data elements, we've created a place for them in `app/Data`, 282 | because most likely over time writing the application there could develop a need for more than Models in data, 283 | such as Repositories, Value Objects, Collections and more. 284 | 285 | ``` 286 | app/Data 287 | ├── Models 288 | ├── Values 289 | ├── Collections 290 | └── Repositories 291 | ``` 292 | 293 | ## Benefits 294 | 295 | There are valuable advantages to what may seem as overengineering. 296 | 297 | ### Organization 298 | 299 | - Predictable impact of changes on the system when reviewing code 300 | - Reduced debugging time since we’re dividing our application into isolated areas of focus (divide and conquer) 301 | - With Monolith, each of our services can have their own versioning system (e.g. Api service is at v1 while Chat is at v2.3 yet reside) 302 | yet reside in the same codebase 303 | 304 | ### Reuse & Replace 305 | 306 | By dissecting our application into small building blocks of code - a.k.a units - we've instantly opened the door for a high 307 | degree of code sharing across the application with Data and Domains, as well as replaceability with the least amount of friction 308 | and technical debt. 309 | 310 | ### Boundaries 311 | 312 | By setting boundaries you would've taken a step towards proetcting application code from growing unbearably large 313 | and made it easier for new devs to onboard. Most importantly, that you've reduced technical debt to the minimum so that you don't 314 | have to pay with bugs and sleepless nights; code doesn't run on good intentions nor wishes. 315 | 316 | ### Multitenancy 317 | 318 | When our application scales we'd typically have a bunch of instances of it running in different locations, 319 | at some point we would want to activate certain parts of our codebase in some areas and shut off others. 320 | 321 | Here’s a humble example of running *Api*, *Back Office* and *Web App* instances of the same application, which in Lucid terminology 322 | are *services* that share functionality through *data* and *domains*: 323 | 324 | ![Lucid multitenancy](https://raw.githubusercontent.com/lucidarch/artwork/main/material/concept/multitenancy.jpeg) 325 | 326 | # Contribute 327 | 328 | ## Bug & Issue Reports 329 | 330 | To encourage active collaboration, Lucid strongly encourages contribution through [pull requests](#which-branch-and-how-to-contribute). 331 | "Bug reports" may be searched or created in [issues](https://github.com/lucidarch/lucid/issues) or sent in the form of a [pull request](#which-branch-and-how-to-contribute) containing a failing test or steps to reproduce the bug. 332 | 333 | If you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. 334 | 335 | ⏱ PRs and issues are usually checked about three times a week so there is a high chance yours will be picked up soon. 336 | 337 | The Lucid Architecture source code is on GitHub as [lucidarch/lucid](https://github.com/lucidarch/lucid). 338 | 339 | ## Support Questions 340 | 341 | Lucid Architecture's GitHub issue trackers are not intended to provide help or support. Instead, use one of the following channels: 342 | 343 | - [Discussions](https://github.com/lucidarch/lucid/discussions) is where most conversations takes place 344 | - For a chat hit us on our official [Slack workspace](https://lucid-slack.herokuapp.com/) in the `#support` channel 345 | - If you prefer StackOverflow to post your questions you may use [#lucidarch](https://stackoverflow.com/questions/tagged/lucidarch) to tag them 346 | 347 | ## Core Development Discussion 348 | 349 | You may propose new features or improvements of existing Lucid Architecture behaviour in the [Lucid Discussins](https://github.com/lucidarch/lucid/discussions). 350 | If you propose a new feature, please be willing to implement at least some of the code that would be needed to complete the feature, or collaborate on active ideation in the meantime. 351 | 352 | Informal discussion regarding bugs, new features, and implementation of existing features takes place in the `#internals` channel of the [Lucid Slack workspace](https://lucid-slack.herokuapp.com/). 353 | Abed Halawi, the maintainer of Lucid, is typically present in the channel on weekdays from 8am-5pm EEST (Eastern European Summer Time), and sporadically present in the channel at other times. 354 | 355 | ## Which Branch? And How To Contribute 356 | 357 | The `main` branch is what contains the latest live version and is the one that gets released. 358 | 359 | - Fork this repository 360 | - Clone the forked repository to where you'll edit your code 361 | - Create a branch for your edits (e.g. `feature/queueable-units`, `fix/issue-31`) 362 | - Commit your changes and their tests (if applicable) with meaningful short messages 363 | - Push your branch `git push origin feature/queueable-units` 364 | - Open a [PR](https://github.com/lucidarch/lucid/compare) to the `main` branch, which will run tests for your edits 365 | 366 | ⏱ PRs and issues are usually checked about three times a week. 367 | 368 | 369 | ### Setup for Development 370 | 371 | Following are the steps to setup for development on Lucid: 372 | 373 | > Assuming we're in `~/dev` directory... 374 | 375 | - Clone the forked repository `[your username]/lucid` which will create a `lucid` folder at `~/dev/lucid` 376 | - Create a Laravel project to test your implementation in it `composer create-project laravel/laravel myproject` 377 | - Connect the created Laravel project to the local Lucid installation; in the Laravel project's `composer.json` 378 | ```json 379 | "require": { 380 | "...": "", 381 | "lucidarch/lucid": "@dev" 382 | }, 383 | "repositories": [ 384 | { 385 | "type": "path", 386 | "url": "~/dev/lucid", 387 | "options": { 388 | "symlink": true 389 | } 390 | } 391 | ], 392 | "minimum-stability": "dev", 393 | ``` 394 | > Make sure you change the `url` to the absolute path of your directory 395 | 396 | - Run `composer update` to create the symlink 397 | 398 | Now all your changes in the lucid directory will take effect automatically in the project. 399 | 400 | ## Security Vulnerabilities 401 | 402 | If you discover a security vulnerability within Lucid, please send an email to Abed Halawi at [halawi.abed@gmail.com](mailto:halawi.abed@gmail.com). 403 | All security vulnerabilities will be promptly addressed. 404 | 405 | ## Coding Style 406 | 407 | Lucid Architecture follows the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard and the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading standard. 408 | 409 | ### PHPDoc 410 | 411 | Below is an example of a valid Lucid Architecture documentation block. Note that the `@param` attribute is followed by two spaces, the argument type, two more spaces, and finally the variable name: 412 | 413 | ```php 414 | /** 415 | * Register a binding with the container. 416 | * 417 | * @param string|array $abstract 418 | * @param \Closure|string|null $concrete 419 | * @param bool $shared 420 | * @return void 421 | * 422 | * @throws \Exception 423 | */ 424 | public function bind($abstract, $concrete = null, $shared = false) 425 | { 426 | // 427 | } 428 | ``` 429 | 430 | ## Code of Conduct 431 | 432 | The Lucid Architecture code of conduct is derived from the Laravel code of conduct. Any violations of the code of conduct may be reported to Abed Halawi (halawi.abed@gmail.com): 433 | 434 | - Participants will be tolerant of opposing views. 435 | - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 436 | - When interpreting the words and actions of others, participants should always assume good intentions. 437 | - Behavior that can be reasonably considered harassment will not be tolerated. 438 | -------------------------------------------------------------------------------- /bin/test-commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Check if file or directory exists. Exit if it doesn't. 6 | examine() { 7 | if [ ! -f $1 ] && [ ! -d $1 ]; then 8 | echo "\n-- ERROR -- $1 could not be found!\n" 9 | exit 1 10 | fi 11 | } 12 | 13 | # Lint a PHP file for syntax errors. Exit on error. 14 | lint() { 15 | # echo "\n -- MISSING -- Lint file $1" 16 | RESULT=$(php -l $1) 17 | if [ ! $? -eq 0 ] ; then 18 | echo "$RESULT" && exit 1 19 | fi 20 | } 21 | 22 | if [ ! -f ".env" ]; then 23 | echo 'APP_KEY=' > .env 24 | php artisan key:generate 25 | fi 26 | 27 | examine "app/Providers" 28 | examine "app/Providers/RouteServiceProvider.php" 29 | examine "resources" 30 | examine "resources/lang" 31 | examine "resources/views" 32 | examine "resources/views/welcome.blade.php" 33 | lint "resources/views/welcome.blade.php" 34 | examine "routes" 35 | examine "routes/api.php" 36 | examine "routes/web.php" 37 | lint "routes/api.php" 38 | lint "routes/web.php" 39 | examine "tests" 40 | 41 | ## --- Micro --- 42 | 43 | # Controller 44 | ./vendor/bin/lucid make:controller trade 45 | examine "app/Http/Controllers/TradeController.php" 46 | lint "app/Http/Controllers/TradeController.php" 47 | 48 | # Feature 49 | ./vendor/bin/lucid make:feature trade 50 | examine "app/Features/TradeFeature.php" 51 | lint "app/Features/TradeFeature.php" 52 | examine "tests/Feature/TradeFeatureTest.php" 53 | lint "tests/Feature/TradeFeatureTest.php" 54 | 55 | ## Feature in Subdirectory 56 | ./vendor/bin/lucid make:feature finance/wallet/pay 57 | examine "app/Features/Finance/Wallet/PayFeature.php" 58 | lint "app/Features/Finance/Wallet/PayFeature.php" 59 | examine "tests/Feature/Finance/Wallet/PayFeatureTest.php" 60 | lint "tests/Feature/Finance/Wallet/PayFeatureTest.php" 61 | 62 | # Job 63 | ./vendor/bin/lucid make:job submitTradeRequest shipping 64 | examine "app/Domains/Shipping/Jobs/SubmitTradeRequestJob.php" 65 | lint "app/Domains/Shipping/Jobs/SubmitTradeRequestJob.php" 66 | examine "tests/Unit/Domains/Shipping/Jobs/SubmitTradeRequestJobTest.php" 67 | lint "tests/Unit/Domains/Shipping/Jobs/SubmitTradeRequestJobTest.php" 68 | 69 | ./vendor/bin/lucid make:job sail boat --queue 70 | examine "app/Domains/Boat/Jobs/SailJob.php" 71 | lint "app/Domains/Boat/Jobs/SailJob.php" 72 | examine "tests/Unit/Domains/Boat/Jobs/SailJobTest.php" 73 | lint "tests/Unit/Domains/Boat/Jobs/SailJobTest.php" 74 | 75 | # Model 76 | ./vendor/bin/lucid make:model bridge 77 | examine "app/Data/Models/Bridge.php" 78 | lint "app/Data/Models/Bridge.php" 79 | 80 | # Operation 81 | ./vendor/bin/lucid make:operation spin 82 | examine "app/Operations/SpinOperation.php" 83 | lint "app/Operations/SpinOperation.php" 84 | examine "tests/Unit/Operations/SpinOperationTest.php" 85 | lint "tests/Unit/Operations/SpinOperationTest.php" 86 | 87 | ./vendor/bin/lucid make:operation twist --queue 88 | examine "app/Operations/TwistOperation.php" 89 | lint "app/Operations/TwistOperation.php" 90 | examine "tests/Unit/Operations/TwistOperationTest.php" 91 | lint "tests/Unit/Operations/TwistOperationTest.php" 92 | 93 | # Policy 94 | ./vendor/bin/lucid make:policy fly 95 | examine "app/Policies/FlyPolicy.php" 96 | lint "app/Policies/FlyPolicy.php" 97 | 98 | # Ensure nothing is breaking 99 | ./vendor/bin/lucid list:features 100 | ./vendor/bin/lucid list:services 101 | 102 | # Run PHPUnit tests 103 | ./vendor/bin/phpunit 104 | 105 | echo "\nMicro tests PASSED!\n" 106 | 107 | ## --- Monolith --- 108 | 109 | # Controller 110 | ./vendor/bin/lucid make:controller trade harbour 111 | examine "app/Services/Harbour/Http/Controllers/TradeController.php" 112 | lint "app/Services/Harbour/Http/Controllers/TradeController.php" 113 | 114 | # Feature 115 | ./vendor/bin/lucid make:feature trade harbour 116 | examine "app/Services/Harbour/Features/TradeFeature.php" 117 | lint "app/Services/Harbour/Features/TradeFeature.php" 118 | examine "tests/Feature/Services/Harbour/TradeFeatureTest.php" 119 | lint "tests/Feature/Services/Harbour/TradeFeatureTest.php" 120 | 121 | ## Feature in Subdirectory 122 | ./vendor/bin/lucid make:feature port/yacht/park harbour 123 | examine "app/Services/Harbour/Features/Port/Yacht/ParkFeature.php" 124 | lint "app/Services/Harbour/Features/Port/Yacht/ParkFeature.php" 125 | examine "tests/Feature/Services/Harbour/Port/Yacht/ParkFeatureTest.php" 126 | lint "tests/Feature/Services/Harbour/Port/Yacht/ParkFeatureTest.php" 127 | 128 | ## Operation 129 | ./vendor/bin/lucid make:operation spin harbour 130 | examine "app/Services/Harbour/Operations/SpinOperation.php" 131 | lint "app/Services/Harbour/Operations/SpinOperation.php" 132 | examine "tests/Unit/Services/Harbour/Operations/SpinOperationTest.php" 133 | lint "tests/Unit/Services/Harbour/Operations/SpinOperationTest.php" 134 | 135 | ./vendor/bin/lucid make:operation twist harbour --queue 136 | examine "app/Services/Harbour/Operations/TwistOperation.php" 137 | lint "app/Services/Harbour/Operations/TwistOperation.php" 138 | examine "tests/Unit/Services/Harbour/Operations/TwistOperationTest.php" 139 | lint "tests/Unit/Services/Harbour/Operations/TwistOperationTest.php" 140 | 141 | # Ensure nothing is breaking 142 | ./vendor/bin/lucid list:features 143 | ./vendor/bin/lucid list:services 144 | 145 | ./vendor/bin/phpunit 146 | 147 | ## --- TEARDOWN --- 148 | 149 | ./vendor/bin/lucid delete:feature trade 150 | ./vendor/bin/lucid delete:job submitTradeRequest shipping 151 | ./vendor/bin/lucid delete:job sail boat 152 | ./vendor/bin/lucid delete:model bridge 153 | ./vendor/bin/lucid delete:operation spin 154 | ./vendor/bin/lucid delete:operation twist 155 | ./vendor/bin/lucid delete:policy fly 156 | rm app/Http/Controllers/TradeController.php 157 | 158 | ./vendor/bin/lucid delete:feature trade harbour 159 | ./vendor/bin/lucid delete:operation spin harbour 160 | ./vendor/bin/lucid delete:operation twist harbour 161 | rm app/Services/Harbour/Http/Controllers/TradeController.php 162 | 163 | echo "\nPASSED!\n" 164 | 165 | exit 0 166 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucidarch/lucid", 3 | "description": "An architecture for scalable software.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["architecture", "lucid", "laravel"], 7 | "homepage": "https://lucidarch.dev", 8 | "bin": ["lucid"], 9 | "support": { 10 | "source": "https://github.com/lucidarch/lucid", 11 | "issues": "https://github.com/lucidarch/lucid/issues", 12 | "discussions": "https://github.com/lucidarch/lucid/discussions" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Abed Halawi", 17 | "email": "halawi.abed@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "ext-dom": "*", 22 | "mockery/mockery": "*", 23 | "symfony/console": "*", 24 | "symfony/filesystem": "*", 25 | "symfony/finder": "*", 26 | "symfony/process": "*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Lucid\\": "src/" 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true 38 | } 39 | -------------------------------------------------------------------------------- /lucid: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 48 | -------------------------------------------------------------------------------- /src/Bus/Marshal.php: -------------------------------------------------------------------------------- 1 | $parameter) { 25 | $parameters[$name] = $parameter; 26 | } 27 | 28 | $parameters = array_merge($parameters, $extras); 29 | 30 | return app($command, $parameters); 31 | } 32 | 33 | /** 34 | * Get a parameter value for a marshaled command. 35 | * 36 | * @param string $command 37 | * @param \ArrayAccess $source 38 | * @param \ReflectionParameter $parameter 39 | * @param array $extras 40 | * 41 | * @return mixed 42 | * @throws Exception 43 | */ 44 | protected function getParameterValueForCommand($command, ArrayAccess $source, 45 | ReflectionParameter $parameter, array $extras = []) 46 | { 47 | if (array_key_exists($parameter->name, $extras)) { 48 | return $extras[$parameter->name]; 49 | } 50 | 51 | if (isset($source[$parameter->name])) { 52 | return $source[$parameter->name]; 53 | } 54 | 55 | if ($parameter->isDefaultValueAvailable()) { 56 | return $parameter->getDefaultValue(); 57 | } 58 | 59 | throw new Exception("Unable to map parameter [{$parameter->name}] to command [{$command}]"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Bus/ServesFeatures.php: -------------------------------------------------------------------------------- 1 | dispatch($this->marshal($feature, new Collection(), $arguments)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Bus/UnitDispatcher.php: -------------------------------------------------------------------------------- 1 | dispatch($unit); 39 | } elseif ($arguments instanceof Request) { 40 | $result = $this->dispatch($this->marshal($unit, $arguments, $extra)); 41 | } else { 42 | if (!is_object($unit)) { 43 | $unit = $this->marshal($unit, new Collection(), $arguments); 44 | 45 | // don't dispatch unit when in tests and have a mock for it. 46 | } elseif (App::runningUnitTests() && app(UnitMockRegistry::class)->has(get_class($unit))) { 47 | /** @var UnitMock $mock */ 48 | $mock = app(UnitMockRegistry::class)->get(get_class($unit)); 49 | $mock->compareTo($unit); 50 | 51 | // Reaching this step confirms that the expected mock is similar to the passed instance, so we 52 | // get the unit's mock counterpart to be dispatched. Otherwise, the previous step would 53 | // throw an exception when the mock doesn't match the passed instance. 54 | $unit = $this->marshal( 55 | get_class($unit), 56 | new Collection(), 57 | $mock->getConstructorExpectationsForInstance($unit) 58 | ); 59 | } 60 | 61 | $result = $this->dispatch($unit); 62 | } 63 | 64 | if ($unit instanceof Operation) { 65 | event(new OperationStarted(get_class($unit), $arguments)); 66 | } 67 | 68 | if ($unit instanceof Job) { 69 | event(new JobStarted(get_class($unit), $arguments)); 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | /** 76 | * Run the given unit in the given queue. 77 | * 78 | * @param string $unit 79 | * @param array $arguments 80 | * @param string|null $queue 81 | * 82 | * @return mixed 83 | * @throws ReflectionException 84 | */ 85 | public function runInQueue($unit, array $arguments = [], $queue = 'default') 86 | { 87 | // instantiate and queue the unit 88 | $reflection = new ReflectionClass($unit); 89 | $instance = $reflection->newInstanceArgs($arguments); 90 | $instance->onQueue((string) $queue); 91 | 92 | return $this->dispatch($instance); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | setName($this->name) 31 | ->setDescription($this->description); 32 | 33 | foreach ($this->getArguments() as $arguments) { 34 | call_user_func_array([$this, 'addArgument'], $arguments); 35 | } 36 | 37 | foreach ($this->getOptions() as $options) { 38 | call_user_func_array([$this, 'addOption'], $options); 39 | } 40 | } 41 | 42 | /** 43 | * Default implementation to get the arguments of this command. 44 | * 45 | * @return array 46 | */ 47 | public function getArguments() 48 | { 49 | return []; 50 | } 51 | 52 | /** 53 | * Default implementation to get the options of this command. 54 | * 55 | * @return array 56 | */ 57 | public function getOptions() 58 | { 59 | return []; 60 | } 61 | 62 | /** 63 | * Execute the command. 64 | * 65 | * @param \Symfony\Component\Console\Input\InputInterface $input 66 | * @param \Symfony\Component\Console\Output\OutputInterface $output 67 | */ 68 | public function execute(InputInterface $input, OutputInterface $output) 69 | { 70 | $this->input = $input; 71 | $this->output = $output; 72 | 73 | return (int) $this->handle(); 74 | } 75 | 76 | /** 77 | * Get an argument from the input. 78 | * 79 | * @param string $key 80 | * 81 | * @return string 82 | */ 83 | public function argument($key) 84 | { 85 | return $this->input->getArgument($key); 86 | } 87 | 88 | /** 89 | * Get an option from the input. 90 | * 91 | * @param string $key 92 | * 93 | * @return string 94 | */ 95 | public function option($key) 96 | { 97 | return $this->input->getOption($key); 98 | } 99 | 100 | /** 101 | * Write a string as information output. 102 | * 103 | * @param string $string 104 | */ 105 | public function info($string) 106 | { 107 | $this->output->writeln("$string"); 108 | } 109 | 110 | /** 111 | * Write a string as comment output. 112 | * 113 | * @param string $string 114 | * @return void 115 | */ 116 | public function comment($string) 117 | { 118 | $this->output->writeln("$string"); 119 | } 120 | 121 | /** 122 | * Write a string as error output. 123 | * 124 | * @param string $string 125 | * @return void 126 | */ 127 | public function error($string) 128 | { 129 | $this->output->writeln("$string"); 130 | } 131 | 132 | /** 133 | * Format input to textual table. 134 | * 135 | * @param array $headers 136 | * @param \Illuminate\Contracts\Support\Arrayable|array $rows 137 | * @param string $style 138 | * @return void 139 | */ 140 | public function table(array $headers, $rows, $style = 'default') 141 | { 142 | $table = new Table($this->output); 143 | 144 | if ($rows instanceof Arrayable) { 145 | $rows = $rows->toArray(); 146 | } 147 | 148 | $table->setHeaders($headers)->setRows($rows)->setStyle($style)->render(); 149 | } 150 | 151 | /** 152 | * Ask the user the given question. 153 | * 154 | * @param string $question 155 | * 156 | * @return string 157 | */ 158 | public function ask($question, $default = false) 159 | { 160 | $question = ''.$question.' '; 161 | 162 | $confirmation = new ConfirmationQuestion($question, false); 163 | 164 | return $this->getHelperSet()->get('question')->ask($this->input, $this->output, $confirmation); 165 | } 166 | 167 | /** 168 | * Ask the user the given secret question. 169 | * 170 | * @param string $question 171 | * 172 | * @return string 173 | */ 174 | public function secret($question) 175 | { 176 | $question = ''.$question.' '; 177 | 178 | return $this->getHelperSet()->get('dialog')->askHiddenResponse($this->output, $question, false); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Console/Commands/ChangeSourceNamespaceCommand.php: -------------------------------------------------------------------------------- 1 | files = new Filesystem(); 54 | $this->composer = new Composer($this->files); 55 | } 56 | 57 | /** 58 | * Execute the console command. 59 | */ 60 | public function handle() 61 | { 62 | try { 63 | $this->setAppDirectoryNamespace(); 64 | 65 | $this->setAppConfigNamespaces(); 66 | 67 | $this->setComposerNamespace(); 68 | 69 | $this->info('Lucid source directory namespace set!'); 70 | 71 | $this->composer->dumpAutoloads(); 72 | } catch (\Exception $e) { 73 | $this->error($e->getMessage()); 74 | } 75 | } 76 | 77 | /** 78 | * Set the namespace on the files in the app directory. 79 | */ 80 | protected function setAppDirectoryNamespace() 81 | { 82 | $files = SymfonyFinder::create() 83 | ->in(base_path()) 84 | ->exclude('vendor') 85 | ->contains($this->findRootNamespace()) 86 | ->name('*.php'); 87 | 88 | foreach ($files as $file) { 89 | $this->replaceNamespace($file->getRealPath()); 90 | } 91 | } 92 | 93 | /** 94 | * Replace the App namespace at the given path. 95 | * 96 | * @param string $path 97 | */ 98 | protected function replaceNamespace($path) 99 | { 100 | $search = [ 101 | 'namespace '.$this->findRootNamespace().';', 102 | $this->findRootNamespace().'\\', 103 | ]; 104 | 105 | $replace = [ 106 | 'namespace '.$this->argument('name').';', 107 | $this->argument('name').'\\', 108 | ]; 109 | 110 | $this->replaceIn($path, $search, $replace); 111 | } 112 | 113 | /** 114 | * Set the PSR-4 namespace in the Composer file. 115 | */ 116 | protected function setComposerNamespace() 117 | { 118 | $this->replaceIn( 119 | $this->getComposerPath(), str_replace('\\', '\\\\', $this->findRootNamespace()).'\\\\', str_replace('\\', '\\\\', $this->argument('name')).'\\\\' 120 | ); 121 | } 122 | 123 | /** 124 | * Set the namespace in the appropriate configuration files. 125 | */ 126 | protected function setAppConfigNamespaces() 127 | { 128 | $search = [ 129 | $this->findRootNamespace().'\\Providers', 130 | $this->findRootNamespace().'\\Foundation', 131 | $this->findRootNamespace().'\\Http\\Controllers\\', 132 | ]; 133 | 134 | $replace = [ 135 | $this->argument('name').'\\Providers', 136 | $this->argument('name').'\\Foundation', 137 | $this->argument('name').'\\Http\\Controllers\\', 138 | ]; 139 | 140 | $this->replaceIn($this->getConfigPath('app'), $search, $replace); 141 | } 142 | 143 | /** 144 | * Replace the given string in the given file. 145 | * 146 | * @param string $path 147 | * @param string|array $search 148 | * @param string|array $replace 149 | */ 150 | protected function replaceIn($path, $search, $replace) 151 | { 152 | if ($this->files->exists($path)) { 153 | $this->files->put($path, str_replace($search, $replace, $this->files->get($path))); 154 | } 155 | } 156 | 157 | /** 158 | * Get the console command arguments. 159 | * 160 | * @return array 161 | */ 162 | protected function getArguments() 163 | { 164 | return [ 165 | ['name', InputArgument::REQUIRED, 'The source directory namespace.'], 166 | ]; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Console/Commands/ControllerMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('service'); 51 | $name = $this->argument('controller'); 52 | 53 | try { 54 | $controller = $generator->generate($name, $service, $this->option('resource')); 55 | 56 | $this->info('Controller class created successfully.'. 57 | "\n". 58 | "\n". 59 | 'Find it at '.$controller.''."\n" 60 | ); 61 | } catch (Exception $e) { 62 | $this->error($e->getMessage()); 63 | } 64 | } 65 | 66 | /** 67 | * Get the console command arguments. 68 | * 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | return [ 74 | ['controller', InputArgument::REQUIRED, 'The controller\'s name.'], 75 | ['service', InputArgument::OPTIONAL, 'The service in which the controller should be generated.'], 76 | ]; 77 | } 78 | 79 | /** 80 | * Get the console command options. 81 | * 82 | * @return array 83 | */ 84 | protected function getOptions() 85 | { 86 | return [ 87 | ['resource', null, InputOption::VALUE_NONE, 'Generate a resource controller class.'], 88 | ]; 89 | } 90 | 91 | /** 92 | * Parse the feature name. 93 | * remove the Controller.php suffix if found 94 | * we're adding it ourselves. 95 | * 96 | * @param string $name 97 | * 98 | * @return string 99 | */ 100 | protected function parseName($name) 101 | { 102 | return Str::studly(preg_replace('/Controller(\.php)?$/', '', $name).'Controller'); 103 | } 104 | 105 | /** 106 | * Get the stub file for the generator. 107 | * 108 | * @return string 109 | */ 110 | protected function getStub() 111 | { 112 | if ($this->option('plain')) { 113 | return __DIR__ . '/../Generators/stubs/controller.plain.stub'; 114 | } 115 | 116 | return __DIR__ . '/../Generators/stubs/controller.resource.stub'; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Console/Commands/FeatureDeleteCommand.php: -------------------------------------------------------------------------------- 1 | argument('service')); 48 | $title = $this->parseName($this->argument('feature')); 49 | 50 | if (!$this->exists($feature = $this->findFeaturePath($service, $title))) { 51 | $this->error('Feature class '.$title.' cannot be found.'); 52 | } else { 53 | $this->delete($feature); 54 | 55 | $this->info('Feature class '.$title.' deleted successfully.'); 56 | } 57 | } catch (Exception $e) { 58 | $this->error($e->getMessage()); 59 | } 60 | } 61 | 62 | /** 63 | * Get the console command arguments. 64 | * 65 | * @return array 66 | */ 67 | protected function getArguments() 68 | { 69 | return [ 70 | ['feature', InputArgument::REQUIRED, 'The feature\'s name.'], 71 | ['service', InputArgument::OPTIONAL, 'The service from which the feature should be deleted.'], 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the stub file for the generator. 77 | * 78 | * @return string 79 | */ 80 | protected function getStub() 81 | { 82 | return __DIR__ . '/../Generators/stubs/feature.stub'; 83 | } 84 | 85 | /** 86 | * Parse the feature name. 87 | * remove the Feature.php suffix if found 88 | * we're adding it ourselves. 89 | * 90 | * @param string $name 91 | * 92 | * @return string 93 | */ 94 | protected function parseName($name) 95 | { 96 | return Str::feature($name); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Console/Commands/FeatureDescribeCommand.php: -------------------------------------------------------------------------------- 1 | findFeature($this->argument('feature'))) { 37 | $parser = new Parser(); 38 | $jobs = $parser->parseFeatureJobs($feature); 39 | 40 | $features = []; 41 | foreach ($jobs as $index => $job) { 42 | $features[$feature->title][] = [$index+1, $job->title, $job->domain->name, $job->relativePath]; 43 | } 44 | 45 | foreach ($features as $feature => $jobs) { 46 | $this->comment("\n$feature\n"); 47 | $this->table(['', 'Job', 'Domain', 'Path'], $jobs); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | throw new InvalidArgumentException('Feature with name "'.$this->argument('feature').'" not found.'); 54 | } 55 | 56 | 57 | /** 58 | * Get the console command arguments. 59 | * 60 | * @return array 61 | */ 62 | protected function getArguments() 63 | { 64 | return [ 65 | ['feature', InputArgument::REQUIRED, 'The feature name to list the jobs of.'], 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Console/Commands/FeatureMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('service')); 50 | $title = $this->parseName($this->argument('feature')); 51 | 52 | $generator = new FeatureGenerator(); 53 | $feature = $generator->generate($title, $service); 54 | 55 | $this->info( 56 | 'Feature class '.$feature->title.' created successfully.'. 57 | "\n". 58 | "\n". 59 | 'Find it at '.$feature->relativePath.''."\n" 60 | ); 61 | } catch (Exception $e) { 62 | $this->error($e->getMessage()); 63 | } 64 | } 65 | 66 | /** 67 | * Get the console command arguments. 68 | * 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | return [ 74 | ['feature', InputArgument::REQUIRED, 'The feature\'s name.'], 75 | ['service', InputArgument::OPTIONAL, 'The service in which the feature should be implemented.'], 76 | ]; 77 | } 78 | 79 | /** 80 | * Get the stub file for the generator. 81 | * 82 | * @return string 83 | */ 84 | protected function getStub() 85 | { 86 | return __DIR__ . '/../Generators/stubs/feature.stub'; 87 | } 88 | 89 | /** 90 | * Parse the feature name. 91 | * remove the Feature.php suffix if found 92 | * we're adding it ourselves. 93 | * 94 | * @param string $name 95 | * 96 | * @return string 97 | */ 98 | protected function parseName($name) 99 | { 100 | return Str::feature($name); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Console/Commands/FeaturesListCommand.php: -------------------------------------------------------------------------------- 1 | listFeatures($this->argument('service')) as $service => $features) { 37 | $this->comment("\n$service\n"); 38 | $features = array_map(function($feature) { 39 | return [$feature->title, $feature->service->name, $feature->file, $feature->relativePath]; 40 | }, $features->all()); 41 | $this->table(['Feature', 'Service', 'File', 'Path'], $features); 42 | } 43 | } 44 | 45 | /** 46 | * Get the console command arguments. 47 | * 48 | * @return array 49 | */ 50 | protected function getArguments() 51 | { 52 | return [ 53 | ['service', InputArgument::OPTIONAL, 'The service to list the features of.'], 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/Commands/GeneratorCommand.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Console/Commands/InitCommandTrait.php: -------------------------------------------------------------------------------- 1 | info(''); 14 | $this->info("You're all set to build something awesome that scales!"); 15 | $this->info(''); 16 | $this->info('Here are some examples to get you started:'); 17 | $this->info(''); 18 | 19 | $this->info('You may wish to start with a feature'); 20 | $this->comment("lucid make:feature LoginUser $service"); 21 | if ($service) { 22 | $this->info("will generate app/Services/$service/Features/LoginUserFeature.php"); 23 | } else { 24 | $this->info("will generate app/Features/LoginUserFeature.php"); 25 | } 26 | 27 | $this->info(''); 28 | 29 | $this->info('Or a job to do a single thing'); 30 | $this->comment('lucid make:job GetUserByEmail User'); 31 | $this->info('will generate app/Domains/User/Jobs/GetUserByEmailJob.php'); 32 | $this->info(''); 33 | $this->info('For more Job examples check out Lucid\'s built-in jobs:'); 34 | $this->comment('- Lucid\Domains\Http\Jobs\RespondWithJsonJob'); 35 | $this->info('for consistent JSON structure responses.'); 36 | $this->info(''); 37 | $this->comment('- Lucid\Domains\Http\Jobs\RespondWithJsonErrorJob'); 38 | $this->info('for consistent JSON error responses.'); 39 | $this->info(''); 40 | $this->comment('- Lucid\Domains\Http\Jobs\RespondWithViewJob'); 41 | $this->info('basic view and data response functionality.'); 42 | 43 | $this->info(''); 44 | 45 | $this->info('Finally you can group multiple jobs in an operation'); 46 | $this->comment("lucid make:operation ProcessUserLogin $service"); 47 | 48 | if ($service) { 49 | $this->info("will generate app/Services/$service/Operations/ProcessUserLoginOperation.php"); 50 | } else { 51 | $this->info('will generate app/Operations/ProcessUserLoginOperation.php'); 52 | } 53 | 54 | $this->info(''); 55 | 56 | $this->info('For more details, help yourself with the docs at https://docs.lucidarch.dev'); 57 | $this->info(''); 58 | $this->info('Remember to enjoy the journey.'); 59 | $this->info('Cheers!'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/Commands/InitMicroCommand.php: -------------------------------------------------------------------------------- 1 | version(); 40 | $this->info("Initializing Lucid Micro for Laravel $version\n"); 41 | 42 | $generator = new MicroGenerator(); 43 | $paths = $generator->generate(); 44 | 45 | $this->comment("Created directories:"); 46 | $this->comment(join("\n", $paths)); 47 | 48 | $this->welcome(); 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Console/Commands/InitMonolithCommand.php: -------------------------------------------------------------------------------- 1 | version(); 60 | $this->info("Initializing Lucid Monolith for Laravel $version\n"); 61 | 62 | $service = $this->argument('service'); 63 | 64 | $directories = (new MonolithGenerator())->generate(); 65 | $this->comment('Created directories:'); 66 | $this->comment(join("\n", $directories)); 67 | 68 | // create service 69 | if ($service) { 70 | $this->getApplication() 71 | ->find('make:service') 72 | ->run(new ArrayInput(['name' => $service]), $this->output); 73 | 74 | $this->ask('Once done, press Enter/Return to continue...'); 75 | } 76 | 77 | $this->welcome($service); 78 | 79 | return 0; 80 | } 81 | 82 | /** 83 | * Get the console command arguments. 84 | * 85 | * @return array 86 | */ 87 | protected function getArguments() 88 | { 89 | return [ 90 | ['service', InputArgument::OPTIONAL, 'Your first service.'], 91 | ]; 92 | } 93 | 94 | /** 95 | * Get the console command options. 96 | * 97 | * @return array 98 | */ 99 | protected function getOptions() 100 | { 101 | return [ 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Console/Commands/JobDeleteCommand.php: -------------------------------------------------------------------------------- 1 | argument('domain')); 48 | $title = $this->parseName($this->argument('job')); 49 | 50 | if (!$this->exists($job = $this->findJobPath($domain, $title))) { 51 | $this->error('Job class '.$title.' cannot be found.'); 52 | } else { 53 | $this->delete($job); 54 | 55 | if (count($this->listJobs($domain)->first()) === 0) { 56 | $this->delete($this->findDomainPath($domain)); 57 | } 58 | 59 | $this->info('Job class '.$title.' deleted successfully.'); 60 | } 61 | } catch (Exception $e) { 62 | $this->error($e->getMessage()); 63 | } 64 | } 65 | 66 | public function getArguments() 67 | { 68 | return [ 69 | ['job', InputArgument::REQUIRED, 'The job\'s name.'], 70 | ['domain', InputArgument::REQUIRED, 'The domain from which the job will be deleted.'], 71 | ]; 72 | } 73 | 74 | /** 75 | * Get the stub file for the generator. 76 | * 77 | * @return string 78 | */ 79 | public function getStub() 80 | { 81 | return __DIR__ . '/../Generators/stubs/job.stub'; 82 | } 83 | 84 | /** 85 | * Parse the job name. 86 | * remove the Job.php suffix if found 87 | * we're adding it ourselves. 88 | * 89 | * @param string $name 90 | * 91 | * @return string 92 | */ 93 | protected function parseName($name) 94 | { 95 | return Str::job($name); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Console/Commands/JobMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('domain')); 51 | $title = $this->parseName($this->argument('job')); 52 | $isQueueable = $this->option('queue'); 53 | try { 54 | $job = $generator->generate($title, $domain, $isQueueable); 55 | 56 | $this->info( 57 | 'Job class '.$title.' created successfully.'. 58 | "\n". 59 | "\n". 60 | 'Find it at '.$job->relativePath.''."\n" 61 | ); 62 | } catch (Exception $e) { 63 | $this->error($e->getMessage()); 64 | } 65 | } 66 | 67 | public function getArguments() 68 | { 69 | return [ 70 | ['job', InputArgument::REQUIRED, 'The job\'s name.'], 71 | ['domain', InputArgument::REQUIRED, 'The domain to be responsible for the job.'], 72 | ]; 73 | } 74 | 75 | public function getOptions() 76 | { 77 | return [ 78 | ['queue', 'Q', InputOption::VALUE_NONE, 'Whether a job is queueable or not.'], 79 | ]; 80 | } 81 | 82 | /** 83 | * Get the stub file for the generator. 84 | * 85 | * @return string 86 | */ 87 | public function getStub() 88 | { 89 | return __DIR__ . '/../Generators/stubs/job.stub'; 90 | } 91 | 92 | /** 93 | * Parse the job name. 94 | * remove the Job.php suffix if found 95 | * we're adding it ourselves. 96 | * 97 | * @param string $name 98 | * 99 | * @return string 100 | */ 101 | protected function parseName($name) 102 | { 103 | return Str::job($name); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Console/Commands/MigrationMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('service'); 37 | $migration = $this->argument('migration'); 38 | 39 | $path = $this->findMigrationPath(Str::service($service)); 40 | 41 | $output = shell_exec('php artisan make:migration '.$migration.' --path='.$path); 42 | 43 | $this->info($output); 44 | $this->info("\n".'Find it at '.$path.''."\n"); 45 | } 46 | 47 | /** 48 | * Get the console command arguments. 49 | * 50 | * @return array 51 | */ 52 | protected function getArguments() 53 | { 54 | return [ 55 | ['migration', InputArgument::REQUIRED, 'The migration\'s name.'], 56 | ['service', InputArgument::OPTIONAL, 'The service in which the migration should be generated.'], 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Console/Commands/ModelDeleteCommand.php: -------------------------------------------------------------------------------- 1 | parseModelName($this->argument('model')); 48 | 49 | if ( ! $this->exists($path = $this->findModelPath($model))) { 50 | $this->error('Model class ' . $model . ' cannot be found.'); 51 | } else { 52 | $this->delete($path); 53 | 54 | $this->info('Model class ' . $model . ' deleted successfully.'); 55 | } 56 | } catch (Exception $e) { 57 | $this->error($e->getMessage()); 58 | } 59 | } 60 | 61 | /** 62 | * Get the console command arguments. 63 | * 64 | * @return array 65 | */ 66 | public function getArguments() 67 | { 68 | return [ 69 | ['model', InputArgument::REQUIRED, 'The Model\'s name.'] 70 | ]; 71 | } 72 | 73 | /** 74 | * Get the stub file for the generator. 75 | * 76 | * @return string 77 | */ 78 | public function getStub() 79 | { 80 | return __DIR__ . '/../Generators/stubs/model.stub'; 81 | } 82 | 83 | /** 84 | * Parse the model name. 85 | * 86 | * @param string $name 87 | * @return string 88 | */ 89 | public function parseModelName($name) 90 | { 91 | return Str::model($name); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Console/Commands/ModelMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 49 | 50 | try { 51 | $model = $generator->generate($name); 52 | 53 | $this->info('Model class created successfully.' . 54 | "\n" . 55 | "\n" . 56 | 'Find it at ' . $model->relativePath . '' . "\n" 57 | ); 58 | } catch (Exception $e) { 59 | $this->error($e->getMessage()); 60 | } 61 | } 62 | 63 | /** 64 | * Get the console command arguments. 65 | * 66 | * @return array 67 | */ 68 | public function getArguments() 69 | { 70 | return [ 71 | ['model', InputArgument::REQUIRED, 'The Model\'s name.'] 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the stub file for the generator. 77 | * 78 | * @return string 79 | */ 80 | public function getStub() 81 | { 82 | return __DIR__ . '/../Generators/stubs/model.stub'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Console/Commands/NewCommand.php: -------------------------------------------------------------------------------- 1 | setName('new') 21 | ->setDescription('Create a new Lucid-architected project') 22 | ->addArgument('name', InputArgument::OPTIONAL) 23 | ->addOption('laravel', null, InputOption::VALUE_NONE, 'Specify the Laravel version you wish to install'); 24 | } 25 | 26 | /** 27 | * Execute the command. 28 | * 29 | * @param InputInterface $input 30 | * @param OutputInterface $output 31 | */ 32 | public function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $this->verifyApplicationDoesntExist( 35 | $directory = ($input->getArgument('name')) ? getcwd().'/'.$input->getArgument('name') : getcwd(), 36 | $output 37 | ); 38 | 39 | $output->writeln('Crafting Lucid application...'); 40 | 41 | /* 42 | * @TODO: Get Lucid based on the Laravel version. 43 | */ 44 | $process = new Process($this->findComposer().' create-project laravel/laravel '.$directory); 45 | 46 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 47 | $process->setTty(true); 48 | } 49 | 50 | $process->run(function ($type, $line) use ($output) { 51 | $output->write($line); 52 | }); 53 | 54 | $output->writeln('Application ready! Make your dream a reality.'); 55 | } 56 | 57 | /** 58 | * Verify that the application does not already exist. 59 | * 60 | * @param string $directory 61 | */ 62 | protected function verifyApplicationDoesntExist($directory, OutputInterface $output) 63 | { 64 | if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { 65 | throw new RuntimeException('Application already exists!'); 66 | } 67 | } 68 | 69 | /** 70 | * Get the composer command for the environment. 71 | * 72 | * @return string 73 | */ 74 | protected function findComposer() 75 | { 76 | $composer = 'composer'; 77 | 78 | if (file_exists(getcwd().'/composer.phar')) { 79 | $composer = '"'.PHP_BINARY.'" composer.phar'; 80 | } 81 | 82 | return $composer; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Console/Commands/OperationDeleteCommand.php: -------------------------------------------------------------------------------- 1 | argument('service')); 48 | $title = $this->parseName($this->argument('operation')); 49 | 50 | if (!$this->exists($operation = $this->findOperationPath($service, $title))) { 51 | $this->error('Operation class '.$title.' cannot be found.'); 52 | } else { 53 | $this->delete($operation); 54 | 55 | $this->info('Operation class '.$title.' deleted successfully.'); 56 | } 57 | } catch (Exception $e) { 58 | $this->error($e->getMessage()); 59 | } 60 | } 61 | 62 | /** 63 | * Get the console command arguments. 64 | * 65 | * @return array 66 | */ 67 | protected function getArguments() 68 | { 69 | return [ 70 | ['operation', InputArgument::REQUIRED, 'The operation\'s name.'], 71 | ['service', InputArgument::OPTIONAL, 'The service from which the operation should be deleted.'], 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the stub file for the generator. 77 | * 78 | * @return string 79 | */ 80 | protected function getStub() 81 | { 82 | return __DIR__ . '/../Generators/stubs/operation.stub'; 83 | } 84 | 85 | /** 86 | * Parse the operation name. 87 | * remove the Operation.php suffix if found 88 | * we're adding it ourselves. 89 | * 90 | * @param string $name 91 | * 92 | * @return string 93 | */ 94 | protected function parseName($name) 95 | { 96 | return Str::operation($name); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Console/Commands/OperationMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('service')); 51 | $title = $this->parseName($this->argument('operation')); 52 | $isQueueable = $this->option('queue'); 53 | try { 54 | $operation = $generator->generate($title, $service, $isQueueable); 55 | 56 | $this->info( 57 | 'Operation class '.$title.' created successfully.'. 58 | "\n". 59 | "\n". 60 | 'Find it at '.$operation->relativePath.''."\n" 61 | ); 62 | } catch (Exception $e) { 63 | $this->error($e->getMessage()); 64 | } 65 | } 66 | 67 | public function getArguments() 68 | { 69 | return [ 70 | ['operation', InputArgument::REQUIRED, 'The operation\'s name.'], 71 | ['service', InputArgument::OPTIONAL, 'The service in which the operation should be implemented.'], 72 | ['jobs', InputArgument::IS_ARRAY, 'A list of Jobs Operation calls'] 73 | ]; 74 | } 75 | 76 | public function getOptions() 77 | { 78 | return [ 79 | ['queue', 'Q', InputOption::VALUE_NONE, 'Whether a operation is queueable or not.'], 80 | ]; 81 | } 82 | 83 | /** 84 | * Get the stub file for the generator. 85 | * 86 | * @return string 87 | */ 88 | public function getStub() 89 | { 90 | return __DIR__ . '/../Generators/stubs/operation.stub'; 91 | } 92 | 93 | /** 94 | * Parse the operation name. 95 | * remove the Operation.php suffix if found 96 | * we're adding it ourselves. 97 | * 98 | * @param string $name 99 | * 100 | * @return string 101 | */ 102 | protected function parseName($name) 103 | { 104 | return Str::operation($name); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Console/Commands/PolicyDeleteCommand.php: -------------------------------------------------------------------------------- 1 | parsePolicyName($this->argument('policy')); 48 | 49 | if ( ! $this->exists($path = $this->findPolicyPath($policy))) { 50 | $this->error('Policy class ' . $policy . ' cannot be found.'); 51 | } else { 52 | $this->delete($path); 53 | 54 | $this->info('Policy class ' . $policy . ' deleted successfully.'); 55 | } 56 | } catch (Exception $e) { 57 | $this->error($e->getMessage()); 58 | } 59 | } 60 | 61 | /** 62 | * Get the console command arguments. 63 | * 64 | * @return array 65 | */ 66 | public function getArguments() 67 | { 68 | return [ 69 | ['policy', InputArgument::REQUIRED, 'The Policy\'s name.'] 70 | ]; 71 | } 72 | 73 | /** 74 | * Get the stub file for the generator. 75 | * 76 | * @return string 77 | */ 78 | public function getStub() 79 | { 80 | return __DIR__ . '/../Generators/stubs/policy.stub'; 81 | } 82 | 83 | /** 84 | * Parse the model name. 85 | * 86 | * @param string $name 87 | * @return string 88 | */ 89 | public function parsePolicyName($name) 90 | { 91 | return Str::policy($name); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Console/Commands/PolicyMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('policy'); 49 | 50 | try { 51 | $policy = $generator->generate($name); 52 | 53 | $this->info('Policy class created successfully.' . 54 | "\n" . 55 | "\n" . 56 | 'Find it at ' . $policy->relativePath . '' . "\n" 57 | ); 58 | } catch (Exception $e) { 59 | $this->error($e->getMessage()); 60 | } 61 | } 62 | 63 | /** 64 | * Get the console command arguments. 65 | * 66 | * @return array 67 | */ 68 | public function getArguments() 69 | { 70 | return [ 71 | ['policy', InputArgument::REQUIRED, 'The Policy\'s name.'] 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the stub file for the generator. 77 | * 78 | * @return string 79 | */ 80 | public function getStub() 81 | { 82 | return __DIR__ . '/../Generators/stubs/policy.stub'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Console/Commands/RequestDeleteCommand.php: -------------------------------------------------------------------------------- 1 | parseRequestName($this->argument('request')); 48 | $service = Str::service($this->argument('service')); 49 | 50 | if ( ! $this->exists($path = $this->findRequestPath($service, $request))) { 51 | $this->error('Request class ' . $request . ' cannot be found.'); 52 | } else { 53 | $this->delete($path); 54 | 55 | $this->info('Request class ' . $request . ' deleted successfully.'); 56 | } 57 | } catch (Exception $e) { 58 | $this->error($e->getMessage()); 59 | } 60 | } 61 | 62 | /** 63 | * Get the console command arguments. 64 | * 65 | * @return array 66 | */ 67 | public function getArguments() 68 | { 69 | return [ 70 | ['request', InputArgument::REQUIRED, 'The Request\'s name.'], 71 | ['service', InputArgument::REQUIRED, 'The Service\'s name.'], 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the stub file for the generator. 77 | * 78 | * @return string 79 | */ 80 | public function getStub() 81 | { 82 | return __DIR__ . '/../Generators/stubs/request.stub'; 83 | } 84 | 85 | /** 86 | * Parse the model name. 87 | * 88 | * @param string $name 89 | * @return string 90 | */ 91 | public function parseRequestName($name) 92 | { 93 | return Str::request($name); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Console/Commands/RequestMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 49 | $service = $this->argument('domain'); 50 | 51 | try { 52 | $request = $generator->generate($name, $service); 53 | 54 | $this->info('Request class created successfully.' . 55 | "\n" . 56 | "\n" . 57 | 'Find it at ' . $request->relativePath . '' . "\n" 58 | ); 59 | } catch (Exception $e) { 60 | $this->error($e->getMessage()); 61 | } 62 | } 63 | 64 | /** 65 | * Get the console command arguments. 66 | * 67 | * @return array 68 | */ 69 | public function getArguments() 70 | { 71 | return [ 72 | ['name', InputArgument::REQUIRED, 'The name of the class.'], 73 | ['domain', InputArgument::REQUIRED, 'The Domain in which this request should be generated.'], 74 | ]; 75 | } 76 | 77 | /** 78 | * Get the stub file for the generator. 79 | * 80 | * @return string 81 | */ 82 | public function getStub() 83 | { 84 | return __DIR__ . '/../Generators/stubs/request.stub'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Console/Commands/ServiceDeleteCommand.php: -------------------------------------------------------------------------------- 1 | isMicroservice()) { 64 | return $this->error('This functionality is disabled in a Microservice'); 65 | } 66 | 67 | try { 68 | $name = Str::service($this->argument('name')); 69 | 70 | if (!$this->exists($service = $this->findServicePath($name))) { 71 | return $this->error('Service '.$name.' cannot be found.'); 72 | } 73 | 74 | $this->delete($service); 75 | 76 | $this->info('Service '.$name.' deleted successfully.'."\n"); 77 | 78 | $this->info('Please remove your registered service providers, if any.'); 79 | } catch (\Exception $e) { 80 | dd($e->getMessage(), $e->getFile(), $e->getLine()); 81 | } 82 | } 83 | 84 | public function getArguments() 85 | { 86 | return [ 87 | ['name', InputArgument::REQUIRED, 'The service name.'], 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Console/Commands/ServiceMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 65 | 66 | $generator = new ServiceGenerator(); 67 | $service = $generator->generate($name); 68 | 69 | $this->info('Service '.$service->name.' created successfully.'."\n"); 70 | 71 | $rootNamespace = $this->findRootNamespace(); 72 | $serviceNamespace = $this->findServiceNamespace($service->name); 73 | 74 | $serviceProvider = $serviceNamespace.'\\Providers\\'.$service->name.'ServiceProvider'; 75 | 76 | $this->info('Activate it by adding '. 77 | ''.$serviceProvider.'::class '. 78 | "\nto 'providers' in config/app.php". 79 | "\n" 80 | ); 81 | } catch (\Exception $e) { 82 | $this->error($e->getMessage()."\n".$e->getFile().' at '.$e->getLine()); 83 | } 84 | } 85 | 86 | public function getArguments() 87 | { 88 | return [ 89 | ['name', InputArgument::REQUIRED, 'The service name.'], 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Console/Commands/ServicesListCommand.php: -------------------------------------------------------------------------------- 1 | listServices()->all(); 31 | 32 | $this->table(['Service', 'Slug', 'Path'], array_map(function($service) { 33 | return [$service->name, $service->slug, $service->relativePath]; 34 | }, $services)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domains/Http/Jobs/RespondWithJsonErrorJob.php: -------------------------------------------------------------------------------- 1 | content = [ 13 | 'status' => $status, 14 | 'error' => [ 15 | 'code' => $code, 16 | 'message' => $message, 17 | ], 18 | ]; 19 | 20 | $this->status = $status; 21 | $this->headers = $headers; 22 | $this->options = $options; 23 | } 24 | 25 | public function handle(ResponseFactory $response) 26 | { 27 | return $response->json($this->content, $this->status, $this->headers, $this->options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Domains/Http/Jobs/RespondWithJsonJob.php: -------------------------------------------------------------------------------- 1 | content = $content; 18 | $this->status = $status; 19 | $this->headers = $headers; 20 | $this->options = $options; 21 | } 22 | 23 | public function handle(ResponseFactory $factory) 24 | { 25 | $response = [ 26 | 'data' => $this->content, 27 | 'status' => $this->status, 28 | ]; 29 | 30 | return $factory->json($response, $this->status, $this->headers, $this->options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domains/Http/Jobs/RespondWithViewJob.php: -------------------------------------------------------------------------------- 1 | template = $template; 18 | $this->data = $data; 19 | $this->status = $status; 20 | $this->headers = $headers; 21 | } 22 | 23 | public function handle(ResponseFactory $factory) 24 | { 25 | return $factory->view($this->template, $this->data, $this->status, $this->headers); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Entities/Domain.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 12 | 'name' => $name, 13 | 'slug' => Str::studly($name), 14 | 'namespace' => $namespace, 15 | 'realPath' => $path, 16 | 'relativePath' => $relativePath, 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Entities/Entity.php: -------------------------------------------------------------------------------- 1 | attributes; 28 | } 29 | 30 | /** 31 | * Set the attributes for this component. 32 | * 33 | * @param array $attributes 34 | */ 35 | protected function setAttributes(array $attributes) 36 | { 37 | $this->attributes = $attributes; 38 | } 39 | 40 | /** 41 | * Get an attribute's value if found. 42 | * 43 | * @param string $key 44 | * 45 | * @return mixed 46 | */ 47 | public function __get($key) 48 | { 49 | if (isset($this->attributes[$key])) { 50 | return $this->attributes[$key]; 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Entities/Feature.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 12 | 'title' => $title, 13 | 'className' => $className, 14 | 'service' => $service, 15 | 'file' => $file, 16 | 'realPath' => $realPath, 17 | 'relativePath' => $relativePath, 18 | 'content' => $content, 19 | ]); 20 | } 21 | 22 | // public function toArray() 23 | // { 24 | // $attributes = parent::toArray(); 25 | // 26 | // // real path not needed 27 | // unset($attributes['realPath']); 28 | // 29 | // // map the service object to its name 30 | // $attributes['service'] = $attributes['service']->name; 31 | // 32 | // return $attributes; 33 | // } 34 | } 35 | -------------------------------------------------------------------------------- /src/Entities/Job.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 11 | 'title' => $title, 12 | 'className' => $className, 13 | 'namespace' => $namespace, 14 | 'file' => $file, 15 | 'realPath' => $path, 16 | 'relativePath' => $relativePath, 17 | 'domain' => $domain, 18 | 'content' => $content, 19 | ]); 20 | } 21 | 22 | public function toArray() 23 | { 24 | $attributes = parent::toArray(); 25 | 26 | if ($attributes['domain'] instanceof Domain) { 27 | $attributes['domain'] = $attributes['domain']->toArray(); 28 | } 29 | 30 | return $attributes; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Entities/Model.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 10 | 'model' => $title, 11 | 'namespace' => $namespace, 12 | 'file' => $file, 13 | 'path' => $path, 14 | 'relativePath' => $relativePath, 15 | 'content' => $content, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Entities/Operation.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 12 | 'title' => $title, 13 | 'className' => $className, 14 | 'service' => $service, 15 | 'file' => $file, 16 | 'realPath' => $realPath, 17 | 'relativePath' => $relativePath, 18 | 'content' => $content, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Entities/Policy.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 10 | 'policy' => $title, 11 | 'namespace' => $namespace, 12 | 'file' => $file, 13 | 'path' => $path, 14 | 'relativePath' => $relativePath, 15 | 'content' => $content, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Entities/Request.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 11 | 'request' => $title, 12 | 'domain' => $domain, 13 | 'namespace' => $namespace, 14 | 'file' => $file, 15 | 'path' => $path, 16 | 'relativePath' => $relativePath, 17 | 'content' => $content, 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Entities/Service.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 12 | 'name' => $name, 13 | 'slug' => Str::snake($name), 14 | 'realPath' => $realPath, 15 | 'relativePath' => $relativePath, 16 | ]); 17 | } 18 | 19 | // public function toArray() 20 | // { 21 | // $attributes = parent::toArray(); 22 | // 23 | // unset($attributes['realPath']); 24 | // 25 | // return $attributes; 26 | // } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/FeatureStarted.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->arguments = $arguments; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/JobStarted.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->arguments = $arguments; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/OperationStarted.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->arguments = $arguments; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidInputException.php: -------------------------------------------------------------------------------- 1 | messages()->all(); 18 | } 19 | 20 | if (is_array($message)) { 21 | $message = implode("\n", $message); 22 | } 23 | 24 | parent::__construct($message, $code, $previous); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Filesystem.php: -------------------------------------------------------------------------------- 1 | createDirectory(dirname($path)); 32 | 33 | return file_put_contents($path, $contents, $lock ? LOCK_EX : 0); 34 | } 35 | 36 | /** 37 | * Create a directory. 38 | * 39 | * @param string $path 40 | * @param int $mode 41 | * @param bool $recursive 42 | * @param bool $force 43 | * 44 | * @return bool 45 | */ 46 | public function createDirectory($path, $mode = 0755, $recursive = true, $force = true) 47 | { 48 | if ($force) { 49 | return @mkdir($path, $mode, $recursive); 50 | } 51 | 52 | return mkdir($path, $mode, $recursive); 53 | } 54 | 55 | /** 56 | * Delete an existing file or directory at the given path. 57 | * 58 | * @param string $path 59 | * 60 | * @return bool 61 | */ 62 | public function delete($path) 63 | { 64 | $filesystem = new SymfonyFilesystem(); 65 | 66 | $filesystem->remove($path); 67 | } 68 | 69 | public function rename($path, $name) 70 | { 71 | $filesystem = new SymfonyFilesystem(); 72 | 73 | $filesystem->rename($path, $name); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Generators/ControllerGenerator.php: -------------------------------------------------------------------------------- 1 | findControllerPath($service, $name); 16 | 17 | if ($this->exists($path)) { 18 | throw new Exception('Controller already exists!'); 19 | 20 | return false; 21 | } 22 | 23 | $namespace = $this->findControllerNamespace($service); 24 | 25 | $content = file_get_contents($this->getStub($resource)); 26 | $content = str_replace( 27 | ['{{controller}}', '{{namespace}}', '{{unit_namespace}}'], 28 | [$name, $namespace, $this->findUnitNamespace()], 29 | $content 30 | ); 31 | 32 | $this->createFile($path, $content); 33 | 34 | return $this->relativeFromReal($path); 35 | } 36 | 37 | /** 38 | * Get the stub file for the generator. 39 | * 40 | * @param $resource Determines whether to return the resource controller 41 | * @return string 42 | */ 43 | protected function getStub($resource) 44 | { 45 | if ($resource) { 46 | return __DIR__ . '/stubs/controller.resource.stub'; 47 | } 48 | 49 | return __DIR__ . '/stubs/controller.plain.stub'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Generators/DirectoriesGeneratorTrait.php: -------------------------------------------------------------------------------- 1 | directories as $parent => $children) { 23 | $paths = array_map(function ($child) use ($root, $parent) { 24 | return "$root/$parent/$child"; 25 | }, $children); 26 | 27 | foreach ($paths as $path) { 28 | $this->createDirectory($path); 29 | $this->createFile("$path/.gitkeep"); 30 | // collect path without root 31 | $created[] = str_replace($root, '', $path); 32 | } 33 | } 34 | 35 | return $created; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Generators/FeatureGenerator.php: -------------------------------------------------------------------------------- 1 | findFeaturePath($service, $feature); 17 | $classname = $this->classname($feature); 18 | 19 | if ($this->exists($path)) { 20 | throw new Exception('Feature already exists!'); 21 | 22 | return false; 23 | } 24 | 25 | $namespace = $this->findFeatureNamespace($service, $feature); 26 | 27 | $content = file_get_contents($this->getStub()); 28 | 29 | $useJobs = ''; // stores the `use` statements of the jobs 30 | $runJobs = ''; // stores the `$this->run` statements of the jobs 31 | 32 | foreach ($jobs as $index => $job) { 33 | $useJobs .= 'use '.$job['namespace'].'\\'.$job['className'].";\n"; 34 | $runJobs .= "\t\t".'$this->run('.$job['className'].'::class);'; 35 | 36 | // only add carriage returns when it's not the last job 37 | if ($index != count($jobs) - 1) { 38 | $runJobs .= "\n\n"; 39 | } 40 | } 41 | 42 | $content = str_replace( 43 | ['{{feature}}', '{{namespace}}', '{{unit_namespace}}', '{{use_jobs}}', '{{run_jobs}}'], 44 | [$classname, $namespace, $this->findUnitNamespace(), $useJobs, $runJobs], 45 | $content 46 | ); 47 | 48 | $this->createFile($path, $content); 49 | 50 | // generate test file 51 | $this->generateTestFile($feature, $service); 52 | 53 | return new Feature( 54 | $feature, 55 | basename($path), 56 | $path, 57 | $this->relativeFromReal($path), 58 | ($service) ? $this->findService($service) : null, 59 | $content 60 | ); 61 | } 62 | 63 | private function classname($feature) 64 | { 65 | $parts = explode(DS, $feature); 66 | 67 | return array_pop($parts); 68 | } 69 | 70 | /** 71 | * Generate the test file. 72 | * 73 | * @param string $feature 74 | * @param string $service 75 | */ 76 | private function generateTestFile($feature, $service) 77 | { 78 | $content = file_get_contents($this->getTestStub()); 79 | 80 | $namespace = $this->findFeatureTestNamespace($service); 81 | $featureClass = $this->classname($feature); 82 | $featureNamespace = $this->findFeatureNamespace($service, $feature)."\\".$featureClass; 83 | $testClass = $featureClass.'Test'; 84 | 85 | $content = str_replace( 86 | ['{{namespace}}', '{{testclass}}', '{{feature}}', '{{feature_namespace}}'], 87 | [$namespace, $testClass, Str::snake(str_replace(DS, '', $feature)), $featureNamespace], 88 | $content 89 | ); 90 | 91 | $path = $this->findFeatureTestPath($service, $feature.'Test'); 92 | 93 | $this->createFile($path, $content); 94 | } 95 | 96 | /** 97 | * Get the stub file for the generator. 98 | * 99 | * @return string 100 | */ 101 | protected function getStub() 102 | { 103 | return __DIR__ . '/stubs/feature.stub'; 104 | } 105 | 106 | /** 107 | * Get the test stub file for the generator. 108 | * 109 | * @return string 110 | */ 111 | private function getTestStub() 112 | { 113 | return __DIR__ . '/stubs/feature-test.stub'; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Generators/Generator.php: -------------------------------------------------------------------------------- 1 | version(); 23 | if ($majorOnly) { 24 | $version = explode('.', $version)[0]; 25 | } 26 | 27 | return $version; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Generators/JobGenerator.php: -------------------------------------------------------------------------------- 1 | findJobPath($domain, $job); 16 | 17 | if ($this->exists($path)) { 18 | throw new Exception('Job already exists'); 19 | } 20 | 21 | // Make sure the domain directory exists 22 | $this->createDomainDirectory($domain); 23 | 24 | // Create the job 25 | $namespace = $this->findDomainJobsNamespace($domain); 26 | 27 | $content = file_get_contents($this->getStub($isQueueable)); 28 | $content = str_replace( 29 | ['{{job}}', '{{namespace}}', '{{unit_namespace}}'], 30 | [$job, $namespace, $this->findUnitNamespace()], 31 | $content 32 | ); 33 | 34 | $this->createFile($path, $content); 35 | 36 | $this->generateTestFile($job, $domain); 37 | 38 | return new Job( 39 | $job, 40 | $namespace, 41 | basename($path), 42 | $path, 43 | $this->relativeFromReal($path), 44 | $this->findDomain($domain), 45 | $content 46 | ); 47 | } 48 | 49 | /** 50 | * Generate test file. 51 | * 52 | * @param string $job 53 | * @param string $domain 54 | */ 55 | private function generateTestFile($job, $domain) 56 | { 57 | $content = file_get_contents($this->getTestStub()); 58 | 59 | $namespace = $this->findDomainJobsTestsNamespace($domain); 60 | $jobNamespace = $this->findDomainJobsNamespace($domain)."\\$job"; 61 | $testClass = $job.'Test'; 62 | 63 | $content = str_replace( 64 | ['{{namespace}}', '{{testclass}}', '{{job}}', '{{job_namespace}}'], 65 | [$namespace, $testClass, Str::snake($job), $jobNamespace], 66 | $content 67 | ); 68 | 69 | $path = $this->findJobTestPath($domain, $testClass); 70 | 71 | $this->createFile($path, $content); 72 | } 73 | 74 | /** 75 | * Create domain directory. 76 | * 77 | * @param string $domain 78 | */ 79 | private function createDomainDirectory($domain) 80 | { 81 | $this->createDirectory($this->findDomainPath($domain).'/Jobs'); 82 | $this->createDirectory($this->findDomainTestsPath($domain).'/Jobs'); 83 | } 84 | 85 | /** 86 | * Get the stub file for the generator. 87 | * 88 | * @return string 89 | */ 90 | public function getStub($isQueueable = false) 91 | { 92 | $stubName; 93 | if ($isQueueable) { 94 | $stubName = '/stubs/job-queueable.stub'; 95 | } else { 96 | $stubName = '/stubs/job.stub'; 97 | } 98 | return __DIR__.$stubName; 99 | } 100 | 101 | /** 102 | * Get the test stub file for the generator. 103 | * 104 | * @return string 105 | */ 106 | public function getTestStub() 107 | { 108 | return __DIR__ . '/stubs/job-test.stub'; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Generators/MicroGenerator.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'Data', 21 | 'Domains', 22 | 'Features', 23 | 'Operations', 24 | 'Data/Models', 25 | ], 26 | 'tests' => [ 27 | 'Domains', 28 | 'Operations', 29 | ] 30 | ]; 31 | 32 | /** 33 | * Generate initial directory structure. 34 | * 35 | * @return array 36 | */ 37 | public function generate() 38 | { 39 | $created = $this->generateDirectories(); 40 | 41 | $created = array_merge($created, $this->generateCustomDirectories()); 42 | 43 | $this->updatePHPUnitXML(); 44 | 45 | return $created; 46 | } 47 | 48 | private function updatePHPUnitXML() 49 | { 50 | $root = base_path(); 51 | $path = "$root/phpunit.xml"; 52 | 53 | if ($this->exists($path)) { 54 | $xml = new DOMDocument('1.0', 'UTF-8'); 55 | $xml->preserveWhiteSpace = true; 56 | $xml->formatOutput = true; 57 | $xml->load($path); 58 | 59 | $fragment = $xml->createDocumentFragment(); 60 | $fragment->appendXML($this->testsuites()); 61 | 62 | $xpath = new DOMXPath($xml); 63 | 64 | // replace tests/Feature with tests/Features 65 | $feature = $xpath->evaluate('//testsuite[@name="Feature"]')->item(0); 66 | if ($feature) { 67 | $feature->parentNode->removeChild($feature); 68 | $xml->save($path); 69 | } 70 | 71 | $xpath->evaluate('//testsuites') 72 | ->item(0) 73 | ->appendChild($fragment); 74 | 75 | $xml->save($path); 76 | } 77 | } 78 | 79 | private function testsuites() 80 | { 81 | return << 83 | ./tests/Domains 84 | 85 | 86 | ./tests/Operations 87 | 88 | 89 | ./tests/Features 90 | 91 | \t 92 | XMLSUITE; 93 | 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | private function generateCustomDirectories() 100 | { 101 | $root = base_path(); 102 | 103 | $created = []; 104 | // rename or create tests/Features directory 105 | if ($this->exists("$root/tests/Feature")) { 106 | $this->rename("$root/tests/Feature", "$root/tests/Features"); 107 | } else { 108 | $this->createDirectory("$root/tests/Features"); 109 | $created[] = 'tests/Features'; 110 | } 111 | 112 | return $created; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Generators/ModelGenerator.php: -------------------------------------------------------------------------------- 1 | findModelPath($model); 22 | 23 | if ($this->exists($path)) { 24 | throw new Exception('Model already exists'); 25 | 26 | return false; 27 | } 28 | 29 | $namespace = $this->findModelNamespace(); 30 | 31 | $content = file_get_contents($this->getStub()); 32 | $content = str_replace( 33 | ['{{model}}', '{{namespace}}', '{{unit_namespace}}'], 34 | [$model, $namespace, $this->findUnitNamespace()], 35 | $content 36 | ); 37 | 38 | $this->createFile($path, $content); 39 | 40 | return new Model( 41 | $model, 42 | $namespace, 43 | basename($path), 44 | $path, 45 | $this->relativeFromReal($path), 46 | $content 47 | ); 48 | } 49 | 50 | /** 51 | * Get the stub file for the generator. 52 | * 53 | * @return string 54 | */ 55 | public function getStub() 56 | { 57 | if ($this->laravelVersion() > 7) { 58 | return __DIR__ . '/../Generators/stubs/model-8.stub'; 59 | } 60 | 61 | return __DIR__ . '/../Generators/stubs/model.stub'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Generators/MonolithGenerator.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'Data', 12 | 'Domains', 13 | 'Services', 14 | 'Foundation', 15 | 'Policies', 16 | 'Data/Models', 17 | ] 18 | ]; 19 | 20 | public function generate() 21 | { 22 | return $this->generateDirectories(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Generators/OperationGenerator.php: -------------------------------------------------------------------------------- 1 | findOperationPath($service, $operation); 17 | 18 | if ($this->exists($path)) { 19 | throw new Exception('Operation already exists!'); 20 | 21 | return false; 22 | } 23 | 24 | $namespace = $this->findOperationNamespace($service); 25 | 26 | $content = file_get_contents($this->getStub($isQueueable)); 27 | 28 | list($useJobs, $runJobs) = self::getUsesAndRunners($jobs); 29 | 30 | $content = str_replace( 31 | ['{{operation}}', '{{namespace}}', '{{unit_namespace}}', '{{use_jobs}}', '{{run_jobs}}'], 32 | [$operation, $namespace, $this->findUnitNamespace(), $useJobs, $runJobs], 33 | $content 34 | ); 35 | 36 | $this->createFile($path, $content); 37 | 38 | // generate test file 39 | $this->generateTestFile($operation, $service); 40 | 41 | return new Operation( 42 | $operation, 43 | basename($path), 44 | $path, 45 | $this->relativeFromReal($path), 46 | ($service) ? $this->findService($service) : null, 47 | $content 48 | ); 49 | } 50 | 51 | /** 52 | * Generate the test file. 53 | * 54 | * @param string $operation 55 | * @param string $service 56 | */ 57 | private function generateTestFile($operation, $service) 58 | { 59 | $content = file_get_contents($this->getTestStub()); 60 | 61 | $namespace = $this->findOperationTestNamespace($service); 62 | $operationNamespace = $this->findOperationNamespace($service)."\\$operation"; 63 | $testClass = $operation.'Test'; 64 | 65 | $content = str_replace( 66 | ['{{namespace}}', '{{testclass}}', '{{operation}}', '{{operation_namespace}}'], 67 | [$namespace, $testClass, Str::snake($operation), $operationNamespace], 68 | $content 69 | ); 70 | 71 | $path = $this->findOperationTestPath($service, $testClass); 72 | 73 | $this->createFile($path, $content); 74 | } 75 | 76 | /** 77 | * Get the stub file for the generator. 78 | * 79 | * @return string 80 | */ 81 | protected function getStub($isQueueable = false) 82 | { 83 | $stubName; 84 | if ($isQueueable) { 85 | $stubName = '/stubs/operation-queueable.stub'; 86 | } else { 87 | $stubName = '/stubs/operation.stub'; 88 | } 89 | return __DIR__.$stubName; 90 | } 91 | 92 | /** 93 | * Get the test stub file for the generator. 94 | * 95 | * @return string 96 | */ 97 | private function getTestStub() 98 | { 99 | return __DIR__ . '/stubs/operation-test.stub'; 100 | } 101 | 102 | /** 103 | * Get de use to import the right class 104 | * Get de job run command 105 | * @param $job 106 | * @return array 107 | */ 108 | static private function getUseAndJobRunCommand($job) 109 | { 110 | $str = str_replace_last('\\','#', $job); 111 | $explode = explode('#', $str); 112 | 113 | $use = 'use '.$explode[0].'\\'.$explode['1'].";\n"; 114 | $runJobs = "\t\t".'$this->run('.$explode['1'].'::class);'; 115 | 116 | return array($use, $runJobs); 117 | } 118 | 119 | /** 120 | * Returns all users and all $this->run() generated 121 | * @param $jobs 122 | * @return array 123 | */ 124 | static private function getUsesAndRunners($jobs) 125 | { 126 | $useJobs = ''; 127 | $runJobs = ''; 128 | foreach ($jobs as $index => $job) { 129 | 130 | list($useLine, $runLine) = self::getUseAndJobRunCommand($job); 131 | $useJobs .= $useLine; 132 | $runJobs .= $runLine; 133 | // only add carriage returns when it's not the last job 134 | if ($index != count($jobs) - 1) { 135 | $runJobs .= "\n\n"; 136 | } 137 | } 138 | return array($useJobs, $runJobs); 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Generators/PolicyGenerator.php: -------------------------------------------------------------------------------- 1 | findPolicyPath($policy); 23 | 24 | if ($this->exists($path)) { 25 | throw new Exception('Policy already exists'); 26 | 27 | return false; 28 | } 29 | 30 | $this->createPolicyDirectory(); 31 | 32 | $namespace = $this->findPolicyNamespace(); 33 | 34 | $content = file_get_contents($this->getStub()); 35 | $content = str_replace( 36 | ['{{policy}}', '{{namespace}}'], 37 | [$policy, $namespace], 38 | $content 39 | ); 40 | 41 | $this->createFile($path, $content); 42 | 43 | return new Policy( 44 | $policy, 45 | $namespace, 46 | basename($path), 47 | $path, 48 | $this->relativeFromReal($path), 49 | $content 50 | ); 51 | } 52 | 53 | /** 54 | * Create Policies directory. 55 | */ 56 | public function createPolicyDirectory() 57 | { 58 | $this->createDirectory($this->findPoliciesPath()); 59 | } 60 | 61 | /** 62 | * Get the stub file for the generator. 63 | * 64 | * @return string 65 | */ 66 | public function getStub() 67 | { 68 | return __DIR__ . '/../Generators/stubs/policy.stub'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Generators/RequestGenerator.php: -------------------------------------------------------------------------------- 1 | findRequestPath($domain, $request); 27 | 28 | if ($this->exists($path)) { 29 | throw new Exception('Request already exists'); 30 | } 31 | 32 | $namespace = $this->findRequestsNamespace($domain); 33 | 34 | $content = file_get_contents($this->getStub()); 35 | $content = str_replace( 36 | ['{{request}}', '{{namespace}}'], 37 | [$request, $namespace], 38 | $content 39 | ); 40 | 41 | $this->createFile($path, $content); 42 | 43 | return new Request( 44 | $request, 45 | $domain, 46 | $namespace, 47 | basename($path), 48 | $path, 49 | $this->relativeFromReal($path), 50 | $content 51 | ); 52 | } 53 | 54 | /** 55 | * Get the stub file for the generator. 56 | * 57 | * @return string 58 | */ 59 | public function getStub() 60 | { 61 | return __DIR__ . '/../Generators/stubs/request.stub'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Generators/ServiceGenerator.php: -------------------------------------------------------------------------------- 1 | findServicePath($name); 42 | 43 | if ($this->exists($path)) { 44 | throw new Exception('Service already exists!'); 45 | 46 | return false; 47 | } 48 | 49 | // create service directory 50 | $this->createDirectory($path); 51 | // create .gitkeep file in it 52 | $this->createFile($path.'/.gitkeep'); 53 | 54 | $this->createServiceDirectories($path); 55 | 56 | $this->addServiceProviders($name, $slug, $path); 57 | 58 | $this->addRoutesFiles($name, $slug, $path); 59 | 60 | $this->addWelcomeViewFile($path); 61 | 62 | return new Service( 63 | $name, 64 | $slug, 65 | $path, 66 | $this->relativeFromReal($path) 67 | ); 68 | } 69 | 70 | /** 71 | * Create the default directories at the given service path. 72 | * 73 | * @param string $path 74 | * 75 | * @return void 76 | */ 77 | public function createServiceDirectories($path) 78 | { 79 | foreach ($this->directories as $directory) { 80 | $this->createDirectory($path.'/'.$directory); 81 | $this->createFile($path.'/'.$directory.'/.gitkeep'); 82 | } 83 | } 84 | 85 | /** 86 | * Add the corresponding service provider for the created service. 87 | * 88 | * @param string $name 89 | * @param string $path 90 | * 91 | * @return bool 92 | */ 93 | public function addServiceProviders($name, $slug, $path) 94 | { 95 | $namespace = $this->findServiceNamespace($name).'\\Providers'; 96 | 97 | $this->createRegistrationServiceProvider($name, $path, $slug, $namespace); 98 | 99 | $this->createRouteServiceProvider($name, $path, $slug, $namespace); 100 | 101 | $this->createBroadcastServiceProvider($name, $path, $slug, $namespace); 102 | } 103 | 104 | /** 105 | * Create the service provider that registers broadcast channels. 106 | * 107 | * @param $name 108 | * @param $path 109 | * @param $slug 110 | * @param $namespace 111 | */ 112 | public function createBroadcastServiceProvider($name, $path, $slug, $namespace) 113 | { 114 | $content = file_get_contents(__DIR__ . "/stubs/broadcastserviceprovider.stub"); 115 | $content = str_replace( 116 | ['{{name}}', '{{slug}}', '{{namespace}}'], 117 | [$name, $slug, $namespace], 118 | $content 119 | ); 120 | 121 | $this->createFile($path.'/Providers/BroadcastServiceProvider.php', $content); 122 | } 123 | 124 | /** 125 | * Create the service provider that registers this service. 126 | * 127 | * @param string $name 128 | * @param string $path 129 | */ 130 | public function createRegistrationServiceProvider($name, $path, $slug, $namespace) 131 | { 132 | $stub = 'serviceprovider.stub'; 133 | if ((int) $this->laravelVersion() > 7) { 134 | $stub = 'serviceprovider-8.stub'; 135 | } 136 | 137 | $content = file_get_contents(__DIR__ . "/stubs/$stub"); 138 | $content = str_replace( 139 | ['{{name}}', '{{slug}}', '{{namespace}}'], 140 | [$name, $slug, $namespace], 141 | $content 142 | ); 143 | 144 | $this->createFile($path.'/Providers/'.$name.'ServiceProvider.php', $content); 145 | } 146 | 147 | /** 148 | * Create the routes service provider file. 149 | * 150 | * @param string $name 151 | * @param string $path 152 | * @param string $slug 153 | * @param string $namespace 154 | */ 155 | public function createRouteServiceProvider($name, $path, $slug, $namespace) 156 | { 157 | $serviceNamespace = $this->findServiceNamespace($name); 158 | $controllers = $serviceNamespace.'\Http\Controllers'; 159 | $foundation = $this->findUnitNamespace(); 160 | 161 | $content = file_get_contents(__DIR__ . '/stubs/routeserviceprovider.stub'); 162 | $content = str_replace( 163 | ['{{name}}', '{{namespace}}', '{{controllers_namespace}}', '{{unit_namespace}}'], 164 | [$name, $namespace, $controllers, $foundation], 165 | $content 166 | ); 167 | 168 | $this->createFile($path.'/Providers/RouteServiceProvider.php', $content); 169 | } 170 | 171 | /** 172 | * Add the routes files. 173 | * 174 | * @param string $name 175 | * @param string $slug 176 | * @param string $path 177 | */ 178 | public function addRoutesFiles($name, $slug, $path) 179 | { 180 | $controllers = 'src/Services/' . $name . '/Http/Controllers'; 181 | 182 | $api = file_get_contents(__DIR__ . '/stubs/routes-api.stub'); 183 | $api = str_replace(['{{slug}}', '{{controllers_path}}'], [$slug, $controllers], $api); 184 | 185 | $web = file_get_contents(__DIR__ . '/stubs/routes-web.stub'); 186 | $web = str_replace(['{{slug}}', '{{controllers_path}}'], [$slug, $controllers], $web); 187 | 188 | $channels = file_get_contents(__DIR__ . '/stubs/routes-channels.stub'); 189 | $channels = str_replace(['{{namespace}}'], [$this->findServiceNamespace($name)], $channels); 190 | 191 | $console = file_get_contents(__DIR__ . '/stubs/routes-console.stub'); 192 | 193 | $this->createFile($path . '/routes/api.php', $api); 194 | $this->createFile($path . '/routes/web.php', $web); 195 | $this->createFile($path . '/routes/channels.php', $channels); 196 | $this->createFile($path . '/routes/console.php', $console); 197 | 198 | unset($api, $web, $channels, $console); 199 | 200 | $this->delete($path . '/routes/.gitkeep'); 201 | } 202 | 203 | /** 204 | * Add the welcome view file. 205 | * 206 | * @param string $path 207 | */ 208 | public function addWelcomeViewFile($path) 209 | { 210 | $this->createFile( 211 | $path.'/resources/views/welcome.blade.php', 212 | file_get_contents(__DIR__ . '/stubs/welcome.blade.stub') 213 | ); 214 | } 215 | 216 | /** 217 | * Get the stub file for the generator. 218 | * 219 | * @return string 220 | */ 221 | protected function getStub() 222 | { 223 | return __DIR__.'/stubs/service.stub'; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Generators/stubs/broadcastserviceprovider.stub: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Generators/stubs/feature.stub: -------------------------------------------------------------------------------- 1 | app->register(CustomServiceProvider::class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Generators/stubs/job-queueable.stub: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Generators/stubs/job.stub: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Generators/stubs/operation.stub: -------------------------------------------------------------------------------- 1 | '{{slug}}'], function() { 16 | 17 | // Controllers live in {{controllers_path}} 18 | 19 | Route::get('/', function() { 20 | return response()->json(['path' => '/api/{{slug}}']); 21 | }); 22 | 23 | Route::middleware('auth:api')->get('/user', function (Request $request) { 24 | return $request->user(); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /src/Generators/stubs/routes-channels.stub: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /src/Generators/stubs/routes-console.stub: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->describe('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /src/Generators/stubs/routes-web.stub: -------------------------------------------------------------------------------- 1 | '{{slug}}'], function() { 15 | 16 | // The controllers live in {{controllers_path}} 17 | // Route::get('/', 'UserController@index'); 18 | 19 | Route::get('/', function() { 20 | return view('{{slug}}::welcome'); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/Generators/stubs/routeserviceprovider.stub: -------------------------------------------------------------------------------- 1 | loadRoutesFiles($router, $namespace, $pathApi, $pathWeb); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Generators/stubs/serviceprovider-8.stub: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom([ 27 | realpath(__DIR__ . '/../database/migrations') 28 | ]); 29 | } 30 | 31 | /** 32 | * Register the {{name}} service provider. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $this->app->register(RouteServiceProvider::class); 39 | $this->app->register(BroadcastServiceProvider::class); 40 | 41 | $this->registerResources(); 42 | } 43 | 44 | /** 45 | * Register the {{name}} service resource namespaces. 46 | * 47 | * @return void 48 | */ 49 | protected function registerResources() 50 | { 51 | // Translation must be registered ahead of adding lang namespaces 52 | $this->app->register(TranslationServiceProvider::class); 53 | 54 | Lang::addNamespace('{{slug}}', realpath(__DIR__.'/../resources/lang')); 55 | 56 | View::addNamespace('{{slug}}', base_path('resources/views/vendor/{{slug}}')); 57 | View::addNamespace('{{slug}}', realpath(__DIR__.'/../resources/views')); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Generators/stubs/serviceprovider.stub: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom([ 30 | realpath(__DIR__ . '/../database/migrations') 31 | ]); 32 | 33 | $this->app->make(EloquentFactory::class) 34 | ->load(realpath(__DIR__ . '/../database/factories')); 35 | } 36 | 37 | /** 38 | * Register the {{name}} service provider. 39 | * 40 | * @return void 41 | */ 42 | public function register() 43 | { 44 | $this->app->register(RouteServiceProvider::class); 45 | $this->app->register(BroadcastServiceProvider::class); 46 | 47 | $this->registerResources(); 48 | } 49 | 50 | /** 51 | * Register the {{name}} service resource namespaces. 52 | * 53 | * @return void 54 | */ 55 | protected function registerResources() 56 | { 57 | // Translation must be registered ahead of adding lang namespaces 58 | $this->app->register(TranslationServiceProvider::class); 59 | 60 | Lang::addNamespace('{{slug}}', realpath(__DIR__.'/../resources/lang')); 61 | 62 | View::addNamespace('{{slug}}', base_path('resources/views/vendor/{{slug}}')); 63 | View::addNamespace('{{slug}}', realpath(__DIR__.'/../resources/views')); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Generators/stubs/welcome.blade.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Laravel • Lucid 5 | 6 | 7 | 8 | 37 | 38 | 39 |
40 |
41 |
Laravel • Lucid
42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | realPath); 28 | 29 | $body = explode("\n", $this->parseFunctionBody($contents, 'handle')); 30 | 31 | $jobs = []; 32 | foreach ($body as $line) { 33 | $job = $this->parseJobInLine($line, $contents); 34 | if (!empty($job)) { 35 | $jobs[] = $job; 36 | } 37 | } 38 | 39 | return $jobs; 40 | } 41 | 42 | public function parseFunctionBody($contents, $function) 43 | { 44 | // $pattern = "/function\s$function\([a-zA-Z0-9_\$\s,]+\)?". // match "function handle(...)" 45 | // '[\n\s]?[\t\s]*'. // regardless of the indentation preceding the { 46 | // '{([^{}]*)}/'; // find everything within braces. 47 | 48 | $pattern = '~^\s*[\w\s]+\(.*\)\s*\K({((?>"[^"]*+"|\'[^\']*+\'|//.*$|/\*[\s\S]*?\*/|#.*$|<<<\s*["\']?(\w+)["\']?[^;]+\3;$|[^{}<\'"/#]++|[^{}]++|(?1))*)})~m'; 49 | 50 | // '~^ \s* [\w\s]+ \( .* \) \s* \K'. # how it matches a function definition 51 | // '('. # (1 start) 52 | // '{'. # opening brace 53 | // '('. # (2 start) 54 | /* '(?>'.*/ # atomic grouping (for its non-capturing purpose only) 55 | // '" [^"]*+ "'. # double quoted strings 56 | // '| \' [^\']*+ \''. # single quoted strings 57 | // '| // .* $'. # a comment block starting with // 58 | // '| /\* [\s\S]*? \*/'. # a multi line comment block /*...*/ 59 | // '| \# .* $'. # a single line comment block starting with #... 60 | // '| <<< \s* ["\']?'. # heredocs and nowdocs 61 | // '( \w+ )'. # (3) ^ 62 | // '["\']? [^;]+ \3 ; $'. # ^ 63 | // '| [^{}<\'"/#]++'. # force engine to backtack if it encounters special characters [<'"/#] (possessive) 64 | // '| [^{}]++'. # default matching bahaviour (possessive) 65 | // '| (?1)'. # recurse 1st capturing group 66 | // ')*'. # zero to many times of atomic group 67 | // ')'. # (2 end) 68 | // '}'. # closing brace 69 | // ')~'; # (1 end) 70 | 71 | 72 | preg_match($pattern, $contents, $match); 73 | 74 | return $match[1]; 75 | } 76 | 77 | /** 78 | * Parses the job class out of the given line of code. 79 | * 80 | * @param string $line 81 | * @param string $contents 82 | * 83 | * @return string 84 | */ 85 | public function parseJobInLine($line, $contents) 86 | { 87 | $line = trim($line); 88 | // match the line that potentially has the job, 89 | // they're usually called by "$this->run(Job...)" 90 | preg_match('/->run\(([^,]*),?.*\)?/i', $line, $match); 91 | 92 | // we won't do anything if no job has been matched. 93 | if (empty($match)) { 94 | return ''; 95 | } 96 | 97 | $match = $match[1]; 98 | // prepare for parsing 99 | $match = $this->filterJobMatch($match); 100 | 101 | /* 102 | * determine syntax style and afterwards detect how the job 103 | * class name was put into the "run" method as a parameter. 104 | * 105 | * Following are the different ways this might occur: 106 | * 107 | * - ValidateArticleInputJob::class 108 | * The class name has been imported with a 'use' statement 109 | * and uses the ::class keyword. 110 | * - \Fully\Qualified\Namespace::class 111 | * Using the ::class keyword with a FQDN. 112 | * - 'Fully\Qualified\Namespace' 113 | * Using a string as a class name with FQDN. 114 | * - new \Full\Class\Namespace 115 | * Instantiation with FQDN 116 | * - new ImportedClass($input) 117 | * Instantiation with an imported class using a `use` statement 118 | * passing parameters to the construction of the instance. 119 | * - new ImportedClass 120 | * Instantiation without parameters nor parantheses. 121 | */ 122 | switch ($this->jobSyntaxStyle($match)) { 123 | case self::SYNTAX_STRING: 124 | list($name, $namespace) = $this->parseStringJobSyntax($match, $contents); 125 | break; 126 | 127 | case self::SYNTAX_KEYWORD: 128 | list($name, $namespace) = $this->parseKeywordJobSyntax($match, $contents); 129 | break; 130 | 131 | case self::SYNTAX_INSTANTIATION: 132 | list($name, $namespace) = $this->parseInitJobSyntax($match, $contents); 133 | break; 134 | } 135 | 136 | $domainName = $this->domainForJob($namespace); 137 | 138 | $domain = new Domain( 139 | $domainName, 140 | $this->findDomainNamespace($domainName), 141 | $domainPath = $this->findDomainPath($domainName), 142 | $this->relativeFromReal($domainPath) 143 | ); 144 | 145 | $path = $this->findJobPath($domainName, $name); 146 | 147 | $job = new Job( 148 | $name, 149 | $namespace, 150 | basename($path), 151 | $path, 152 | $this->relativeFromReal($path), 153 | $domain 154 | ); 155 | 156 | return $job; 157 | } 158 | 159 | /** 160 | * Parse the given job class written in the string syntax: 'Some\Domain\Job' 161 | * 162 | * @param string $match 163 | * @param string $contents 164 | * 165 | * @return string 166 | */ 167 | private function parseStringJobSyntax($match, $contents) 168 | { 169 | $slash = strrpos($match, '\\'); 170 | if ($slash !== false) { 171 | $name = str_replace('\\', '', substr($match, $slash)); 172 | $namespace = '\\'.preg_replace('/^\\\/', '', $match); 173 | } 174 | 175 | return [$name, $namespace]; 176 | } 177 | 178 | /** 179 | * Parse the given job class written in the ::class keyword syntax: SomeJob::class 180 | * 181 | * @param string $match 182 | * @param string $contents 183 | * 184 | * @return string 185 | */ 186 | private function parseKeywordJobSyntax($match, $contents) 187 | { 188 | // is it of the form \Full\Name\Space::class? 189 | // (using full namespace in-line) 190 | // to figure that out we look for 191 | // the last occurrence of a \ 192 | $slash = strrpos($match, '\\'); 193 | if ($slash !== false) { 194 | $namespace = str_replace('::class', '', $match); 195 | // remove the ::class and the \ prefix 196 | $name = str_replace(['\\', '::class'], '', substr($namespace, $slash)); 197 | } else { 198 | // nope it's just Space::class, we will figure 199 | // out the namespace from a "use" statement. 200 | $name = str_replace(['::class', ');'], '', $match); 201 | preg_match("/use\s(.*$name)/", $contents, $namespace); 202 | // it is necessary to have a \ at the beginning. 203 | $namespace = '\\'.preg_replace('/^\\\/', '', $namespace[1]); 204 | } 205 | 206 | return [$name, $namespace]; 207 | } 208 | 209 | /** 210 | * Parse the given job class written in the ini syntax: new SomeJob() 211 | * 212 | * @param string $match 213 | * @param string $contents 214 | * 215 | * @return string 216 | */ 217 | private function parseInitJobSyntax($match, $contents) 218 | { 219 | // remove the 'new ' from the beginning. 220 | $match = str_replace('new ', '', $match); 221 | 222 | // match the job's class name 223 | preg_match('/(.*Job).*[\);]?/', $match, $name); 224 | $name = $name[1]; 225 | 226 | // Determine Namespace 227 | $slash = strrpos($name, '\\'); 228 | // when there's a slash when matching the reverse of the namespace, 229 | // it is considered to be the full namespace we have. 230 | if ($slash !== false) { 231 | $namespace = $name; 232 | // prefix with a \ if not found. 233 | $name = str_replace('\\', '', substr($namespace, $slash)); 234 | } else { 235 | // we don't have the full namespace so we will figure it out 236 | // from the 'use' statements that we have in the file. 237 | preg_match("/use\s(.*$name)/", $contents, $namespace); 238 | $namespace = '\\'.preg_replace('/^\\\/', '', $namespace[1]); 239 | } 240 | 241 | return [$name, $namespace]; 242 | } 243 | 244 | /** 245 | * Get the domain for the given job's namespace. 246 | * 247 | * @param string $namespace 248 | * 249 | * @return string 250 | */ 251 | private function domainForJob($namespace) 252 | { 253 | preg_match('/Domains\\\([^\\\]*)\\\Jobs/', $namespace, $domain); 254 | 255 | return (!empty($domain)) ? $domain[1] : ''; 256 | } 257 | 258 | // if (strpos($match, '::class') !== false) { 259 | // 260 | // } elseif(strpos($match, 'new ') !== false) { 261 | // 262 | // } else { 263 | // 264 | // } 265 | 266 | /** 267 | * Filter the matched line in preparation for parsing. 268 | * 269 | * @param string $match The matched job line. 270 | * 271 | * @return string 272 | */ 273 | private function filterJobMatch($match) 274 | { 275 | // we don't want any quotes 276 | return str_replace(['"', "'"], '', $match); 277 | } 278 | 279 | /** 280 | * Determine the syntax style of the class name. 281 | * There are three styles possible: 282 | * 283 | * - Using the 'TheJob::class' keyword 284 | * - Using instantiation: new TheJob(...) 285 | * - Using a string with the full namespace: '\Domain\TheJob' 286 | * 287 | * @param string $match 288 | * 289 | * @return string 290 | */ 291 | private function jobSyntaxStyle($match) 292 | { 293 | if (strpos($match, '::class') !== false) { 294 | $style = self::SYNTAX_KEYWORD; 295 | } elseif(strpos($match, 'new ') !== false) { 296 | $style = self::SYNTAX_INSTANTIATION; 297 | } else { 298 | $style = self::SYNTAX_STRING; 299 | } 300 | 301 | return $style; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes($router, $namespace, $pathApi); 27 | } 28 | if (is_string($pathWeb) && is_file($pathWeb)) { 29 | $this->mapWebRoutes($router, $namespace, $pathWeb); 30 | } 31 | } 32 | 33 | /** 34 | * Define the "api" routes for the application. 35 | * 36 | * These routes are typically stateless. 37 | * 38 | * @param $router 39 | * @param $namespace 40 | * @param $path 41 | * @param $prefix 42 | * 43 | * @return void 44 | */ 45 | protected function mapApiRoutes($router, $namespace, $path, $prefix='api') 46 | { 47 | $router->group([ 48 | 'middleware' => 'api', 49 | 'namespace' => $namespace, 50 | 'prefix' => $prefix // to allow the delete or change of api prefix 51 | ], function ($router) use ($path) { 52 | require $path; 53 | }); 54 | } 55 | 56 | /** 57 | * Define the "web" routes for the application. 58 | * 59 | * These routes all receive session state, CSRF protection, etc. 60 | * 61 | * @param $router 62 | * @param $namespace 63 | * @param $path 64 | * 65 | * @return void 66 | */ 67 | protected function mapWebRoutes($router, $namespace, $path) 68 | { 69 | $router->group([ 70 | 'middleware' => 'web', 71 | 'namespace' => $namespace 72 | ], function ($router) use ($path) { 73 | require $path; 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Str.php: -------------------------------------------------------------------------------- 1 | has($unit)) { 17 | $mock = $registry->get($unit); 18 | $mock->setConstructorExpectations($constructorExpectations); 19 | } else { 20 | $mock = new UnitMock($unit, $constructorExpectations); 21 | $registry->register($unit, $mock); 22 | } 23 | 24 | return $mock; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Testing/UnitMock.php: -------------------------------------------------------------------------------- 1 | unit = $unit; 50 | $this->setConstructorExpectations($constructorExpectations); 51 | } 52 | 53 | public function setConstructorExpectations(array $constructorExpectations) 54 | { 55 | $this->currentConstructorExpectations = $constructorExpectations; 56 | $this->constructorExpectations[] = $this->currentConstructorExpectations; 57 | } 58 | 59 | public function getConstructorExpectations() 60 | { 61 | return $this->constructorExpectations; 62 | } 63 | 64 | /** 65 | * Returns constructor expectations array that matches the given $unit. 66 | * Empty array otherwise. 67 | * 68 | * @param $unit 69 | * @return array|mixed|void 70 | * @throws ReflectionException 71 | */ 72 | public function getConstructorExpectationsForInstance($unit) 73 | { 74 | foreach ($this->constructorExpectations as $index => $args) { 75 | $expected = new $unit(...$args); 76 | 77 | $ref = new ReflectionClass($unit); 78 | 79 | // we start by assuming that the unit instance and the $expected one are equal 80 | // until proven otherwise when we find differences between properties. 81 | $isEqual = true; 82 | foreach ($ref->getProperties() as $property) { 83 | if ($property->getValue($unit) !== $property->getValue($expected)) { 84 | $isEqual = false; 85 | break; 86 | } 87 | } 88 | 89 | if ($isEqual) { 90 | return $this->constructorExpectations[$index]; 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * @return array 97 | * @throws ReflectionException 98 | * @throws Exception 99 | */ 100 | private function getCurrentConstructorArgs(): array 101 | { 102 | $args = []; 103 | 104 | $reflection = new ReflectionClass($this->unit); 105 | 106 | if ($constructor = $reflection->getConstructor()) { 107 | $args = array_map(function ($parameter) { 108 | return $this->getParameterValueForCommand( 109 | $this->unit, 110 | collect(), 111 | $parameter, 112 | $this->currentConstructorExpectations 113 | ); 114 | }, $constructor->getParameters()); 115 | } 116 | 117 | return $args; 118 | } 119 | 120 | /** 121 | * Register unit mock for current constructor expectations. 122 | * 123 | * @return $this 124 | * @throws ReflectionException 125 | * @throws Exception 126 | */ 127 | private function registerMock(): UnitMock 128 | { 129 | $this->currentMock = Mockery::mock("{$this->unit}[handle]", $this->getCurrentConstructorArgs()); 130 | $this->mocks[] = $this->currentMock; 131 | 132 | // $args will be what the developer passed to the unit in actual execution 133 | app()->bind($this->unit, function ($app, $args) { 134 | foreach ($this->constructorExpectations as $key => $expectations) { 135 | if ($args == $expectations) { 136 | return $this->mocks[$key]; 137 | } 138 | } 139 | 140 | throw new Mockery\Exception\NoMatchingExpectationException( 141 | "\n\nExpected one of the following arguments sets for {$this->unit}::__construct(): " . 142 | print_r($this->constructorExpectations, true) . "\nGot: " . 143 | print_r($args, true) 144 | ); 145 | }); 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Compare the mock to an actual instance. 152 | * 153 | * @param object $unit 154 | * @return void 155 | * @throws Mockery\Exception\NoMatchingExpectationException 156 | */ 157 | public function compareTo(object $unit) 158 | { 159 | $expected = array_map(fn($args) => new $unit(...$args), $this->constructorExpectations); 160 | 161 | $ref = new ReflectionClass($unit); 162 | foreach ($ref->getProperties() as $property) { 163 | 164 | $expectations = array_map(fn($instance) => $property->getValue($instance), $expected); 165 | 166 | if (!in_array($property->getValue($unit), $expectations)) { 167 | throw new Mockery\Exception\NoMatchingExpectationException( 168 | "Mismatch in \${$property->getName()} when running {$this->unit} \n\n--- Expected (one of)\n". 169 | print_r(join("\n", array_map(fn($instance) => $property->getValue($instance), $expected)), true). 170 | "\n\n+++Actual:\n".print_r($property->getValue($unit), true)."\n\n" 171 | ); 172 | } 173 | } 174 | } 175 | 176 | public function getMock(): MockInterface 177 | { 178 | $this->registerMock(); 179 | 180 | return $this->currentMock; 181 | } 182 | 183 | public function shouldBeDispatched() 184 | { 185 | $this->getMock()->shouldReceive('handle')->once(); 186 | } 187 | 188 | public function shouldNotBeDispatched() 189 | { 190 | if ($this->currentMock) { 191 | $this->getMock()->shouldNotReceive('handle'); 192 | } else { 193 | $mock = Mockery::mock($this->unit)->makePartial(); 194 | $mock->shouldNotReceive('handle'); 195 | app()->bind($this->unit, function () use ($mock) { 196 | return $mock; 197 | }); 198 | } 199 | } 200 | 201 | public function shouldReturn($value) 202 | { 203 | $this->getMock()->shouldReceive('handle')->once()->andReturn($value); 204 | } 205 | 206 | public function shouldReturnTrue() 207 | { 208 | $this->shouldReturn(true); 209 | } 210 | 211 | public function shouldReturnFalse() 212 | { 213 | $this->shouldReturn(false); 214 | } 215 | 216 | public function shouldThrow($exception, $message = '', $code = 0, Exception $previous = null) 217 | { 218 | $this->getMock()->shouldReceive('handle') 219 | ->once() 220 | ->andThrow($exception, $message, $code, $previous); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Testing/UnitMockRegistry.php: -------------------------------------------------------------------------------- 1 | instance(static::class, $this); 19 | } 20 | 21 | public function has(string $unit): bool 22 | { 23 | return isset($this->mocks[$unit]); 24 | } 25 | 26 | public function get(string $unit): ?UnitMock 27 | { 28 | if (!$this->has($unit)) return null; 29 | 30 | return $this->mocks[$unit]; 31 | } 32 | 33 | public function register(string $unit, UnitMock $mock) 34 | { 35 | $this->mocks[$unit] = $mock; 36 | } 37 | 38 | public function count() 39 | { 40 | return count($this->mocks); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Units/Controller.php: -------------------------------------------------------------------------------- 1 | getValidationFactory()->make($data, $rules, $messages, $customAttributes); 23 | } 24 | 25 | /** 26 | * Get a validation factory instance. 27 | * 28 | * @return \Illuminate\Validation\Factory 29 | */ 30 | public function getValidationFactory() 31 | { 32 | return app(\Illuminate\Contracts\Validation\Factory::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Validation/Validator.php: -------------------------------------------------------------------------------- 1 | validation = $validation; 23 | } 24 | 25 | /** 26 | * Validate the given input. 27 | * 28 | * @param array $input The input to validate 29 | * @param array $rules Specify custom rules (will override class rules) 30 | * @param array $messages Specify custom messages (will override class messages) 31 | * 32 | * @return bool 33 | * 34 | * @throws \Lucid\Exceptions\InvalidInputException 35 | */ 36 | public function validate(array $input, array $rules = [], array $messages = []) 37 | { 38 | $validation = $this->validation($input, $rules, $messages); 39 | 40 | if ($validation->fails()) { 41 | throw new InvalidInputException($validation); 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Get a validation instance out of the given input and optionatlly rules 49 | * by default the $rules property will be used. 50 | * 51 | * @param array $input 52 | * @param array $rules 53 | * @param array $messages 54 | * 55 | * @return \Illuminate\Validation\Validator 56 | */ 57 | public function validation(array $input, array $rules = [], array $messages = []) 58 | { 59 | if (empty($rules)) { 60 | $rules = $this->rules; 61 | } 62 | 63 | return $this->validation->make($input, $rules, $messages); 64 | } 65 | } 66 | --------------------------------------------------------------------------------