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

Refactor un/installer class #404

Closed
JDGrimes opened this issue Jul 7, 2016 · 39 comments
Closed

Refactor un/installer class #404

JDGrimes opened this issue Jul 7, 2016 · 39 comments
Assignees
Milestone

Comments

@JDGrimes
Copy link
Member

JDGrimes commented Jul 7, 2016

The un/installer class bundles all of the bootstrap for installation, uninstallation, and update, into a single class. It is growing to thousands of lines long, with hundreds of methods. We need to refactor this. Ultimately, I think the uninstall, install, and update portions should each be split into a separate helper class. Then we'll probably want to make each uninstall feature a separate class, which will allow for greater extensibility. We may also want to make it so that each update is a separate class as well.

During all of this, we will want to maintain back-compat of course, which probably won't be too tricky.

@JDGrimes
Copy link
Member Author

Related: #264

@JDGrimes JDGrimes added this to the 2.3.0 milestone Dec 8, 2016
@JDGrimes
Copy link
Member Author

JDGrimes commented Dec 8, 2016

Let's see if we can take a whack at this in 2.3. Possibly without doing #264 yet, but preparing the way for it.

@JDGrimes JDGrimes modified the milestones: 2.4.0, 2.3.0 Feb 23, 2017
@JDGrimes JDGrimes self-assigned this Jul 5, 2017
@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 5, 2017

I'm thinking that we'll probably want to introduce an installable class, that will keep track of the version info, network install status, etc. This will sort of be the core of the current un/installer class, which will likely be able to extend it. But the install, update, and uninstall features will be split out into separate helper classes that are utilized by or operate on the installable class.

@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 5, 2017

One thing I noticed while refactoring is that currently in unset_db_version() we actually unset all of the data for the entity, not just the version, except for when dealing with WordPoints itself. This has not caused us problems only because we only call this during uninstall. The behavior was actually what we wanted to accomplish in that case, but I'm going to change to avoid bugs in the future.

@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 6, 2017

I was going to have the base un/installer class extend the new installable class. However, many of the methods that we are now public were previously protected. As a result, any child classes that are overriding these methods will get a fatal error:

PHP Fatal error: Access level to WordPoints_Breaking_Updater::is_network_installed() must be public (as in class WordPoints_Un_Installer_Base) in /Users/johngrimes/git/wordpoints/src/classes/breakin...

Unfortunately, I don't think that there is any way to work around this. So instead of extending the new installable class, we'd have to either just utilize it internally, or leave the methods that were copied to it intact on the un/installer, and suffer the duplication. I'll try seeing if we can utilize the new installable class internally first though.

@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 7, 2017

I'm now working on splitting out an installer, and I was thinking that each part of the install routine would be split out into a class representing the subroutine. Currently installing database tables and custom caps is automatically handled, for example. So we'd have a subroutine that would handle each of those.

I was thinking that we'd grab the install info from the installable object, and then run the necessary subroutines, passing the needed data to each one. However, I was getting snagged on the question of what to do when we were given a subroutine slug by the installable that we didn't recognize. So instead, I'm now thinking that the installable object should return the already-constructed subroutine objects, and then we just have to loop over them as needed.

@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 10, 2017

I think that in the new installer there is no need for the bitmask return from the skip install check function as was introduced in 55267ab. We don't need this because we can now check if an installable's routines need to run on each site or not, since we have the list of routines that need to run. Also, we're not expecting it to be overridden in sub-classes in that way anymore. If we do need to do that in the future, we can just introduce a second method to check if there is any install to run, while this one will still just check if the install should be skipped.

@JDGrimes
Copy link
Member Author

I've just realized that when there is no per-site install routine to run, the IDs of the sites aren't added to the list of sites on which the entity is installed. This is true both in the new and old code.

Fortunately, this only affects the ranks component in core, since that is the only thing that doesn't have a per-site install routine. But we ought to fill in the site IDs on update.

We'll also need to modify the code in both the old and new implementations, because I don't think that we'll be able to have the old un/installer class extend the new installer. :-(

@JDGrimes
Copy link
Member Author

This also made me realize that we realize that we ought to optimize the adding of these site IDs anyway, so that we don't have to switch to each site (which in this case we don't need to do otherwise). We should just add all of these site IDs at once, saving ourselves a bunch of option updates (and retrievals as well, I guess, for that matter).

@JDGrimes
Copy link
Member Author

I'd already split out the database table creation code into its own class as part of the new installer design. But as I am writing the tests for it, I realized that it just assumed that the dbDelta() function will be loaded (that function is part of WordPress's update functions, and isn't normally loaded).

I don't really want to require_once the file within the class, for the sake of performance. It would be silly for that to run every single time on multisite (maybe negligible in comparison to database creation, but still). I guess though, that we could put it in the constructor, and since the class would only be constructed once in the new design, that wouldn't be a problem. So let's do that, even if it feels a little strange.

@JDGrimes
Copy link
Member Author

On the other hand, I was wondering if we really need to use dbDelta() here at all. That function includes a lot of extra handling for updating a database table based on the supplied schema, if it already exists, and that isn't necessary if we are just creating the tables. So the only reason to use dbDelta() is if we also want to be able to use the same class for updates as well as install. And I guess that may make sense, so let's give ourselves that option.

@JDGrimes
Copy link
Member Author

I'm thinking that we'll circle back and fix the site IDs issue after we get the updater split out. Then we can write the update code using the new API.

@JDGrimes
Copy link
Member Author

While extracting the updater, I noticed that we required the network-active status of the entity to be passed in, just as we do for install. However, this seems unnecessary for updates, since at that point the entity has already been activated and so we can determine whether it is network active or not independently.

I was going to use the is_network_installed() method to check this, but of course that only means that the entity has been network active at some point, not that it is currently network active.

A little more investigation revealed, however, that we actually never pass a value in for the $network parameter of the update() method, and so it has always been internally defaulting to the value of is_wordpoints_network_active().

Now that I think about it, I recall that the logic behind this was that even when an extension isn't network active itself, it should still update all sites on the network where it is active at once, rather than one by one as the update routine is triggered on each. The reason is that we also have to think about site switching, which could cause data to be attempted to be pulled from another site on the network before the update routine has been triggered on it. If we still think that logic is valid, then perhaps we don't need to offer a $network parameter for the updater at all.

@JDGrimes
Copy link
Member Author

A problem here though, is that we loop through all sites where the entity is installed, not just the sites where it is currently active. When it is network-activated, these two would be the same; there'd be no sites where the entity was installed but not active (nor should there be any sites where it is not installed, but active, either). But when per-site installed on multisite, the entity would end up having the update routine run for all of the sites where it was installed, even though it wouldn't necessarily still be active on all of them.

This wouldn't be a problem if we were running the update for each site separately, when the site was visited. We'd only run where we were active, because we'd only run for the current site; we wouldn't use the installed site IDs list at all.

As it stands, we don't actually have an easy way to get a list of all sites where the entity is active, so other than adding such a feature or changing this behavior, I see no other means to remedy this.

On the other hand, perhaps this behavior should really be considered correct. After all, if we don't run the updates per-site, then the update won't end up getting run on any site's that the entity is deactivated on, even if/when it is reactivated on that site. So if we don't run the update along with all of the others, it will never occur.

As such, I'm going to call this a "feature", and leave it as it is for now. If we find a compelling reason to change it in the future, we can do so.

Which also means that there's no reason to have a network argument for the updater; for now we will just have it match WordPoints's network activation status internally, as is the current behavior of the un/installer.

@JDGrimes
Copy link
Member Author

For the updates, I am trying to decide whether to return all update routines and then decide which one to run in the updater, or how to design it. I think that it doesn't make much sense to load a bunch of update routines that don't even need to be run. So we should only construct the update routines that actually need to run.

I suppose that we could return the class names for the routines from the installable object, and not the constructed routines themselves, and then let the updater decide which ones to construct. But that doesn't allow us to pass any information to the objects when we construct them, which sound limiting. So I don't think that is really an option.

Which means that we need to return the update routines already constructed, and only those that actually need to run, for optimization. So we'll have had to already do the version checks.

@JDGrimes
Copy link
Member Author

We currently do the version checks all in one place, and this avoidance in duplication is especially nice if we allow the to and from versions to be specified arbitrarily, since that contributes additional logic.

However, I'm now wondering whether the to and from checks are necessary at all. Since the installable knows its current code version and database version internally, there is no need for that information to be passed in. The fact that the un/installer didn't know that internally is why it was designed that way to start with—not because we wanted to be able to update from and to arbitrary versions. And really, I can't conceive of a situation where we would need to update from or to an arbitrary version. All of the updates needed really do need to be run, or else the database, etc., won't match the expectations of the code.

So passing in arbitrary to and from versions is now a useless feature that we don't need to carry over.

@JDGrimes
Copy link
Member Author

JDGrimes commented Jul 12, 2017

In the installer, we automatically set the DB version, and we automatically remove it upon uninstallation. But we don't automatically update the version on update, that has to be done externally. This doesn't really make sense though, so I'm going to have the new updater update the installable's DB version automatically.

@JDGrimes
Copy link
Member Author

On a similar note, when we detect that there are no per-site updates for a particular entity, we skip that part. But this means that the database version on that site is not updated. This is true even in the old code, I think. If the entity is not network wide, we use the version from the current site, and only update it for the current site. But as pointed out above, we still run the update for all sites where the entity is installed. Except that of course the version isn't updated for those sites.

I guess we need to either stop doing that, update the version for every site, or store the version network wide even when only per-site active.

@JDGrimes
Copy link
Member Author

But as pointed out above, we still run the update for all sites where the entity is installed.

When WordPoints is network active. Otherwise we don't.

Also, I ended up mixing two separate issues above. The one is that in the old code when the entity is per-site installed and WordPoints is network active only the version for the current site is updated. (Though it should be noted that the update will be triggered to run on the other sites as well, and then the version will be updated there. But this in and of itself represents another issue, because the updates may not be designed to run more than once.) The other issue is that in the new design, when we take care of this internally, and so it is fixed in the most general case, we still have the same problem when we skip per-site updates altogether, because then we don't end up calling the per-site update method at all, which is where we update the version. To fix that we'd have to switch to each site just to update the version number, which seems silly.

@JDGrimes
Copy link
Member Author

Split into a separate issue, let's carry on. We can't really address the issue in the new design until we make some decisions from that ticket, so we'll just leave it with that bug in it for now.

@JDGrimes
Copy link
Member Author

Our logic that skips the per-site update on large networks doesn't make much sense either, because it only does that when the entity is network installed (regardless of whether it is network active), but the updates will be run for all sites that it is installed on even when it is not network installed, no matter how many sites it is installed on. So the number of sites that it is installed on should probably be checked then too, and the process skipped if the number is too many.

JDGrimes added a commit that referenced this issue Aug 21, 2017
To ensure that it is added to a list, if the installable object is added
in a code update after the extension is initially released.

We also tweak the installable class to ensure that site IDs are only
added once.

Fixes #715
See #404, #714, #713, #704
JDGrimes added a commit that referenced this issue Aug 21, 2017
When an extension was not network active, the old installables class
would still run the updates for all sites at once. However, in the new
installables app this was fixed for the new installable objects. We
still weren't handling it properly in our shim for legacy entities still
using the un/installer API though, so we've updated it to pass in the
`$network` parameter correctly for extensions.

See #706, #404
JDGrimes added a commit that referenced this issue Aug 21, 2017
JDGrimes added a commit that referenced this issue Aug 21, 2017
Introduces a factory class for update routines, to enable most logic to
be removed from the installables an into the updater itself.

The installable must now return an array of update routine factories to
the updater, rather than the update routines themselves. The updater
then uses the factories to retrieve the routines that it determines need
to run.

The logic in the updater is also updated to determine the network
version for per-site installed entities separately from per-site
versions. The installer is updated to save the per-site version in that
case as well, and not just when network-active.

The core installables have been updated to use the new factory design.

Because one of the updater objects needs to be constructed with 5 args,
the utility function for that purpose had to be updated to support 5
args instead of just 4.

Fixes #706
See #404
JDGrimes added a commit that referenced this issue Aug 21, 2017
For other installables, run the updater anyway.

Restores behavior previous to #404.

Fixes #716
JDGrimes added a commit that referenced this issue Aug 21, 2017
JDGrimes added a commit that referenced this issue Aug 22, 2017
Fixes issues resulting from the installables app not being mocked during
some tests, leading to the installable remaining registered with the
app, and perpetuating its version in the database. In order to fully fix
this, we needed to also allow the installable to be registered more than
once, so that it would have its app registered again. Some issues with
the tests were also revealed by this, and fixed.

We also realized that we use 'extension' as the type for the
installables registered in the new code, but the legacy code was still
using 'module', so we've standardized on 'extension'.

See #404
JDGrimes added a commit that referenced this issue Aug 22, 2017
The class is now deprecated and no longer contains any functionality, so
the tests would be useless.

See #404
JDGrimes added a commit that referenced this issue Aug 22, 2017
When we decided to make this class abstract we missed updating these
usages.

See #404
JDGrimes added a commit that referenced this issue Aug 23, 2017
Calling a static method by name from a variable is not supported prior
to PHP 7.

See #404
JDGrimes added a commit that referenced this issue Aug 23, 2017
Otherwise the installables app will attempt to use the registered class,
which won't be loaded because the component isn't active.

See #404
JDGrimes added a commit that referenced this issue Aug 23, 2017
This isn't a good pattern to use in general anyway, and it didn't work
on PHP 5.2, so we've removed these methods.

This updates the tests to avoid using the methods.

- First, we can avoid using them in the component and extension
  installable tests by updating the code to use a method we already
  provide instead of accessing the property directly.
- We change how the installables class handles custom caps, so that it
  now uses a method instead of a property. This was a carry over from
  the old un/installer, but it was a poor design. We've already made a
  similar change for the database tables, this makes it so that the code
  for the custom caps is now consistent with that.
- In the installable tests where these methods were used to access the
  properties of install or uninstall routines, we've simply omitted this
  from the code completely. We test that the routines returned exhibit
  the correct behavior in our uninstall tests.
- We utilize the new basic installable class in the installable tests
  rather than a mock, which avoids having to set the properties this way
  since we can just pass them to the constructor.
- For the order test for the routine class, I've moved the test of this
  behavior to the tests for the uninstall routine class, which actually
  sets the property internally to utilize this behavior.

See #404
JDGrimes added a commit that referenced this issue Aug 23, 2017
We would get failures like this:

```
1) WordPoints_Installer_DB_Tables_Test::test_creates_db_tables

Failed asserting that format description matches text.

--- Expected
+++ Actual
@@ @@
 CREATE TABLE `wptests_test` (
   `id` bigint(20) NOT NULL
-) %autf8 %autf8_general_ci
+) ENGINE=InnoDB DEFAULT CHARSET=utf8

/home/travis/build/WordPoints/wordpoints/tests/phpunit/tests/classes/installer/db/tables.php:67

2) WordPoints_Installer_DB_Tables_Test::test_creates_db_tables_with_prefix

Failed asserting that format description matches text.

--- Expected
+++ Actual
@@ @@
 CREATE TABLE `wptests_test` (
   `id` bigint(20) NOT NULL
-) %autf8 %autf8_general_ci
+) ENGINE=InnoDB DEFAULT CHARSET=utf8

/home/travis/build/WordPoints/wordpoints/tests/phpunit/tests/classes/installer/db/tables.php:95
```

The problem is that MySQL does not always include the collation in
`SHOW CREATE TABLES`. So we've adjusted the tests to not include the
collation in the pattern we check for.

See #404
JDGrimes added a commit that referenced this issue Aug 24, 2017
Supports more mock object methods on PHP 5.2.

See #404
JDGrimes added a commit to WordPoints/dev-lib that referenced this issue Aug 25, 2017
So that we don't have to try to set the protected property using
reflection, which doesn't work on PHP 5.2.

See WordPoints/wordpoints#404
JDGrimes added a commit that referenced this issue Aug 25, 2017
So that we don't have to try to set the protected property using
reflection, which doesn't work on PHP 5.2.

See #404
@JDGrimes
Copy link
Member Author

OK, I guess if anything else needs to be done here new issues should be opened.

JDGrimes added a commit that referenced this issue Sep 7, 2017
The "r" key on my keyboard has been acting up...

See #404
@JDGrimes JDGrimes moved this from Related to Completed in Un/installer rewrite Sep 11, 2017
JDGrimes added a commit that referenced this issue Sep 20, 2017
The legacy installables app needs to use `module` internally, but the
new installables app uses `extension`. We attempted to handle this in
2e516a7, but accidentally were modifying the `$slug` of the installable
instead. We also didn't correctly ensure that the installable objects
were still constructed with `module`, because that is what they expect.

See #404, 2e516a7
JDGrimes added a commit that referenced this issue Sep 20, 2017
It is no longer used in core, and has been replaced by the new
installables API.

Only the breaking updater still uses it, and that will be fixed in a
subsequent commit.

See #404
JDGrimes added a commit that referenced this issue Sep 20, 2017
The code for uninstalling an extension with an installable class was
broken because we were passing the basename of the extension to
`wordpoints_get_module_data()`, when it actually expects the full path.
Our tests were updated in the last commit so that this is now tested,
and of course that exposed that it wasn't working. We thought it was
worth committing the fix separately, since it wasn't directly related
to the main brunt of that commit.

See #404
JDGrimes added a commit that referenced this issue Sep 20, 2017
And deprecate the old version, which extended the now-deprecated base
un/installer.

In the process we found a bug in the filesystem mock library, which we
fixed, requiring us to update to the newer version.

See #404
JDGrimes added a commit to WordPoints/dynamic-points that referenced this issue Oct 5, 2017
Deprecates the old un/installer.

See WordPoints/wordpoints#404
JDGrimes added a commit to WordPoints/dev-lib that referenced this issue Oct 12, 2017
Extensions that use the new installables API were not being updated
correctly, because it was always assumed that they were network active
when WordPoints was network active. This is not the case. In fact, the
extensions are not network active by default, and so update tests for
them would fail.

See WordPoints/wordpoints#404
JDGrimes added a commit to WordPoints/reset-points that referenced this issue Oct 19, 2017
And deprecate the old un/installer class.

See WordPoints/wordpoints#404
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

1 participant