-
Notifications
You must be signed in to change notification settings - Fork 14
A non-breaking solution for (#45) #49
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
Conversation
…at capitalization for attribute names are retained. So the short form for New York is now NY instead of ny. test/model/Author, Book, and Venue now have those strtoupper() workarounds in getter/setter methods removed as it's done in Model.php
…rcased in Model.php in order to be case insensitive.
…at capitalization for attribute names are retained. So the short form for New York is now NY instead of ny. test/model/Author, Book, and Venue now have those strtoupper() workarounds in getter/setter methods removed as it's done in Model.php
…rcased in Model.php in order to be case insensitive.
# Conflicts: # lib/Model.php
…cord # Conflicts: # test/ActiveRecordTest.php # test/models/Book.php
…Disable it so that regression does not timeout.
…d#35: Authors table now has a column that is camel cased: firstName Added tests that demonstrate case insensitivity when accessing this firstName column
- Correct link to contributing.md
…vity() is enough.
|
Hey, looks pretty cool. I'll have to analyze this in detail, but just a quick note:
Because it aligns with the conventional Active Record pattern, and it IS type-checked. Please familiarize yourself with PHPStan and the extensions I've been laboring over for the past week. |
|
The options parameter though is not checked at compile time. I've written bugs like these before: Author::find(3, ['orderBy'=>'author_id DESC']);gives a run time error in a line later than where the bug resides. It should be: Author::find(3, ['order'=>'author_id DESC']);In the new way, this runs fine: Author::orderBy('author_id DESC'])->where(3);while this fails on the line where the typo bug is at. Author::order('author_id DESC'])->where(3);The old way still works, but I prefer the new way as typo-related errors are found where they are made. |
|
Right, so with your new design we wouldn't pass that stuff to Author::order('author_id DESC'])->where(["name" => "bob"])->find([1,3, 4]); |
|
All I did I is rebase after the merge of #48. PHPUnit 8.1 passes, but PHPUnit 8.2 gives an error I've never seen before: in the setup job phase. Isn't that a build environment error rather than a coding error? shmax, is this something you can fix or is this something fixable on my end? |
|
Hmm, beats me, but I don't think it's your fault--I re-ran the jobs, and they're green, now. |
This is a good question, a philosophical one. I really don't like find() returning a So the above would be (simplified) to: $query = Author::where(["name" => "bob"])->find(1);
$queries = Author::where(["name" => "bob"])->all([1,3]);Most of the time though, you're not filtering on the primary keys (find) and the non-primary keys at the same time (where): $query = Author::where(["name" => "bob"])->find();
$queries = Author::where(["name" => "bob"])->all();to the point where you would always be ending every call with a In the existing design, you would do the above with: $query = Author::where(["name" => "bob"]);
$queries = Author::all(["name" => "bob"]);which is easier to read and write for the common cases, and $query = Author::where(["name" => "bob", "id" => 1]);
$queries = Author::all(['name = (?) and id in (?)', 'bob', [1, 3]]);for the non-common cases where you're filtering on key and non-key fields at the same time. The A philosophical question. Your thoughts? |
|
I read you loud and clear, but my instinct is still not to diverge too far from the basic behavior of But let me look into it in more depth when I get done with the phpstan stuff I'm currently toiling away on, and then I'll be in a better position to discuss this. |
|
In my work on this PR, I ran into two types of name space collisions: A problem with last and find()Say you have a table with the primary key as a string. Now does: $query = Author::where(["name" => "bob"])->find('last');mean you're searching on the primary key='last' or you want to find the last row with name=bob? The existing design fixes the problem with: $query = Author::where(["name" => "bob"], false); // use true, which is the default, to return the first rowA problem with raw where statements and all()Say again you have a table with the primary key as a string. $queries = Author::all(["a", "b"]);is obvious what the string[] input parameter does, but what about? $queries = Author::all(['name = (?) and publisher <> (?)', 'Bill Clinton', 'Random House']);That's also a string[], so how based on the type of the input parameter, do you know to do a primary key search or a conditions search? The existing solution to try the primary key search first, catch the The new design would solve this problem by: $queries = Author::all(["a", "b"]);
$queries = Author::where(['name = (?) and publisher <> (?)', 'Bill Clinton', 'Random House'])->all();These corner cases are hard to imagine and find, and only by working deeply with the as: /**
* Alias for self::find('all').
*
* @see find
*
* @return static[]
*/
public static function all(/* ... */)is now: /**
* @param array<number|mixed|string> $needle An array containing values for the pk
*
* @return array<Model> All the rows that matches query. If no rows match, returns []
*/
public static function all(array $needle = []): arraybut that was hard work to figure that out. I expect that I might be able to do something like that for As for the the final fate of |
lib/Relation.php
Outdated
| * | ||
| * @return Model|null The single row that matches query. If no rows match, returns null | ||
| */ | ||
| public function where(int|string|array $needle): Model|null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, should return a Relation.
| * | ||
| * @return array<Model> All the rows that matches query. If no rows match, returns [] | ||
| */ | ||
| public static function all(array $needle = []): array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all doesn't take any arguments; I think it's only meant as a means to get a handle to the Relation without executing anything:
$allBooks = Books::all();
...
$books = $allBooks->where(['title'=>'foo'].to_a()'
lib/Relation.php
Outdated
| return $this; | ||
| } | ||
|
|
||
| public function orderBy(string $order): Relation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't we still doing snake_case in the lib code?
| */ | ||
| public function all(array $needle = []): array | ||
| { | ||
| $list = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all doesn't take any arguments or do anything; all it needs to do is return itself.
…her than joins() - Add from chainable function. It doesn't exist in Rails4, but it is in the PHP ActiveRecord spec - Move implementation of find() from Model to Relation - Remove find_by_pk() as the regression tests and documentation says you should call find() instead - Fix bugs in ActiveRecordFindTest which expected RecordNotFound exceptions as opposed to TypeError exceptions when passing invalid input
Codecov Report
@@ Coverage Diff @@
## master #49 +/- ##
============================================
- Coverage 95.23% 94.99% -0.25%
- Complexity 935 986 +51
============================================
Files 32 33 +1
Lines 2246 2378 +132
============================================
+ Hits 2139 2259 +120
- Misses 107 119 +12
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
|
Ok, the next step is done. Model::find() is now calling Relation::where() and Relation::all() and Model::find*() are calling Model::find(). That means all the find() are going through Relation's implementation and this major refactoring passes regression.
Model does the One issue that caused great complexity in the code is the version 1 inconsistent handling of queries that don't return a record. If it's a find by primary key, then it throws an exception. If it's a find by condition, then it returns null, or []. However, this is not even internally consistent. $thisIsNull = Author::find_by_author_id(999999);
$butThisThrowsAnError = Author::find(999999);The reason is that dynamic finders are always converted to a find by condition even though that condition is searching on the primary key. If we're allowed to break things for version 2, I would like the return value to always be null when expecting a single |
|
This is still a draft so I assume it's very much a work in progress, but I still see some architectural rough edges that need to be smoothed out:
And so on.
Don't worry about breaking compatibility with 1.x (that ship has sailed); I'm more interested in aligning with RoR wherever possible. If you need to drop Keep up the good work. 👍 |
|
Also, I'm out of other stuff to do, so consider me available to help. If you need me to tune up the dynamic static checking stuff or otherwise contribute just say the word. |
…n the test suite and it's obviously an internal function that should have never been made public. Use find() instead.
|
Ok, where() and find() are now implemented! I've tried to hew closely to the Ruby on Rails standard. To do:
Unfortunately, I'm going to have to pass the rest of this feature to you. I have to go back to work now, so it'll be at least about a week before I can check in again. Please feel free to take it anywhere from here without consulting me. Since Ruby on Rails has a published spec and we're following it, it's now just a matter of implementation rather than design. The heavy lifting has been done though, and I think the larger community will appreciate the new chainable interface once it's released to Packagist. It's been nice working with you as you're really quick to answer, and I've learned lots on this project. Thanks for everything. |
|
That's definitely some great progress! I'm happy to tag-in on this, and will begin pushing changes soon. Thanks so much for taking the lead on this, and we'll catch up again in a week or so. 👏 |
|
Just thought I'd give a little update:
The tricky part is [
'conditions' => [
'select id from books where name = :name', [
'name' => 'bob'
]
]
];That will quickly become complicated when we start trying to aggregate multiple |
|
I did a little experimenting with Ruby on Rails, and it does allow you to mix named and unnamed parameters: @res = User.where("admin = ?", 1).where("name = :name", { name: "Michael Example" }).to_aI'd like to see how they handle that, so today's project is going to be to download the source for rails and do some deep debugging... |
|
Thanks for the update. It sounds like the project is is good hands. I only had enough time to demonstrate that the chainable implementation was doable while maintaining complete backwards compatibility, but finishing off all the corner cases is a lot of detail work and I'm so I'm glad you're willing to do it. I have four thoughts that have come to mind since the last commit:
Really, Finally, there's a question of how much of Ruby on Rails to replicate before you release version 2 of ActiveRecord. My 2 cents is to completely implement version 1 functionality in version 2 (and we're pretty close already, especially if you implement the above 4 points) as the minimum standard to release. Also, a LOT of documentation has to be written, but it could point to working test cases. As a user, I find the test suite the most helpful way to understand ActiveRecord and how to actually do something. For example, |
We'll get 'em! Actually, it seems that I haven't come across
If by version 1 you're talking about php-activerecord, then agreed, I think, with the understanding that the concept of passing "conditions" into I did a little more RoR debugging, here, and I think I can now answer the question of how it handles mixing of named and unnamed params: it sanitizes them on the spot. So, if you do something like |
:) Yes, I actually know how crazy that is. Exposing the conditions internal variable made life a lot more difficult to moving to version 2 as that was an implementation detail rather than a spec one, and now we want to change the implementation while maintaining backwards compatibility and removing conditions from the spec. The conditions regularization code is in I have two thoughts on
Author::where(['name' => 'bob', 'parentId=4'])
->where("mixedCaseName='Bill'")
->find(2);into: [0] => ['name' => 'bob', 'parentId=4']
[1] => ['mixedCaseName=Bill']and then [(name=('bob') and parentId=(4) AND mixedCaseName = 'Bill') AND id = (?), 2]The brackets are to specify that all of the [((name=('bob') and parentId=(4)) AND (mixedCaseName = 'Bill')) AND (id = (?)), 2]That means Author::where(['name' => 'bob', 'parentId=4'])
->where("mixedCaseName='Bill'")
->or->where("name<>'charlie' and name <> 'david'")
->find(2);then converts to: [((name=('bob') and parentId=(4)) AND (mixedCaseName = 'Bill') OR (name <> 'charlie' and name <> 'david')) AND (id = (?)), 2] |
Right on both counts. I'm busily rewriting the sucker, and I came to the same conclusions you did before I even got started. With luck will have an update fairly soon.... |
|
Replaced by pull request #63 |
This is a draft non-breaking solution for #45 based on the Pipes and Filter Pattern. It:
Model::where()function that replacesModel::find(). Everything thatModel::find()used to do can be done byModel::where(), but has a saner and type-checked input parameter list.SQLExecutionPlanTest.phpwhere()will always return a Model, or null if not found, whileall()will always return aModel[], no matter what is passed into the input lists of both functionsTo understand this code:
composer.jsonandTestCommand.php)SQLExecutionPlanTest::testWhereChained()andSQLExecutionPlanTest::testAllChained()to see the beauty of this chaining method, and how it's more like Ruby and easier to use than the old Model::find() and Model::all()Model.phpand noteSQLExecutionPlanis a State class that is repeated passed around for chaining purposes. By splitting this functionality fromModel, a user can setup anSQLExecutionPlanand reuse it among multiple invocations and even cache itTo do if this design is approved:
Model::find()to move that functionality intoSQLExecutionPlan.php.Model::all()has already been rewritten to replaceModel::find()withSQLExecutionPlan::all()Model::find()emit a deprecation notice? Ifwhere()can do all thatfind()can do but is easier to use and is type checked, why havefind()?