Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ As you can see, we are installing two packages here.
- **mouf/utils.patcher** contains the patch service. The patch service can be used to install any kinds of patches, but does not contain any patches implementation. This is why we need the second packages.
- **mouf/database.patcher** adds an easy way to create database patches (the most common use of the patch system).

Using the patch service
-----------------------
Using the patch service (graphical user interface)
--------------------------------------------------

Once the patch service is installed, you will notice there is a new menu in Mouf UI.

Expand All @@ -55,6 +55,28 @@ If you need a more *fine-tuned* approach, you can **apply** each patch one by on
**skip** the patch if you prefer to run it yourself or if you know it has already been applied.
Finally, you will notice that some patches can be **reverted**.

Using the patch service (command line interface)
------------------------------------------------

You can also apply patches using the Mouf console. This is especially useful for deployments in continuous integration environments or on a production server.

```sh
$ # Apply all default patches
$ vendor/bin/mouf_console patches:apply-all
$
$ # Apply all patches from type default AND test_data
$ vendor/bin/mouf_console patches:apply-all --test-data
$
$ # View a list of all patches
$ vendor/bin/mouf_console patches:list
$
$ # Apply one specific patch by name
$ vendor/bin/mouf_console patches:apply [patch_name]
$
$ # Revert one specific patch by name
$ vendor/bin/mouf_console patches:revert [patch_name]
```


Creating/Editing a database patch
---------------------------------
Expand All @@ -74,13 +96,23 @@ In this case, you should **skip** the patch (there is no point in applying this
- If you haven't applied the patch yet, you can choose to save and **apply** the patch.
- Finally, you can also choose to save, but **do not apply** the patch yet. In this case, the patch will be in **Awating** state.

###Advanced options
### Advanced options

There are a number of advanced options. These will allow you to:

- Choose the file saving the patch (the SQL of the patch is stored in its own file, usually in the **database/up** directory.
- Set up a *reverse patch* that can be used to cancel/revert your patch.

### Patch type

When creating a database patch, you can select a patch "type".

By default, the package comes with 2 bundled types:

- *default*: for patches that should always be applied (like patches modifying the database model)
- *test_data*: for patches that should be applied conditionnally based on the environment (you might want test data in your development environment but not in production)

You can also edit thos patch types or add your own patch types by editing the `patchService` instance in Mouf.

[You are a package developer? You want your own package to create/modify tables? See how you can use the patch system for that.](doc/for_packages_developer.md)
[Want to learn more about the patch system? Want to learn how to create you own non db-related patches? Have a look at the advanced documentation.](doc/advanced.md)
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}
],
"require": {
"php": ">=5.5.0",
"php": ">=7.0",
"mouf/mouf-validators-interface": "~2.0",
"mouf/utils.console": "~1.0"
},
Expand All @@ -26,7 +26,7 @@
"mouf": {
"install" : [{
"type" : "class",
"class" : "Mouf\\Utils\\Patcher\\PatchInstaller",
"class" : "Mouf\\Utils\\Patcher\\PatchInstaller2",
"description": "Create the patchService instance."
}
],
Expand Down
12 changes: 9 additions & 3 deletions src/Mouf/Utils/Patcher/Commands/ApplyAllPatchesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class ApplyAllPatchesCommand extends Command

public function __construct(PatchService $patchService)
{
parent::__construct();
$this->patchService = $patchService;
parent::__construct();
}


Expand All @@ -34,16 +34,22 @@ protected function configure()
{
$this
->setName('patches:apply-all')
->setDescription('Apply all pending patches.')
->setDescription('Apply pending patches.')
->setDefinition(array(

))
->setHelp(<<<EOT
Apply all pending patches.
Apply pending patches. You can select the type of patches to be applied using the options. Default patches are always applied.

Use patches:apply if you want to cherry-pick a particular patch.
EOT
);

foreach ($this->patchService->getTypes() as $type) {
if ($type->getName() !== '') {
$this->addOption($type->getName(), null, InputOption::VALUE_NONE, 'Applies patches of type "'.$type->getName().'". '.$type->getDescription());
}
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Mouf/Utils/Patcher/Commands/ListPatchesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ protected function execute(InputInterface $input, OutputInterface $output)
$patches = $this->patchService->getView();

$rows = array_map(function($row) {
return [ $row['uniqueName'], $this->renderStatus($row['status']) ];
return [ $row['uniqueName'], $this->renderStatus($row['status']), $row['patch_type'] ?: '(default)' ];
}, $patches);

$table = new Table($output);
$table
->setHeaders(array('Patch', 'Status'))
->setHeaders(array('Patch', 'Status', 'Type'))
->setRows($rows)
;
$table->render();
Expand Down
133 changes: 116 additions & 17 deletions src/Mouf/Utils/Patcher/Controllers/PatchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;

use Mouf\Html\Widgets\MessageService\Service\UserMessageInterface;
use Mouf\MoufManager;

use Mouf\Mvc\Splash\Controllers\Controller;
Expand Down Expand Up @@ -36,6 +37,8 @@ class PatchController extends AbstractMoufInstanceController {

protected $nbAwaiting = 0;
protected $nbError = 0;

protected $nbPatchesByType = [];

/**
* Page listing the patches to be applied.
Expand All @@ -57,7 +60,7 @@ public function defaultAction($name, $selfedit="false") {
}
}

$this->content->addFile(dirname(__FILE__)."/../../../../views/patchesList.php", $this);
$this->content->addFile(__DIR__."/../../../../views/patchesList.php", $this);
$this->template->toHtml();
}

Expand Down Expand Up @@ -93,30 +96,126 @@ public function runPatch($name, $uniqueName, $action, $selfedit) {

header('Location: .?name='.urlencode($name));
}

/**
* Runs all patches in a row.
*
* @Action
* @Logged
* @param string $name
* @param string $selfedit
*/
public function runAllPatches($name, $selfedit) {

/**
* Displays the page to select the patch types to be applied.
*
* @Action
* @Logged
* @param string $name
* @param string $selfedit
*/
public function runAllPatches($name, $selfedit) {
$this->initController($name, $selfedit);

$patchService = new InstanceProxy($name, $selfedit == "true");
$this->patchesArray = $patchService->getView();

$types = $patchService->_getSerializedTypes();

foreach ($types as $type) {
$this->nbPatchesByType[$type['name']] = 0;
}

$nbNoneDefaultPatches = 0;

foreach ($this->patchesArray as $patch) {
if ($patch['status'] == PatchInterface::STATUS_AWAITING || $patch['status'] == PatchInterface::STATUS_ERROR) {
$type = $patch['patch_type'];
if ($type !== '') {
$nbNoneDefaultPatches++;
}
$this->nbPatchesByType[$type]++;
}
}

// If all patches to be applied are default patches, let's do this right now.
if ($nbNoneDefaultPatches === 0) {
$this->applyAllPatches($name, [''], $selfedit);
return;
}

ksort($this->nbPatchesByType);

// Otherwise, let's display a screen to select the patch types to be applied.
$this->content->addFile(__DIR__."/../../../../views/applyPatches.php", $this);
$this->template->toHtml();
}


/**
* Runs all patches in a row.
*
* @Action
* @Logged
* @param string $name
* @param array $types
* @param string $selfedit
*/
public function applyAllPatches($name, array $types, $selfedit) {
$patchService = new InstanceProxy($name, $selfedit == "true");
$this->patchesArray = $patchService->getView();


// Array of cound of applied and skip patched. Key is the patch type.
$appliedPatchArray = [];
$skippedPatchArray = [];

try {
foreach ($this->patchesArray as $patch) {
if ($patch['status'] == PatchInterface::STATUS_AWAITING || $patch['status'] == PatchInterface::STATUS_ERROR) {
$patchService->apply($patch['uniqueName']);
}
if ($patch['status'] == PatchInterface::STATUS_AWAITING || $patch['status'] == PatchInterface::STATUS_ERROR) {
$type = $patch['patch_type'];
if (in_array($type, $types) || $type === '') {
$patchService->apply($patch['uniqueName']);
if (!isset($appliedPatchArray[$type])) {
$appliedPatchArray[$type] = 0;
}
$appliedPatchArray[$type]++;
} else {
$patchService->skip($patch['uniqueName']);
if (!isset($skippedPatchArray[$type])) {
$skippedPatchArray[$type] = 0;
}
$skippedPatchArray[$type]++;
}
}
}

} catch (\Exception $e) {
$htmlMessage = "An error occured while applying the patch: ".$e->getMessage();
set_user_message($htmlMessage);
}

header('Location: .?name='.urlencode($name));

$this->displayNotificationMessage($appliedPatchArray, $skippedPatchArray);

header('Location: .?name='.urlencode($name));
}

private function displayNotificationMessage(array $appliedPatchArray, array $skippedPatchArray)
{
$nbPatchesApplied = array_sum($appliedPatchArray);
$nbPatchesSkipped = array_sum($skippedPatchArray);
$msg = '';
if ($nbPatchesApplied !== 0) {
$patchArr = [];
foreach ($appliedPatchArray as $name => $number) {
$name = $name ?: 'default';
$patchArr[] = plainstring_to_htmlprotected($name).': '.$number;
}

$msg .= sprintf('%d patch(es) applied (%s)', $nbPatchesApplied, implode(', ', $patchArr));
}
if ($nbPatchesSkipped !== 0) {
$patchArr = [];
foreach ($skippedPatchArray as $name => $number) {
$name = $name ?: 'default';
$patchArr[] = plainstring_to_htmlprotected($name).': '.$number;
}

$msg .= sprintf('%d patch(es) skipped (%s)', $nbPatchesSkipped, implode(', ', $patchArr));
}

if ($msg !== '') {
set_user_message($msg, UserMessageInterface::SUCCESS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use Mouf\Actions\InstallUtils;
use Mouf\Console\ConsoleUtils;
use Mouf\Database\Patcher\PatchConnection;
use Mouf\Installer\PackageInstallerInterface;
use Mouf\MoufManager;
use Mouf\Utils\Patcher\Commands\ApplyAllPatchesCommand;
Expand All @@ -16,7 +17,7 @@
use Mouf\Utils\Patcher\Commands\RevertPatchCommand;
use Mouf\Utils\Patcher\Commands\SkipPatchCommand;

class PatchInstaller implements PackageInstallerInterface
class PatchInstaller2 implements PackageInstallerInterface
{
/**
* (non-PHPdoc)
Expand All @@ -27,7 +28,19 @@ class PatchInstaller implements PackageInstallerInterface
public static function install(MoufManager $moufManager)
{
// Let's create the instance.
$patchService = InstallUtils::getOrCreateInstance('patchService', 'Mouf\\Utils\\Patcher\\PatchService', $moufManager);
$patchDefaultType = InstallUtils::getOrCreateInstance('patch.default_type', PatchType::class, $moufManager);
$patchDefaultType->getConstructorArgumentProperty('name')->setValue('');
$patchDefaultType->getConstructorArgumentProperty('description')->setValue('Patches that should be always applied should have this type. Typically, use this type for DDL changes or reference data insertion.');

$patchTestDataType = InstallUtils::getOrCreateInstance('patch.testdata_type', PatchType::class, $moufManager);
$patchTestDataType->getConstructorArgumentProperty('name')->setValue('test_data');
$patchTestDataType->getConstructorArgumentProperty('description')->setValue('Use this type to mark patches that contain test data that should only be used in staging environment.');

$patchService = InstallUtils::getOrCreateInstance('patchService', PatchService::class, $moufManager);

if (empty($patchService->getConstructorArgumentProperty('types')->getValue())) {
$patchService->getConstructorArgumentProperty('types')->setValue([ $patchDefaultType, $patchTestDataType ]);
}

$consoleUtils = new ConsoleUtils($moufManager);

Expand Down
Loading