├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── conductor
├── conductor-logo.png
├── examples
└── todo
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ └── cli
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── composer.json
│ │ ├── phpunit.xml.dist
│ │ ├── src
│ │ ├── App.php
│ │ └── JsonFileTaskRepository.php
│ │ ├── tests
│ │ └── JsonFileTaskRepositoryTest.php
│ │ └── todo
│ ├── artifact
│ └── mybuilder_todo-package_0.1.zip
│ ├── composer.json
│ ├── conductor.yml
│ └── package
│ └── todo
│ ├── .gitignore
│ ├── README.md
│ ├── composer.json
│ ├── phpunit.xml.dist
│ ├── src
│ ├── Task.php
│ ├── TaskRepository.php
│ └── TaskService.php
│ └── tests
│ ├── TaskServiceTest.php
│ └── TaskTest.php
├── phpunit.xml.dist
├── src
├── Command
│ ├── BaseCommand.php
│ ├── LockFixerCommand.php
│ ├── SymlinkCommand.php
│ └── UpdateCommand.php
├── Conductor.php
├── Exception
│ └── ChecksumMismatchException.php
└── PackageZipper.php
└── tests
├── ConductorTest.php
├── PackageZipperTest.php
├── bootstrap.php
└── fixtures
├── packages
├── package-a-changed
│ └── composer.json
└── package-a
│ └── composer.json
└── symlink
├── package-a
└── content.txt
└── package-b
└── package-a
└── .gitkeep
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 | - hhvm
9 |
10 | before_script:
11 | - composer install
12 |
13 | script: vendor/bin/phpunit
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-2016 MyBuilder Limited
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://travis-ci.org/mybuilder/conductor)
2 |
3 | 
4 |
5 |
6 | **We no longer use Conductor at MyBuilder and instead now use the [Composer path](https://getcomposer.org/doc/05-repositories.md#path) but if you want to take over development of Conductor let us know.**
7 |
8 | ---
9 |
10 | This tool allows you to manage isolated, internal [Composer](https://getcomposer.org/) packages within a single, monolithic repository.
11 | Separating units of code based on directory structure, as opposed to at the repository level, maintains a single source of truth whilst providing the benefits of clearly defined component boundaries.
12 |
13 | When would you use it?
14 | ----------------------
15 |
16 | You would use this tool in a project setting where multiple separate applications co-exist (i.e. admin, frontend and mobile-api).
17 | Within this context each application will share code, such as business logic, to provide the end solution.
18 |
19 | An example project repository structure that we use in-kind is shown below:
20 |
21 | ```bash
22 | ├── app/
23 | │ ├── admin
24 | │ │ ├── src/
25 | │ │ ├── tests/
26 | │ │ └── composer.json
27 | │ ├── frontend
28 | │ │ ├── src/
29 | │ │ ├── tests/
30 | │ │ └── composer.json
31 | │ └── mobile-api
32 | │ ├── src/
33 | │ ├── tests/
34 | │ └── composer.json
35 | ├── artifact/
36 | ├── bin
37 | │ └── conductor
38 | ├── package
39 | │ ├── bar
40 | │ │ ├── src/
41 | │ │ ├── tests/
42 | │ │ └── composer.json
43 | │ └── foo
44 | │ ├── src/
45 | │ ├── tests/
46 | │ └── composer.json
47 | ├── composer.json
48 | └── conductor.yml
49 |
50 | ```
51 |
52 | As you can see the root-level composer.json file is only used for uniform tooling - so no project specific code should be stored at this level.
53 | The business logic is contained within each of the isolated packages, with the delivery supplied via the 'app' directory.
54 |
55 | Compatibility
56 | -------------
57 |
58 | - ✔ Mac OSX
59 | - ✔ Unix-derived systems (CentOS, Debian etc.)
60 | - ? Windows - Not tested at this time
61 |
62 | Examples
63 | --------
64 |
65 | At this time the project comes with a simple [todo example](examples/todo/) which illustrates how to use Conductor in it's entirety.
66 |
67 | Further Reading
68 | ---------------
69 |
70 | - [UK Symfony Meetup - Composer in monolithic repositories](http://www.meetup.com/symfony/events/192889222/)
71 |
72 | ---
73 |
74 | Created by [MyBuilder](http://www.mybuilder.com/) - Check out our [blog](http://tech.mybuilder.com/) for more insight into this and other open-source projects we release.
75 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mybuilder/conductor",
3 | "description": "Tools for managing multiple packages in one source repository",
4 | "license": "MIT",
5 | "authors": [{
6 | "name": "Sten Hiedel",
7 | "email": "r.sten@hiedel.ee"
8 | }],
9 | "require": {
10 | "symfony/console": "~2.6",
11 | "symfony/finder": "~2.6",
12 | "symfony/yaml": "~2.6",
13 | "symfony/filesystem": "~2.6"
14 | },
15 | "require-dev": {
16 | "phpunit/phpunit": "~4.5"
17 | },
18 | "autoload": {
19 | "psr-4": {
20 | "MyBuilder\\Conductor\\": "src/"
21 | }
22 | },
23 | "bin": ["conductor"],
24 | "extra": {
25 | "branch-alias": {
26 | "dev-master": "0.9-dev"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/conductor:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new Commands\UpdateCommand($conductor));
19 | $app->add(new Commands\SymlinkCommand($conductor));
20 | $app->add(new Commands\LockFixerCommand($conductor));
21 | $app->run();
22 |
--------------------------------------------------------------------------------
/conductor-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mybuilder/conductor/c9c7127dd0c251c3687a1dfcc137defaa0ce8b19/conductor-logo.png
--------------------------------------------------------------------------------
/examples/todo/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | bin/
--------------------------------------------------------------------------------
/examples/todo/README.md:
--------------------------------------------------------------------------------
1 | To-do Listing Example
2 | =====================
3 |
4 | Like many developers, I find that a good example is sometimes the best means of grasping a new concept or tool.
5 | This example highlights a solution to the contrived example of displaying task title listings on the CLI.
6 |
7 | Setup
8 | -----
9 |
10 | The initial step to solving this problem is to first gain access to the Conductor console application within our repository.
11 | Below is the sample 'composer.json' file used to define the root repositories tool dependencies.
12 |
13 | ```json
14 | {
15 | "name": "mybuilder/todo",
16 | "require": {
17 | "mybuilder/conductor": "*@dev"
18 | },
19 | "config": {
20 | "bin-dir": "bin"
21 | }
22 | }
23 | ```
24 |
25 | We are then required to specify within 'conductor.yml', the zipped archive and internal packages directories present.
26 | You will notice by looking at the following YAML definition that we have decided to place our packages into a single directory called 'package'.
27 | In the case where more fine-grained separation is required, simply adding further directories to this list will suffice.
28 |
29 | ```yaml
30 | artifacts_repository: ./artifact
31 | packages:
32 | - ./package/*
33 | ```
34 |
35 | With these two files now in-place we are able to 'composer install' and download Conductor, along with all it's required dependencies.
36 |
37 | The Package
38 | -----------
39 |
40 | The model in which we internally represent a task, along with the title listing projection service is core domain logic.
41 | It is also agnostic and does not concern itself with the delivery and persistent methods it could be used in.
42 | As a result of this, the task model, service and repository interface contract can all be packaged up in isolation without any external dependencies.
43 |
44 | ```bash
45 | package
46 | └── todo
47 | ├── src
48 | │ ├── Task.php
49 | │ ├── TaskRepository.php
50 | │ └── TaskService.php
51 | ├── tests
52 | │ ├── TaskServiceTest.php
53 | │ └── TaskTest.php
54 | ├── README.md
55 | ├── composer.json
56 | └── phpunit.xml.dist
57 | ```
58 |
59 | As you will notice above, the package's structure follows a similar pattern found in most typical composer dependent packages.
60 | As the package does not depend on anything from the outside world, no Conductor dependent additions are required to the 'composer.json' file.
61 |
62 | ```json
63 | {
64 | "name": "mybuilder/todo-package",
65 | "version": "0.1",
66 | "autoload": {
67 | "psr-4": {
68 | "MyBuilder\\Package\\ToDo\\": "src/"
69 | }
70 | },
71 | "require-dev": {
72 | "phpunit/phpunit": "~4.0"
73 | },
74 | "config": {
75 | "bin-dir": "bin"
76 | }
77 | }
78 | ```
79 |
80 | The Application
81 | ---------------
82 |
83 | With the core domain-logic now in place we are able to move on to implementing the CLI application.
84 | Following a similar pattern to how the package is structured we will isolate the application into a single directory - allowing us to easily add other delivery mechanisms (such as 'web') at a later date.
85 |
86 | ```bash
87 | app/
88 | └── cli
89 | ├── src
90 | │ ├── App.php
91 | │ └── JsonFileTaskRepository.php
92 | ├── tests
93 | │ └── JsonFileTaskRepositoryTest.php
94 | ├── README.md
95 | ├── composer.json
96 | ├── phpunit.xml.dist
97 | └── todo
98 | ```
99 |
100 | Looking at the file-based JSON repositories implementation you will notice that we depend upon several domain concepts.
101 | This is the use-case for the Conductor tool.
102 | Not only does it allow us to locally depend on other packages within the same repository, but also symbolically links the dependencies to the working directories current state.
103 |
104 | ```json
105 | {
106 | "name": "mybuilder/todo-cli",
107 | "version": "0.1",
108 | "autoload": {
109 | "psr-4": {
110 | "MyBuilder\\App\\ToDo\\Cli\\": "src/"
111 | }
112 | },
113 | "require": {
114 | "mybuilder/todo-package": "*@dev"
115 | },
116 | "require-dev": {
117 | "phpunit/phpunit": "~4.0"
118 | },
119 | "config": {
120 | "bin-dir": "bin"
121 | },
122 | "scripts": {
123 | "pre-install-cmd": [
124 | "../../bin/conductor update -c ../.."
125 | ],
126 | "pre-update-cmd": [
127 | "../../bin/conductor update -c ../.."
128 | ],
129 | "post-update-cmd": [
130 | "../../bin/conductor fix-composer-lock -c ../.."
131 | ],
132 | "pre-autoload-dump": [
133 | "../../bin/conductor symlink -c ../.."
134 | ]
135 | },
136 | "repositories": [
137 | {
138 | "type": "artifact",
139 | "url": "../../artifact/"
140 | }
141 | ]
142 | }
143 | ```
144 |
145 | As you can see from the above composer definition, there is a little boilerplate that is required to correctly configure and invoke Conductor, highlighted within 'scripts'.
146 | When an install or update composer action is invoked, all packages found within the defined directories are zipped and archived.
147 | In the case of a successful update, we also address an issue in-regard to how Composer always stores absolute paths in lock files, even though relative paths have been supplied.
148 | So as to allow for the lock files to be committed and developers freely specify where they wish to place the repository, we replace these absolute paths with their relative counterparts.
149 | Finally, before dumping the autoloader we symbolically link the 'vendor' directories internal dependences used within each package to the current working directory version.
150 | This is required as internally the archived directories do not contain any source code, only the directory paths to symbolically link.
151 |
152 | With the command line application now having access to the 'todo-package', we are able to bootstrap the application and solve the problem laid out in its entirety.
153 |
154 | ```php
155 | service = new TaskService(
169 | new JsonFileTaskRepository($this->filePath = $filePath)
170 | );
171 | }
172 |
173 | public function run()
174 | {
175 | $this->fillWithDummyTasks();
176 |
177 | foreach ($this->service->fetchAllTitles() as $title) {
178 | echo "* $title\n";
179 | }
180 |
181 | $this->removeAllTasks();
182 | }
183 |
184 | private function fillWithDummyTasks()
185 | {
186 | foreach (range(1, 3) as $n) {
187 | $this->service->addTask("Task $n", "Task $n Description");
188 | }
189 | }
190 |
191 | private function removeAllTasks()
192 | {
193 | unlink($this->filePath);
194 | }
195 | }
196 | ```
197 |
198 | You will notice above that we provide a dummy set of tasks for the service to consume.
199 | This works in this use-case as we have not been tasked with adding the ability to persist user inputted items at this time.
200 | Finally, you are able to 'composer install' this package, bringing in the symbolically linked internal dependencies, and execute the solution.
201 |
202 | ```php
203 | #!/usr/bin/env php
204 | run();
210 | ```
211 |
212 | ```bash
213 | $ ./todo
214 | * Title 1
215 | * Title 2
216 | * Title 3
217 | ```
218 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | bin/
--------------------------------------------------------------------------------
/examples/todo/app/cli/README.md:
--------------------------------------------------------------------------------
1 | To-do Cli App
2 | =============
3 |
4 | This application allows you to access the 'mybuilder/todo-package' title listing
5 | service from the CLI.
6 | Tasks are persisted using a file-based JSON representation, and the application
7 | prefills this store with dummy data so as to provide the example with meaning.
8 |
9 | Demo
10 | ----
11 |
12 | To run the example, the following commands must be executed from within this
13 | directory:
14 |
15 | ``` bash
16 | $ composer install
17 | $ ./todo
18 | * Task 1
19 | * Task 2
20 | * Task 3
21 | ```
22 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mybuilder/todo-cli",
3 | "version": "0.1",
4 | "autoload": {
5 | "psr-4": {
6 | "MyBuilder\\App\\ToDo\\Cli\\": "src/"
7 | }
8 | },
9 | "require": {
10 | "mybuilder/todo-package": "*@dev"
11 | },
12 | "require-dev": {
13 | "phpunit/phpunit": "~4.0"
14 | },
15 | "config": {
16 | "bin-dir": "bin"
17 | },
18 | "scripts": {
19 | "pre-install-cmd": [
20 | "../../bin/conductor update -c ../.."
21 | ],
22 | "pre-update-cmd": [
23 | "../../bin/conductor update -c ../.."
24 | ],
25 | "post-update-cmd": [
26 | "../../bin/conductor fix-composer-lock -c ../.."
27 | ],
28 | "pre-autoload-dump": [
29 | "../../bin/conductor symlink -c ../.."
30 | ]
31 | },
32 | "repositories": [
33 | {
34 | "type": "artifact",
35 | "url": "../../artifact/"
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | ./tests/
17 |
18 |
19 |
20 |
21 |
22 | ./src
23 |
24 | ./tests
25 | ./vendor
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/src/App.php:
--------------------------------------------------------------------------------
1 | service = new TaskService(
15 | new JsonFileTaskRepository($this->filePath = $filePath)
16 | );
17 | }
18 |
19 | public function run()
20 | {
21 | $this->fillWithDummyTasks();
22 |
23 | foreach ($this->service->fetchAllTitles() as $title) {
24 | echo "* $title\n";
25 | }
26 |
27 | $this->removeAllTasks();
28 | }
29 |
30 | private function fillWithDummyTasks()
31 | {
32 | foreach (range(1, 3) as $n) {
33 | $this->service->addTask("Task $n", "Task $n Description");
34 | }
35 | }
36 |
37 | private function removeAllTasks()
38 | {
39 | unlink($this->filePath);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/src/JsonFileTaskRepository.php:
--------------------------------------------------------------------------------
1 | filePath = $filePath;
15 | }
16 |
17 | public function store(Task $task)
18 | {
19 | $tasks = $this->fetchTaskJson();
20 |
21 | $tasks[] = array(
22 | 'title' => $task->getTitle(),
23 | 'description' => $task->getDescription(),
24 | );
25 |
26 | file_put_contents($this->filePath, json_encode($tasks));
27 | }
28 |
29 | public function fetchAll()
30 | {
31 | return array_map(function ($task) {
32 | return new Task($task['title'], $task['description']);
33 | }, $this->fetchTaskJson());
34 | }
35 |
36 | private function fetchTaskJson()
37 | {
38 | if (file_exists($this->filePath)) {
39 | return json_decode(file_get_contents($this->filePath), true);
40 | }
41 |
42 | return array();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/tests/JsonFileTaskRepositoryTest.php:
--------------------------------------------------------------------------------
1 | filePath = __DIR__ . "/" . md5(time()) . ".json";
15 | $this->tasks = new JsonFileTaskRepository($this->filePath);
16 | }
17 |
18 | public function tearDown()
19 | {
20 | unlink($this->filePath);
21 | }
22 |
23 | /**
24 | * @test
25 | */
26 | public function shouldStoreTask()
27 | {
28 | $this->tasks->store(new Task('Sample Title', 'Sample Description'));
29 |
30 | $this->assertContains('Sample Title', file_get_contents($this->filePath));
31 | }
32 |
33 | /**
34 | * @test
35 | */
36 | public function shouldFetchAllTasks()
37 | {
38 | $this->storeTasks($tasks = array(
39 | new Task('Title 1', 'Description 1'),
40 | new Task('Title 2', 'Description 2'),
41 | ));
42 |
43 | $this->assertEquals($tasks, $this->tasks->fetchAll());
44 | }
45 |
46 | private function storeTasks(array $tasks)
47 | {
48 | foreach ($tasks as $task) {
49 | $this->tasks->store($task);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/examples/todo/app/cli/todo:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
8 |
--------------------------------------------------------------------------------
/examples/todo/artifact/mybuilder_todo-package_0.1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mybuilder/conductor/c9c7127dd0c251c3687a1dfcc137defaa0ce8b19/examples/todo/artifact/mybuilder_todo-package_0.1.zip
--------------------------------------------------------------------------------
/examples/todo/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mybuilder/todo",
3 | "require": {
4 | "mybuilder/conductor": "*@dev"
5 | },
6 | "config": {
7 | "bin-dir": "bin"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/todo/conductor.yml:
--------------------------------------------------------------------------------
1 | artifacts_repository: ./artifact
2 | packages:
3 | - ./package/*
4 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | bin/
--------------------------------------------------------------------------------
/examples/todo/package/todo/README.md:
--------------------------------------------------------------------------------
1 | To-do Package
2 | =============
3 |
4 | Contains the core domain logic required to represent and list tasks stored
5 | within an implemented repository.
6 |
7 | Service
8 | -------
9 |
10 | The supplied service provides functionality to add new tasks, along with a
11 | projection of all present task titles.
12 |
13 | Repository
14 | ----------
15 |
16 | This package is not concerned with how the collection of tasks is stored within
17 | the particular use-case.
18 | All that is required is that their be an implementation abiding by the
19 | 'TaskRepository' interface when instantiating the service.
20 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mybuilder/todo-package",
3 | "version": "0.1",
4 | "autoload": {
5 | "psr-4": {
6 | "MyBuilder\\Package\\ToDo\\": "src/"
7 | }
8 | },
9 | "require-dev": {
10 | "phpunit/phpunit": "~4.0"
11 | },
12 | "config": {
13 | "bin-dir": "bin"
14 | }
15 | }
--------------------------------------------------------------------------------
/examples/todo/package/todo/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | ./tests/
17 |
18 |
19 |
20 |
21 |
22 | ./src
23 |
24 | ./tests
25 | ./vendor
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/src/Task.php:
--------------------------------------------------------------------------------
1 | setTitle($title);
13 | $this->setDescription($description);
14 | }
15 |
16 | private function setTitle($title)
17 | {
18 | if ($title === '') {
19 | throw new \RuntimeException('Task title must be supplied.');
20 | }
21 |
22 | $this->title = $title;
23 | }
24 |
25 | private function setDescription($description)
26 | {
27 | if (strlen($description) < 5) {
28 | throw new \RuntimeException('Description must be greater than 4 chars long.');
29 | }
30 |
31 | $this->description = $description;
32 | }
33 |
34 | public function getTitle()
35 | {
36 | return $this->title;
37 | }
38 |
39 | public function getDescription()
40 | {
41 | return $this->description;
42 | }
43 |
44 | public function equals(Task $that)
45 | {
46 | return
47 | $this->getTitle() === $that->getTitle() &&
48 | $this->getDescription() === $that->getDescription();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/src/TaskRepository.php:
--------------------------------------------------------------------------------
1 | tasks = $tasks;
12 | }
13 |
14 | public function fetchAllTitles()
15 | {
16 | return array_map(function (Task $task) {
17 | return $task->getTitle();
18 | }, $this->tasks->fetchAll());
19 | }
20 |
21 | public function addTask($title, $description)
22 | {
23 | $this->tasks->store(new Task($title, $description));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/tests/TaskServiceTest.php:
--------------------------------------------------------------------------------
1 | service = new TaskService(
13 | $this->repository = new InMemoryTaskRepository
14 | );
15 | }
16 |
17 | /**
18 | * @test
19 | */
20 | public function shouldReturnTaskTitles()
21 | {
22 | $titles = $this->service->fetchAllTitles();
23 |
24 | $this->assertEquals(array('Task 1', 'Task 2', 'Task 3'), $titles);
25 | }
26 |
27 | /**
28 | * @test
29 | */
30 | public function shouldAddNewTask()
31 | {
32 | $this->service->addTask('Task 4', 'Task 4 Description');
33 |
34 | $this->assertContainsTask(
35 | new Task('Task 4', 'Task 4 Description'),
36 | $this->repository->fetchAll()
37 | );
38 | }
39 |
40 | private function assertContainsTask(Task $task, array $tasks)
41 | {
42 | foreach ($tasks as $t) {
43 | if ($t->equals($task)) {
44 | return true;
45 | }
46 | }
47 |
48 | $this->fail('Collection does not contain task');
49 | }
50 | }
51 |
52 | class InMemoryTaskRepository implements TaskRepository
53 | {
54 | private $tasks;
55 |
56 | public function __construct()
57 | {
58 | $this->tasks = array(
59 | new Task('Task 1', 'Task 1 Description'),
60 | new Task('Task 2', 'Task 2 Description'),
61 | new Task('Task 3', 'Task 3 Description'),
62 | );
63 | }
64 |
65 | public function store(Task $task)
66 | {
67 | $this->tasks[] = $task;
68 | }
69 |
70 | public function fetchAll()
71 | {
72 | return $this->tasks;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/examples/todo/package/todo/tests/TaskTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Sample Title', $task->getTitle());
15 | $this->assertEquals('Sample Description', $task->getDescription());
16 | }
17 |
18 | /**
19 | * @test
20 | * @expectedException \RuntimeException
21 | */
22 | public function shouldThrowForInvalidTitle()
23 | {
24 | new Task('', 'Sample Description');
25 | }
26 |
27 | /**
28 | * @test
29 | * @expectedException \RuntimeException
30 | */
31 | public function shouldThrowForInvalidDescription()
32 | {
33 | new Task('Sample Title', 'Foo');
34 | }
35 |
36 | /**
37 | * @test
38 | */
39 | public function shouldBeEqual()
40 | {
41 | $a = new Task('Task 1', 'Task 1 Description');
42 | $b = new Task('Task 1', 'Task 1 Description');
43 |
44 | $this->assertTrue($a->equals($b));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | ./tests
17 |
18 |
19 |
20 |
21 |
22 | ./src
23 |
24 | ./tests
25 | ./vendor
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Command/BaseCommand.php:
--------------------------------------------------------------------------------
1 | conductor = $conductor;
28 | }
29 |
30 | protected function configure()
31 | {
32 | $this->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Configuration file', '');
33 | }
34 |
35 | protected function initialize(InputInterface $input, OutputInterface $output)
36 | {
37 | $this->configurationFile = $this->locateConfigurationFile($input->getOption('config'));
38 | $this->changeWorkingDir(
39 | $this->workingDir = dirname($this->configurationFile)
40 | );
41 |
42 | if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
43 | $output->writeln('Using configuration: ' . $this->configurationFile);
44 | $output->writeln('Changed working dir: ' . $this->workingDir);
45 | }
46 | }
47 |
48 | /**
49 | * @param string $directory
50 | *
51 | * @return string
52 | *
53 | * @throws \RuntimeException When conductor.yml is not found
54 | */
55 | private function locateConfigurationFile($directory)
56 | {
57 | $directory = realpath($directory);
58 |
59 | if (false !== $directory) {
60 | if (file_exists($directory . '/conductor.yml')) {
61 | return $directory . '/conductor.yml';
62 | }
63 | }
64 |
65 | throw new \RuntimeException('Cannot find the conductor.yml configuration file');
66 | }
67 |
68 | /**
69 | * @return mixed
70 | */
71 | protected function getConfiguration()
72 | {
73 | $yaml = new Parser();
74 |
75 | return $yaml->parse(file_get_contents($this->configurationFile));
76 | }
77 |
78 | /**
79 | * @param string $workingDir
80 | *
81 | * @return string
82 | */
83 | protected function changeWorkingDir($workingDir)
84 | {
85 | chdir($workingDir);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Command/LockFixerCommand.php:
--------------------------------------------------------------------------------
1 | setName("fix-composer-lock")
18 | ->setDescription('Fixes composer lock real-paths with relative-paths');
19 | }
20 |
21 | protected function execute(InputInterface $input, OutputInterface $output)
22 | {
23 | $fileSystem = new Filesystem();
24 | $configuration = $this->getConfiguration();
25 | $conductorRealPaths = $this->getConductorRealPaths($configuration['artifacts_repository']);
26 |
27 | foreach ($this->createFinder()->in(getcwd()) as $lockFile) {
28 | $relativePaths = $this->getRelativePaths($fileSystem, $lockFile, $conductorRealPaths);
29 |
30 | if ($this->fixLockFileRealPathsWithRelativePaths($fileSystem, $lockFile, $conductorRealPaths, $relativePaths)) {
31 | $output->writeln('Fixed lock file "' . $lockFile . '"');
32 | }
33 | }
34 | }
35 |
36 | /**
37 | * @return Finder
38 | */
39 | private function createFinder()
40 | {
41 | return Finder::create()
42 | ->files()
43 | ->exclude('vendor')
44 | ->name('composer.lock');
45 | }
46 |
47 | private function getConductorRealPaths($zipsDir)
48 | {
49 | $finder = Finder::create()->files()->name('*.zip')->in($zipsDir);
50 |
51 | return array_map(function (\SplFileInfo $file) {
52 | return $file->getRealPath();
53 | }, iterator_to_array($finder));
54 | }
55 |
56 | private function getRelativePaths(Filesystem $fileSystem, \SplFileInfo $lockFile, array $conductorRealPaths)
57 | {
58 | return array_map(function ($conductorPath) use ($fileSystem, $lockFile) {
59 | return $fileSystem->makePathRelative(dirname($conductorPath), dirname($lockFile->getRealPath())) . basename($conductorPath);
60 | }, $conductorRealPaths);
61 | }
62 |
63 | private function fixLockFileRealPathsWithRelativePaths(Filesystem $fileSystem, $lockFile, $conductorRealPaths, $relativePaths)
64 | {
65 | $initialContent = file_get_contents($lockFile);
66 | $fixedContent = str_replace($conductorRealPaths, $relativePaths, $initialContent);
67 |
68 | if ($initialContent === $fixedContent) {
69 | return false;
70 | }
71 | $fileSystem->dumpFile($lockFile, $fixedContent);
72 |
73 | return true;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Command/SymlinkCommand.php:
--------------------------------------------------------------------------------
1 | setName("symlink")
16 | ->setDescription('Symlinks internal packages to vendors');
17 | }
18 |
19 | protected function execute(InputInterface $input, OutputInterface $output)
20 | {
21 | $output->writeln('Symlink packages');
22 |
23 | $this->conductor->symlinkPackages(getcwd());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Command/UpdateCommand.php:
--------------------------------------------------------------------------------
1 | setName("update")
18 | ->setDescription('Updates the artifacts within the artifacts repository');
19 | }
20 |
21 | protected function execute(InputInterface $input, OutputInterface $output)
22 | {
23 | $configuration = $this->getConfiguration();
24 |
25 | if (false === isset($configuration['artifacts_repository'])) {
26 | throw new \RuntimeException('Missing "conductor.artifacts_repository" configuration');
27 | }
28 |
29 | if (false === isset($configuration['packages'])) {
30 | throw new \RuntimeException('Missing "conductor.packages" configuration');
31 | }
32 |
33 | $output->writeln('Zipping packages');
34 |
35 | $this->ensureRepositoryDirExists($configuration['artifacts_repository']);
36 |
37 | $this->conductor->updatePackages($configuration['packages'], new PackageZipper($configuration['artifacts_repository']));
38 | }
39 |
40 | private function ensureRepositoryDirExists($path)
41 | {
42 | $fileSystem = new Filesystem();
43 | if (false === $fileSystem->exists($path)) {
44 | $fileSystem->mkdir($path);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Conductor.php:
--------------------------------------------------------------------------------
1 | fileSystem = $fileSystem;
18 | }
19 |
20 | public function updatePackages($paths, PackageZipper $packageZipper)
21 | {
22 | $finder = new Finder();
23 | $finder->files()->exclude('vendor')->name('composer.json')->depth(0);
24 |
25 | $results = array();
26 | foreach ($finder->in($paths) as $file) {
27 | $results[] = $packageZipper->zip($file);
28 | }
29 |
30 | return $results;
31 | }
32 |
33 | public function symlinkPackages($rootPath)
34 | {
35 | $finder = new Finder();
36 | $finder->files()->name('replace_with_symlink.path');
37 |
38 | foreach ($finder->in($rootPath) as $file) {
39 | $this->symlinkPackageToVendor(file_get_contents($file), dirname($file));
40 | }
41 | }
42 |
43 | private function symlinkPackageToVendor($packagePath, $vendorPath)
44 | {
45 | $relative = $this->fileSystem->makePathRelative(realpath($packagePath), realpath($vendorPath . '/../'));
46 |
47 | $this->fileSystem->rename($vendorPath, $vendorPath . '_linked', true);
48 | $this->fileSystem->symlink($relative, $vendorPath);
49 | $this->fileSystem->remove($vendorPath . '_linked');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Exception/ChecksumMismatchException.php:
--------------------------------------------------------------------------------
1 | zipsPath = $zipsPath;
12 | }
13 |
14 | /**
15 | * @param \SplFileInfo $composerFile the package composer.json file path
16 | *
17 | * @return string The package conductor file path
18 | */
19 | public function zip(\SplFileInfo $composerFile)
20 | {
21 | $json = json_decode(file_get_contents($composerFile), true);
22 |
23 | if (false === isset($json['version'])) {
24 | throw new \RuntimeException('Package "' . $json['name'] . '" has no version defined in file: ' . $composerFile->getPathname() . '.');
25 | }
26 |
27 | $packageZipPath = $this->getZipPath($json['name'], $json['version']);
28 |
29 | if (false === file_exists($packageZipPath)) {
30 | $this->createZip($composerFile, $packageZipPath);
31 | }
32 |
33 | $this->verifyZip($composerFile, $packageZipPath, $json);
34 |
35 | return $packageZipPath;
36 | }
37 |
38 | private function getZipPath($packageName, $version)
39 | {
40 | return $this->zipsPath . DIRECTORY_SEPARATOR . str_replace("/", '_', $packageName) . '_' . $version . '.zip';
41 | }
42 |
43 | private function createZip($composerFile, $zipPath)
44 | {
45 | $zip = new \ZipArchive();
46 | $zip->open($zipPath, \ZipArchive::CREATE);
47 | $zip->addFile($composerFile, 'Package/composer.json');
48 | $zip->addFromString('Package/replace_with_symlink.path', dirname($composerFile));
49 | $zip->close();
50 | }
51 |
52 | private function verifyZip($composerFile, $zipPath, $info)
53 | {
54 | $zip = new \ZipArchive();
55 | $zip->open($zipPath);
56 | $content = $zip->getFromName('Package/composer.json');
57 | $zip->close();
58 |
59 | if (sha1_file($composerFile) !== sha1($content)) {
60 | throw new Exception\ChecksumMismatchException('
61 | Package ' . $info['name'] . '@' . $info['version'] . '
62 | is already zipped with the given version but
63 | the zip composer.json checksum does not match the package/composer.json checksum
64 | maybe you forgot to increment the package/composer.json version?');
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ConductorTest.php:
--------------------------------------------------------------------------------
1 | fs = new Filesystem();
21 | $this->conductor = new Conductor($this->fs);
22 | }
23 |
24 | public function test_it_should_update_all_packages()
25 | {
26 | $zipperMock = $this->getMock('MyBuilder\Conductor\PackageZipper', array(), array(array()));
27 | $zipperMock
28 | ->expects($this->any())
29 | ->method('zip')
30 | ->will($this->returnCallback(function($a) {
31 | return $a;
32 | }));
33 |
34 | $files = $this->conductor->updatePackages(
35 | array(__DIR__ . '/fixtures/packages/*'),
36 | $zipperMock);
37 |
38 | $this->assertEquals(
39 | array(
40 | __DIR__ . '/fixtures/packages/package-a/composer.json',
41 | __DIR__ . '/fixtures/packages/package-a-changed/composer.json',
42 | ),
43 | array_map(null, $files));
44 | }
45 |
46 | public function test_it_should_symlink_packages()
47 | {
48 | $tempDir = $this->createTempDir();
49 | $this->fs->mirror(__DIR__ . '/fixtures/symlink', $tempDir);
50 | $this->fs->dumpFile($tempDir . '/package-b/package-a/replace_with_symlink.path', $tempDir . '/package-a/');
51 |
52 | $this->conductor->symlinkPackages($tempDir);
53 |
54 | $link = $tempDir . '/package-b/package-a';
55 | $this->assertTrue(is_link($link), $link . ' should be a symlink');
56 | $this->assertEquals('../package-a/', readlink($link), 'It should have a relative symlink');
57 | }
58 |
59 | public function test_it_should_fix_composer_lock_absolute_paths()
60 | {
61 | $this->markTestIncomplete();
62 |
63 | // real paths to the conductor zips
64 | // will be made relative from the point of view of the composer.lock file
65 | }
66 |
67 | private function createTempDir()
68 | {
69 | $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid();
70 | $this->fs->mkdir($tempDir, 0777);
71 |
72 | return $tempDir;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/PackageZipperTest.php:
--------------------------------------------------------------------------------
1 | packageZipper = new PackageZipper($tempZipsDir);
20 | }
21 |
22 | public function test_it_should_zip_the_package()
23 | {
24 | $file = __DIR__ . '/fixtures/packages/package-a/composer.json';
25 |
26 | $zipPath = $this->packageZipper->zip(new \SplFileInfo($file));
27 |
28 | $this->assertZipContains(array(
29 | 'Package/composer.json' => file_get_contents($file),
30 | 'Package/replace_with_symlink.path' => __DIR__ . '/fixtures/packages/package-a',
31 | ), $zipPath);
32 | }
33 |
34 | public function test_it_should_throw_an_mismatch_exception_when_zip_and_package_checksum_does_not_match()
35 | {
36 | $zipPath = $this->packageZipper->zip(new \SplFileInfo(
37 | __DIR__ . '/fixtures/packages/package-a/composer.json'));
38 |
39 | $this->setExpectedException('MyBuilder\Conductor\Exception\ChecksumMismatchException');
40 |
41 | $zipPath = $this->packageZipper->zip(new \SplFileInfo(
42 | __DIR__ . '/fixtures/packages/package-a-changed/composer.json'));
43 | }
44 |
45 | private function assertZipContains(array $contents, $file)
46 | {
47 | $zip = new \ZipArchive();
48 | $zip->open($file);
49 |
50 | $actual = array();
51 | for ($i = 0; $i < $zip->numFiles; $i++) {
52 | $actual[$zip->getNameIndex($i)] = $zip->getFromIndex($i);
53 | }
54 |
55 | $zip->close();
56 |
57 | $this->assertEquals($contents, $actual);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |