Skip to content

Convert from Laminas\Db to Doctrine#2233

Merged
demiankatz merged 458 commits intovufind-org:devfrom
demiankatz:doctrine
Aug 19, 2025
Merged

Convert from Laminas\Db to Doctrine#2233
demiankatz merged 458 commits intovufind-org:devfrom
demiankatz:doctrine

Conversation

@demiankatz
Copy link
Member

@demiankatz demiankatz commented Dec 7, 2021

This is work in progress on replacing VuFind's database abstraction layer.

TODO

@demiankatz demiankatz changed the base branch from dev to dev-9.0 December 7, 2021 20:02
@demiankatz
Copy link
Member Author

I spent some more time on proof-of-concept work today. I decided to try revising some of the code in the admin module's tag controller, since that does complex queries and seemed like a potentially interesting starting point. So far, results are promising -- not only is the DQL query language more readable than the Laminas\Db object-oriented query builder, but I also managed to find and fix a bug along the way.

Some notes on today's progress:

1.) We need to have a place to put database-related logic. In the previous code, this was in the row and table classes. In the new code, I propose a set of VuFind\Db\Service classes designed to deal with various types of entities. I've started work on a TagService here, and I've set up the necessary plugin mechanism for interacting with services. This may need more granularity, but we can decide that as we gain more experience working with Doctrine.

2.) For extensibility purposes, we also need a way to abstract entity classes so that they can be overridden. I have created a VuFind\Db\Entity\PluginManager for this purpose, and I defined a marker interface for entities so that the plugin manager can validate what it is creating. Note that overriding entities appears to have one major limitation: annotations are not inherited from parent classes, so any local custom entity needs to fully duplicate its parent's properties and annotations. Perhaps there is a way around this, but given how infrequently I expect people to need to extend/override entities, it may also be a limitation that we can live with.

There is still a massive amount of work that needs to be done here, but I'm encouraged by this start. I will continue to chip away as time permits, and I welcome input or offers of collaboration. Eventually, it may be best to "divide and conquer" on all the conversion work, since there is so much of it.

@demiankatz
Copy link
Member Author

Just a quick update on progress, because I believe this branch is currently in a somewhat broken state:

In order to incorporate Doctrine queries with the Laminas Paginator, I had to add the doctrine/doctrine-orm-module dependency to get an appropriate paginator adapter. In order to install this, I had to upgrade several other dependencies to newer versions, and the upgrade to the latest laminas-cache seems to be causing problems. I worked around this while developing by hacking out the cache logic that was failing, but obviously that needs a proper solution, which I will address in a separate Laminas upgrade PR.

The doctrine/doctrine-orm-module dependency also raises the question of whether we actually need roave/psr-container-doctrine, which may be less desirable if the official Doctrine piece meets our needs. I'll investigate that further when time permits.

Anyway, I've created TODO checkboxes to capture all these balls that are in the air, and I apologize for leaving this in a somewhat broken state, but I didn't want to lose my work in progress, and I'm nearly out of time for today. I'll pick this up again as soon as practical.

@aleksip
Copy link
Contributor

aleksip commented Oct 27, 2022

Hi @demiankatz, this change is very exciting because our Admin Interface has been using doctrine/doctrine-orm-module for a while and it also reads the VuFind database.

I wonder whether it would make sense to develop the new Doctrine based VuFind database layer as a separate module/package, in a similar way that has been discussed in #2272 and #2283 (which I hope to continue working on as soon as time allows)?

In any case it would be great to have this developed in a way that would allow for code reuse, even if only by copying the classes over!

@demiankatz
Copy link
Member Author

Thanks, @aleksip, I'm glad that you're happy with this approach. I'm open to organizing the code any way that is helpful; I think it's just a question of what works best. Perhaps one approach could be to create a VuFindDb module to contain the Entity classes, ConnectionFactory, etc. I'm not sure if the actual Service classes would fit there as well, or if they'd be better off in the main VuFind module due to their potential entanglement with other dependencies. I think I'd have to get deeper into the project to have a stronger opinion about that, but I'm open to input!

In any case, right now, my priority is finishing up all the outstanding work on VuFind 9, so this is more or less on hold, but I'm hoping this will be one of the priority changes for VuFind 10, and I expect I'll be spending a lot of time on it in 2023. I'm definitely happy to discuss/plan in the meantime so we have a clear path forward when it comes time to execute the work. I'm also very open to brainstorming strategies for collaboration if you or anyone on your team might be able to help with the updates, as this is a very big project. I'm hoping that, in addition to updating everything, we can make strides toward introducing better test coverage for core code, since if we do this right, it should make things easier to test than the old database library did.

@aleksip
Copy link
Contributor

aleksip commented Oct 27, 2022

@demiankatz Sounds good! I am currently experimenting with potential vufind-db, vufind-view and vufind-config packages in our Admin Interface, will report on how it goes. 🙂

@demiankatz
Copy link
Member Author

Thanks, @aleksip, that's great! Certainly if you have classes from your admin code that it would make sense to move over here for use in the core, that might save us some time and trouble. I'm not sure if the existing auto-generated Entity classes are actually the best they could be, as most of them have not yet been tested.

Copy link
Member Author

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

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

@aleksip, thanks for the progress on configuration here -- just leaving a couple of comments/questions related to the latest changes.

'service_manager' => [
'allow_override' => true,
'factories' => [
'Doctrine\ORM\Mapping\Driver\AnnotationDriver' => 'VuFind\Db\AnnotationDriverFactory',
Copy link
Member Author

Choose a reason for hiding this comment

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

@aleksip, I see that you deleted this factory, but the configuration is still here. We should clean something up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, forgot to remove that line. Should I push the change to your branch or what would be the best way to work on this together?

Copy link
Member Author

Choose a reason for hiding this comment

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

Please feel free to push directly here, as long as the code still works. I'm not going to be working on this much until next year, so I welcome any progress you can make in the meantime, and I don't anticipate that we'll "step on each other," so to speak.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, if at any point it would be helpful to get a second opinion or anything, or if you would like me to run any tests, just let me know and I'll be happy to help!

Copy link
Contributor

Choose a reason for hiding this comment

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

@demiankatz I meant to push the changes to my repository and then point you there. Not sure how multi-user PRs should work... Should I push future suggested changes to my clone so you can then decide whether to merge to your branch from there?

Copy link
Member Author

Choose a reason for hiding this comment

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

@aleksip, it is possible to make a pull request against a pull request -- you can just target a PR against this branch. That might be the easiest workflow going forward, especially when multi-user collaboration picks up... but since things are quiet right now, feel free to work directly in this branch, at least until you get things into a correct/stable state again. (I think it's easier to just move forward and fix things than to roll back changes and make a separate PR, at least under the present circumstances).

Copy link
Contributor

@aleksip aleksip Nov 2, 2022

Choose a reason for hiding this comment

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

@demiankatz Seems like I can create a PR for the branch in your repository! Should I do that? Edit: missed your above reply.

Copy link
Member Author

Choose a reason for hiding this comment

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

If you want to try it as a proof-of-concept, feel free... but otherwise, as I said, you can just go ahead and push. :-)

@aleksip
Copy link
Contributor

aleksip commented Nov 2, 2022

@demiankatz I have accidentally pushed my changes to your branch, sorry about that! I have been refactoring our Admin Interface database implementation to be based on this PR, and those were the changes I needed to make. They were needed to support two entity managers with connections to different databases.

I don't claim to fully understand how Doctrine's configuration works, but the orm_default naming convention seems a bit problematic in integrations. It would seem better to define an application specific name, and if necessary create an orm_default alias for that.

I also wondered about the need for AnnotationDriverFactory, as it seems that the same result can be achieved with just configuration.

@demiankatz
Copy link
Member Author

@aleksip, I am certainly not an expert on Doctrine's configuration either, and I've been learning as I go... so if the annotation driver can be replaced by pure configuration, I'm entirely in favor of that, and if a different naming convention would be more robust, I also have no objection to changes.

@demiankatz demiankatz changed the base branch from dev to dev-10.0 June 14, 2023 21:09
@demiankatz demiankatz changed the base branch from dev-10.0 to dev October 19, 2023 18:37
@demiankatz demiankatz added the architecture pull requests that involve significant refactoring / architectural changes label Nov 7, 2023
@demiankatz
Copy link
Member Author

demiankatz commented Jan 23, 2024

I spent some time today working on PostgreSQL compatibility. The test suite is now in parity with the dev branch when running against PostgreSQL (there is one known bug causing a single test to fail in both places).

@demiankatz
Copy link
Member Author

It might be possible to write a regular test and use the SchemaValidator class. I'm not sure if I will have time to work on this in the near future, unfortunately.

Thanks for that pointer -- when time permits, I'll see if I can do something with that! If anyone else has time in the meantime, please just let me know so we can coordinate our work.

$parameters['includeFilter'] = $includeFilter;
}
if (!empty($excludeFilter)) {
$where[] = 'ul NOT IN (:excludeFilter)';
Copy link
Contributor

Choose a reason for hiding this comment

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

Might have noticed a small error here, but not totally sure. Here the list is being compared and in include filter its comparing the id.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this definitely merits deeper testing. I'll try to find time to take a closer look soon!

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 did some experimentation and found that the syntax is equivalent -- whether you use ul or ul.id, the query works correctly when you pass in an array of integers or an array of entity objects. But to make this more consistent, I've pushed a commit to use ul in both places so it's less confusing!

@demiankatz
Copy link
Member Author

demiankatz commented Jun 30, 2025

It might be possible to write a regular test and use the SchemaValidator class. I'm not sure if I will have time to work on this in the near future, unfortunately.

It's definitely possible -- I've created a proof-of-concept DoctrineSchemaValidationTest.php in another branch. The problem is that I don't really understand how to fix the errors it is revealing. It seems that Doctrine wants to drop and recreate many of the indexes; I'm not sure why it doesn't recognize the existing ones as being valid and correct. I imagine some of this may be related to naming issues, but I don't think that's the whole story. I also strongly suspect there will be additional challenges getting this test to pass consistently in both MySQL and PostgreSQL. Any ideas?

@demiankatz
Copy link
Member Author

Okay, thanks to @sambhavp96's excellent work, several schema inconsistencies have been sorted out, and the Doctrine schema validation test I developed is now passing. See f8526d2 for details.

demiankatz added a commit to sambhavp96/vufind that referenced this pull request Jul 29, 2025
@LuomaJuha LuomaJuha mentioned this pull request Jul 30, 2025
8 tasks
@demiankatz demiankatz requested a review from ThoWagen August 18, 2025 13:20
@demiankatz demiankatz merged commit 3d2be48 into vufind-org:dev Aug 19, 2025
6 checks passed
@demiankatz demiankatz deleted the doctrine branch August 19, 2025 11:47
public function getSearchById(int $id): ?SearchEntityInterface
{
return $this->getDbTable('search')->select(['id' => $id])->current();
return $this->entityManager->find(SearchEntityInterface::class, $id);
Copy link
Contributor

Choose a reason for hiding this comment

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

@demiankatz, this line raises a runtime exception in my local test installation:

Ausnahme: Doctrine\Persistence\Mapping\MappingException Class 'VuFind\Db\Entity\SearchEntityInterface' does not exist

Should it be Search::class or \VuFind\Db\Entity\Search::class here (and in the rest of the file)?

Copy link
Member Author

Choose a reason for hiding this comment

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

The SearchEntityInterface is defined in https://github.com/vufind-org/vufind/blob/dev/module/VuFind/src/VuFind/Db/Entity/SearchEntityInterface.php, and SearchEntityInterface is set up as an alias in the PluginManager, so I'm not sure why you are encountering a problem. I would expect the code to work as written (and indeed, if it didn't, I would expect some of our integration tests -- particularly SavedSearchesTest -- to fail).

I'll try to find time to double-check this more carefully in my own test environment when I'm back in the office tomorrow. In the meantime, is there anything unusual about your environment? Can you specify what steps you're taking to raise the exception? Are you able to run integration tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

I get more similar exceptions ("Class 'VuFind\Db\Entity\ResourceEntityInterface' does not exist" and others).

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 the whole system fail to work, or are you just seeing these types of errors occasionally? What PHP version are you using? Is there any possibility that your Composer dependencies are out of date or that there's something odd in your cache?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that macOS is a very common environment for VuFind tests, so yes, that's unusual. The integration tests (vendor/bin/phing qa-php) show lots of messages like Unable to connect to localhost:80 (Connection refused), so it is also unusual that the Homebrew Apache listens on port 8080.

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 believe that @EreMaijala has had success getting things working in the environment, so he might have some advice. Have you seen the notes on the testing wiki page?

Copy link
Contributor

@EreMaijala EreMaijala Oct 6, 2025

Choose a reason for hiding this comment

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

That exception happens at runtime if you catch all exceptions in a debugger, but does not affect functionality. I suppose the mechanism is trying to look for a corresponding class but doesn't find it for an interface.

Copy link
Contributor

@stweil stweil Oct 6, 2025

Choose a reason for hiding this comment

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

I catch "everything" (which includes all exceptions) in my debugger because it typically indicates errors or at least weaknesses in the code. Here an AI chatbot also says that looking for a class with the name of the interface is wrong, and the name of the class which is derived from the interface should be passed instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

@stweil, it is conventional when using Laminas service managers to name services for interfaces, to make it easier to install local custom versions without having to alias concrete classes in potentially confusing ways. That is the pattern that has been followed here, and it is set up in our custom EntityManagerFactory. This may be a conflict between Laminas practices and Doctrine practices, but it was an intentional choice based on Laminas practice.

I don't really have strong feelings about this one way or the other; I think the likelihood of locally overriding entities is relatively small. However, it is a fairly large change to make this close to a major release, so I would be hesitant to change it further unless there's a very strong reason to do so. Maybe the thing worth investigating further is why exceptions are being thrown and caught when aliases are properly set up -- but I imagine that would require a deep dive into the Doctrine code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

architecture pull requests that involve significant refactoring / architectural changes improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants