[wp-trac] [WordPress Trac] #30006: wpdb API refactoring
WordPress Trac
noreply at wordpress.org
Thu Oct 16 14:32:32 UTC 2014
#30006: wpdb API refactoring
-------------------------+-----------------------------
Reporter: aercolino | Owner:
Type: enhancement | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Database | Version: 4.0
Severity: normal | Keywords:
Focuses: |
-------------------------+-----------------------------
I wanted to simplify the task of reading and understanding code of wpdb,
and noticed that that pattern `if is_X then do_X else do_Y` (which affects
many wpdb methods) was distracting my focus onto differences between APIs.
It was also causing some code duplication. I found out that that pattern
was being used in 4 different ways (patterns themselves), so I made this
refactoring by adding a bit of structure to code.
This contains no dependency injection (#9953), no interfaces (#21055), no
database drivers (#21663) but only a refactoring of code introduced in
#21663. (starting from comment 135)
* First I got the list of mysql_* functions at
http://php.net/manual/en/ref.mysql.php and manually searched for their
correspondent mysqli_* functions into http://php.net/. (I hope I didn’t
miss any.)
* Then I selected mysql* uses in wpdb and extracted them into a new
wpdb_API class, which wpdb now extends.
'''Original title''': wpdb API refactoring
(sorry for the length of this description; I tried to document and justify
all bits)
=== Patterns detected into wpdb ===
'''Pattern A''', suitable when mysql_* and mysqli_* functions have the
same arguments, in the same order. (I applied this wherever I see fit)
{{{
extract($this->api_vars(‘function’)); // oops, extract!! Keep
reading...
$mysqli_function(...);
}}}
'''Pattern B''', suitable when mysql_* and mysqli_* functions have the
same arguments, in the same order or not. (I applied this wherever I see
fit)
{{{
$this->api_call( 'mysqli_function', ... )
}}}
The reason for having A while B would have been enough, you might wonder,
is twofold.
On one side, A is needed for doing things like
{{{
if ( function_exists( $mysqli_function ) ...
}}}
On the other side, A is quite nice to use right now for this patch,
instead of B, because it allows to replace the `if is_mysqli then
mysqli_do else mysql_do` branching simply with `$mysqli_do`. Variable
functions are so PHP!
'''Pattern C''', suitable when the difference between mysql and mysqli
branches is a bit more than calling a correspondent function (with or
without an “i” in the middle). It consists of extracting the branching
into its own wpdb_API protected method. (I applied this once)
Example: wpdb::set_sql_mode. It had a piece of code that, after applying
the B refactoring, still needed like 25 lines to get the current SQL
modes. So I extracted that code into its own protected method
wpdb_API::db_modes.
'''Pattern D''', suitable in any other case, i.e. when the difference
between mysql and mysqli branches is quite big. It consists of extracting
the three pieces of the branching (api_check, mysql_branch and
mysqli_branch) into their own wpdb_API protected methods. (I applied this
once)
Example: wpdb::db_connect. It was quite big and complicated, including a
recursion step under certain circumstances. I cut the code into different
pieces, and moved them up into wpdb_API in their own protected methods
wpdb_API::db_connect, wpdb_API::mysql_connect and
wpdb_API::mysqli_connect. With this separation the need for a recursion
step vanished.
=== Notes (see also documentation in the code) ===
'''PHP extract function'''
WP has become a very challenging system: Many bits of code have a big
ticket behind. (I don’t say I don’t like challenges, because I do)
In this case: #27881. It seems that extract is getting discouraged in WP
for improving HHVM compatibility. In this refactoring, we could just use B
instead of A, or we could also use `$function[‘mysqli’][‘name’]` instead
of `$mysqli_name` but it looks much uglier.
If the problem with extract is that we can’t easily spot where variables
in the current scope come from (#22400), that is easy to solve by adding
`/** @var $mysqli_* */` annotations soon after calling extract, as I did
here.
So, for the time being, I’ve left extract in place.
'''Put into wpdb_API: $dbuser, $dbpassword, $dbname, $dbhost, $dbh'''
I thought that these connection data made more sense into wpdb_API than
wpdb, so I moved them. They make more sense not because of the API part
per se but because of the presence of wpdb_API::db_connect, which uses
them.
'''Added wpdb::filter_query, wpdb::filter_row'''
A code comment for the `apply_filters(‘query’, $query)` call in the
wpdb::query method says "Some queries are made before the plugins have
been loaded, and thus cannot be filtered”. That was one of the reasons for
writing my Full UTF-8 plugin as a drop-in.
Due to the lack of those two methods, I had to override wpdb::query (and
recently also _do_query… see below). Now they are defined to echo back
whatever they get and are called immediately before querying and
immediately after retrieving the results.
With these two methods in place, my drop-in gets zero duplication. (I
tried it out) If you are concerned about performances slow down, I can
tell you that my plugin blindly escapes all the query and un-escapes all
the results and I don’t notice it. (used for 4 years) Lately I tried to
benchmark it precisely (also using 200KB "lorem ipsum" posts) but didn’t
get any consistent trend. Some times it was slower and some other times
even faster with my plugin activated. I think the reason is that in the 40
queries needed for one editing page generation, all these PHP time
differences disappear in comparison to lags for moving info to and from
the database.
This is a small change that will allow less duplication in drop-ins.
(nasty duplication, because plugins developers do not constantly monitor
WP code to promptly apply needed patches).
'''Deprecated wpdb::$use_mysqli'''
To open the path to new APIs in the future, I thought that a string would
better fit this scenario than a boolean: `‘mysqli’ == wpdb_API::$api`.
Given to how property overloading methods are implemented in wpdb, even if
wpdb::$use_mysqli was declared private, it’s not possible to just remove
it, because other developers could have used it through that backdoor.
So wpdb::$use_mysqli is now deprecated (and read only). See below for
deprecated property functionality.
'''Deprecated wpdb::$has_connected'''
While perusing wpdb::db_connect I saw that the fallback flow could be made
more explicit, cleaner, and we could avoid 1-step-recursion and get rid of
wpdb::$has_connected.
$has_connected was a bit buggy, in my opinion. If I’m not wrong, it was
introduced for telling apart the second execution (recursion step) from
the first one, but the check was ill placed, causing $has_connected to
never be checked at run time.
In fact, suppose mysqli_connect is called because $use_mysqli is true but
fails for some reason and a fallback is attempted. In that case the
recursion step is triggered after putting $use_mysqli to false. During the
second execution, mysql_connect is called.
* In case of success, $dbh is true-y, so $has_connected becomes true and
the execution ends returning true to the caller.
* In case of failure, $dbh is false-y and either we bail or not, but the
execution ends returning false to the caller.
BUT the caller is the first execution, so the returned value is discarded
and, AFAICT, we’ll again execute the same code we just did. It works “OK”
because all that code must be repeatable (somehow) and the original caller
ends getting the correct return value because $dbh didn’t change after
coming back from the second execution to the first one.
Given to how property overloading methods are implemented in wpdb, even if
wpdb::$has_connected was declared private, it’s not possible to just
remove it, because other developers could have used it through that
backdoor.
So wpdb::$has_connected is now deprecated (and read only). See below for
deprecated property functionality.
'''Added wpdb_API::$fallback'''
The wpdb::db_connect local variable $attempt_fallback was based on the
same checks used for wpdb::$is_mysqli so I introduced a
wpdb_API::$fallback property, which can have four states: impossible,
possible, success, failure.
Initially it’s null. wpdb_API::init_api_to_use (called from the
constructor) sets it to either “possible” or “impossible”. And
wpdb_API::db_connect sets it to either “success” or “failure”, only if a
fallback was possible and has been attempted.
'''Added deprecated properties (separate patch)'''
Given that property overloading methods are already implemented in wpdb,
it was natural to use them for signaling deprecation of wpdb::$use_mysqli
and wpdb::$has_connected.
I added all the bits (hopefully) for the functionality along the lines of
function and argument deprecations. I added a test to make sure the
deprecation error is triggered.
'''wpdb extends wpdb_API'''
Drop-ins could extend wpdb_API instead of wpdb, taking advantage of proven
code for connecting and interfacing the API directly, thus helping reduce
duplication in drop-ins.
'''Refactoring of wpdb::set_sql_mode'''
The foreach that computed $modes minus $incompatible_modes is now
array_diff($modes, $incompatible_modes).
'''Fixed 'deprecated_file_included' in tests'''
File deprecation had a couple of documentation errors coming from copying
and pasting argument deprecation.
* In src/wp-includes/functions.php, we call
{{{
do_action( 'deprecated_file_included', $file, $replacement, $version,
$message );
}}}
* In tests/phpunit/tests/functions/deprecated.php
* we did bind the action to ‘deprecated_file’ but unbind it from
‘deprecated_argument’.
* we defined
{{{
public function deprecated_file( $file, $version, $replacement,
$message ) {
}}}
I assumed this was another error due to copy and paste so I fixed the
signature of the handler (deprecated_file) to have the arguments in the
same order as they appear in the trigger (do_action) and then I unbound it
from 'deprecated_file_included'.
'''Should be protected wpdb::_do_query'''
In the last update to my Full UTF-8 drop-in not only I had to override
wpdb::query (like I used to) but also wpdb::_do_query for no other reason
than that it was declared private. This is another occurrence of what I
called before “nasty duplication” BUT I don’t understand how to fix it
yet.
I tried to change it to protected and it worked perfectly, except for a
fatal error (sigh, it’s not a joke). The fatal error was due to having
made it protected in wpdb but having it still private in my drop-in. Which
means we now can’t change it without breaking stuff.
'''@since X4.2'''
Wherever I introduced something, I marked it with this annotation, to be
able to do a search and replace.
'''Tests clean for wordpress-develop on October 16, 2014 at 13:15 UTC.'''
I updated from svn and ran phpunit and I had no failures due to this
refactoring.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/30006>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list