├── .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 |
5 |
6 |
7 |
8 |
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 | 
105 |
106 | ## The Stack
107 |
108 | At a glance...
109 |
110 | 
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 | 
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 |
--------------------------------------------------------------------------------