Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrations 3.0 #858

Merged
merged 123 commits into from
Nov 30, 2019
Merged

Migrations 3.0 #858

merged 123 commits into from
Nov 30, 2019

Conversation

goetas
Copy link
Member

@goetas goetas commented Sep 15, 2019

Q A
Type improvement
BC Break yes
Fixed issues #857 #853 #726 #805 #742 #854

Summary

This PR proposes what could become doctrine/migrations 3.0. (Some code and ideas are borrowed from #162)

This is a "work in progress", so any feedback is appreciated.

Exiting user-facing features are have been kept as they are ( some BC breaks are present of course).

Summary of changes:

  • The "version" is not anymore a number or date, but the FQCN of the migration class.
  • Multiple namespaces are allowed (and paths). This allows to provide third party migrations, plugin-provided migrations and and dependent migrations as well (as described in Migration dependencies #853)
  • Extracted the "metadata table" logic in its own interface as in [WIP] 2.0 Prototype #162
  • Most of the logs/prints not use psr/log
  • Used dependency injection in most of the "core" code parts (v1 and v2 have the god classes as Configuration, MigrationRepository and Version that were doing all possible kind of things and having circular dependencies between each other )

Tests have not been updated, but the provided prototype has most of the features already working.
Rollup and Diff commands have not been updated yet.

I realize that is a huge PR and hard to review, but the configuration and migration-repository class was passed all over the places, and as soon as I started to remove methods from it, each other class had to be changed...

Todo:

lib/Doctrine/Migrations/Metadata/AvailableMigration.php Outdated Show resolved Hide resolved
/** @var AbstractMigration */
private $migration;

public function __construct(Version $version, AbstractMigration $migration)
Copy link

@OskarStark OskarStark Sep 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am for making the ctor private and a have a static named constructor to be immutable.

Also I am for version() and migration() without the get prefix.

Same should be applied to the other value objects

*/
public $executionTime;

public function __construct(Version $version, ?DateTime $executedOn, ?int $executionTime)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecutionTime::fromMilliseconds(...) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. . And that method what should do? Just return back the same value?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but its self explanatory. Looking at $executionTime variable name is not helpful, unless it would be named $executionTimeInMs oder $executionTimeInMilliseconds.

it could also provide a ExecutionTime->inSeconds() which could be helpful when outputting it to the user? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you are suggesting is a value object instead of the integer, right?

Copy link

@OskarStark OskarStark Sep 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final class ExecutionTime
{
    /** var int */
    private $time;

    private function __construct(int $time)
    {
        $this->time = $time;
    {

    public function fromMilliseconds(int $time): self
    {
        return new self($time);
    }

    public function toMilliseconds(): int
    {
        return $time;
    }

    public function toSeconds(): // int or float here
    {
        return $time / 1000;
    }
}

Something like this, yes and then use it like this:

public function __construct(Version $version, ?DateTime $executedOn, ?ExecutionTime $executionTime)

lib/Doctrine/Migrations/Metadata/ExecutedMigration.php Outdated Show resolved Hide resolved
$this->platform->getDateTimeFormatString(),
$row['executed_on']
);
$migration = new ExecutedMigration($version, $executedOn, $row['execution_time']? intval($row['execution_time']) : null);
Copy link

@OskarStark OskarStark Sep 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here:

Suggested change
$migration = new ExecutedMigration($version, $executedOn, $row['execution_time']? intval($row['execution_time']) : null);
$migration = new ExecutedMigration($version, $executedOn, $row['execution_time']? ExecutionTime::fromMilliseconds(intval($row['execution_time'])) : null);

@jwage
Copy link
Member

jwage commented Sep 16, 2019

This is great. Love to see someone tearing this a part more! Can we document the BC breaks and update some of the docs to reflect the changes? I think it would help me understand what has changed so far from the users perspective and would allow me to provide feedback. Let me know when you are ready for a thorough review.

@goetas
Copy link
Member Author

goetas commented Sep 16, 2019

This is great. Love to see someone tearing this a part more! Can we document the BC breaks and update some of the docs to reflect the changes? I think it would help me understand what has changed so far from the users perspective and would allow me to provide feedback. Let me know when you are ready for a thorough review.

Sounds great! I have discussed this a bit with @alcaeus , currently I'm spending on this PR 5-8 h/week. I was planning in documenting the changes a bit. As soon as I have something will ping you here!

@stof
Copy link
Member

stof commented Sep 16, 2019

  • The "version" is not anymore a number or date, but the FQCN of the migration class.

Is there a migration plan available for that ? This involves at least 2 things regarding BC:

  • it will probably require to change the schema of the migration table (to allow unlimited length on the version field. How would that schema update be handled ?
  • what would happen for an existing project using migrations, where the DB contains dates as versions for already applied migrations. Can these be mapped back to new versions to recognize them as already migrated ?

Also, how will the ordering of migrations work ? Currently, when you have multiple unexecuted migrations, they are applied based on their version using date sorting. But once their version is the FQCN, we need to have a different sorting algorithm, and you don't describe what it is AFAICT.

@goetas
Copy link
Member Author

goetas commented Sep 16, 2019

it will probably require to change the schema of the migration table (to allow unlimited length on the version field. How would that schema update be handled ?
what would happen for an existing project using migrations, where the DB contains dates as versions for already applied migrations. Can these be mapped back to new versions to recognize them as already migrated ?

For both of this questions have the same solution. This part of code will be responsible for doing that migration. Is not yet coded, but is already on my todo-list.

Also, how will the ordering of migrations work ? Currently, when you have multiple unexecuted migrations, they are applied based on their version using date sorting. But once their version is the FQCN, we need to have a different sorting algorithm, and you don't describe what it is AFAICT.

The current sorting algorithm used in v2 for migrations is ksort($migrations,SORT_STRING) and that will be the same for FQCN, and should produce the same results (in v2 the version is just the part of the class name after Version*). So if migrations are in the same namespace there is no need for any change in the sorting algorithm.
If migrations use multiple namespaces (available only in this proposed 3.0), then you are responsible to provide your sorting algorithm to manage dependencies between namespaces.
In an ideal world, composer dependency graph could be used to provide out-of-the-box advanced sorting, but this is too app-specific and out of the scope for now.

@stof
Copy link
Member

stof commented Sep 16, 2019

n an ideal world, composer dependency graph could be used to provide out-of-the-box advanced sorting, but this is too app-specific and out of the scope for now.

I don't even see how that's possible. your own migrations will still need interleaving with the dependency migrations, as some of your migrations will have been written before the upgrade of one of the dependencies bring a new migration there. A dependency graph is not the solution here.

@goetas
Copy link
Member Author

goetas commented Sep 16, 2019 via email

@goetas
Copy link
Member Author

goetas commented Sep 18, 2019

Getting closer to something that works :)
Code Coverage for  home goetas projects doctrine-migrations lib Doctrine Migrations

@goetas
Copy link
Member Author

goetas commented Sep 19, 2019

Current status: ~90% of functionalities have been migrated (--range* feature on the migrate command is still missing).

I have also migrated most of the tests, wrote some new and deleted a lot of other. This is some critical issue. Since the previous version had 3-4 god classes tightly couples doing most of the job (Version, Configuration, MigrationRepository), there were a bunch of tests checking that the interaction between those are correct.
In the new implementation, the size of those classes has been cut by ten in some cases, and logic has been split in more independent classes. Because of that, the previously exiting tests became much more of a problem rather than a benefit.. thus I have deleted them.

My plan in the next days is to restore the test coverage to something around 95%, before that I plan to update the documentation with the user-facing changes. Documenting the code changes is a bit more difficult...

what about using https://github.com/Roave/BackwardCompatibilityCheck ? (here the GIST with the changes detected by it)

/cc @jwage @alcaeus

@OskarStark Thanks for the revirews, as soon as I will have a more polished architecture, will start addressing the comments.

@jwage
Copy link
Member

jwage commented Sep 19, 2019

@goetas Sounds like a good plan to me. We also need to at some point look in to how this affects all the 3rd party integrations, like the DoctrineMigrationsBundle for Symfony for example.

@goetas
Copy link
Member Author

goetas commented Sep 19, 2019

We also need to at some point look in to how this affects all the 3rd party integrations, like the DoctrineMigrationsBundle for Symfony for example.

That is on my todo-list (since the project that i'm working on will need the bundle too)

@goetas
Copy link
Member Author

goetas commented Sep 19, 2019

@jwage d916a07 documents the user-facing changes.

Some of the documented things are not yet fully implemented (home to have some time next week to discuss them with @alcaeus )

@goetas
Copy link
Member Author

goetas commented Sep 22, 2019

I've analyzed the current master and this branch with PhpDependencyAnalysis to track dependencies between classes in this doctrine/migrations.

The configuration file I've used is:

mode: 'usage'
source: './lib'
filePattern: '*.php'
formatter: 'PhpDA\Writer\Strategy\Svg'
target: './complex-cycle.svg'
groupLength: 3
visitorOptions:
  PhpDA\Parser\Visitor\Required\DeclaredNamespaceCollector: {minDepth: 2, sliceLength: 6, excludePattern: '/^((?!.*(Doctrine\\Migrations))|(?=.*Exception)).*$/'}
  PhpDA\Parser\Visitor\Required\MetaNamespaceCollector: {minDepth: 2, sliceLength: 4, excludePattern: '/^((?!.*(Doctrine\\Migrations))|(?=.*Exception)).*$/'}
  PhpDA\Parser\Visitor\Required\UsedNamespaceCollector: {minDepth: 2, sliceLength: 4, excludePattern: '/^((?!.*(Doctrine\\Migrations))|(?=.*Exception)).*$/'}

The results are the following https://gist.github.com/goetas/e5ee7546496e673360df5657ec5b38bd .

Red line in the graph are circular dependencies between classes. As you can see, in v3 all circular deps are gone.

@goetas
Copy link
Member Author

goetas commented Sep 23, 2019

STATIC_ANALYSIS (phpstan) still missing to be green... could be pretty challenging make it work!

Any help?

Copy link
Member

@stof stof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to fix the autoload_directories in phpstan.neon.dist, as the _regression folder is gone. That's what's breaking phpstan entirely right now.

lib/Doctrine/Migrations/DependencyFactory.php Outdated Show resolved Hide resolved
@@ -65,6 +46,9 @@ public function buildMigrationFile(

private function getVersionUpdateQuery(string $version, string $direction) : string
{
// @todo this is hard to implement since MetadataStorage abstracts the migrations table
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but without this, things are broken as the metadata storage is not updated with executing this file. So this must be solved.
It might mean that the MetadataStorage needs to expose a method returning the needed queries.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense then to have metadata storage interface? If it is going to be only one implementation, what about dropping the interface and typehint the concrete implementation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, the metadata storage interface could have a method returning the queries necessary to update the storage (which may be no queries if the storage cannot be updated in SQL).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then metadata storage interface will be bound to SQL, having other "storages" will make no sense... right? or in that case the extra method for the queries will just return NULL? )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have discussed this with @alcaeus . This will be covered in a separate PR/ticket and there can be explored a solution for the issue

@goetas
Copy link
Member Author

goetas commented Sep 24, 2019

Only 111 errors left :-)

@goetas
Copy link
Member Author

goetas commented Oct 1, 2019

PHPStan 0 errors! 🎉 🎉 🎉

@alcaeus alcaeus merged commit d74785f into doctrine:master Nov 30, 2019
@OskarStark
Copy link

Congratulations 🍾🎉🎊🎈

@@ -100,12 +98,12 @@ public function execute(
InputInterface $input,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also change the visibility from "public" to "protected" on the Command class to match the declaration with Symfony/Console ?
If you agree, I may open a PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Did not know was protected

@ostrolucky
Copy link
Member

I must say decision to use FQCN instead of version number is causing us issues. We have deployed branch with doctrine/migrations 3.x to our staging environment. It migrated the table. Now other branches with older version of doctrine/migrations fail because they don't recognize versions as executed anymore. For the next time, there should be a BC layer in currently maintained branch which recognizes both formats.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.