propelorm / Propel2

Propel2 is an open-source high-performance Object-Relational Mapping (ORM) for modern PHP
http://propelorm.org/
MIT License
1.26k stars 395 forks source link

Strict Typing #1656

Open oojacoboo opened 4 years ago

oojacoboo commented 4 years ago

I looked around and couldn't find an issue opened for this. I'm sure it's been discussed though. What's the plan for typing the base classes, or offering the option to have strictly typed base classes? Not having the base classes strictly typed presents interfaces issue for us and I'm sure many others. PHP has moved much more in this direction and Propel is really holding us back.

dereuromark commented 3 years ago

You can make a PR to opt in this behavior. Sounds useful for many I am sure.

dereuromark commented 2 years ago

cc @nederdirk

nederdirk commented 2 years ago

I would love this, but I'm not sure if I have time to go ahead and implement this.

dereuromark commented 2 years ago

Maybe we can have a task force of several people? Helping out to get this solved for the next milestone release?

I see at least possibility of several steps

oojacoboo commented 2 years ago

I have a PHP8 compatibility branch that includes some typing updates to be in compatibility with PHP 8.1's typed interfaces. What branch should I target for this?

Additionally, I think we should focus on generated code with a priority over core lib code. The PR I have will cover the core lib for interface compatibility with typing, at least.

I have Propel running fine on 8.1 now.

nederdirk commented 2 years ago

Some thoughts on this:

Other thoughts:

oojacoboo commented 2 years ago

I've submitted PR #1805 which provides support for PHP 8 now. This is really only a step in the right direction. But, all tests are passing and everything is running well on ^8.0.

dereuromark commented 2 years ago

@oojacoboo I think we should for now keep at least last minor of 7.x as well. There is no good reason to jump too quickly. We can still add already all major types everywhere and clean up the API Wherever we use ...|false the API could be fixed to a cleaner nullable one of ?....

dereuromark commented 2 years ago

I started a PR https://github.com/propelorm/Propel2/pull/1821 but I do need help This is a lot, and quite some question marks in there as well regarding future API and what kind of types there should be.

dereuromark commented 2 years ago

Next PR is https://github.com/propelorm/Propel2/pull/1833 I also need a bit of help for that one.

oojacoboo commented 1 year ago

Where do we stand on this? It's actually really bad that you can't use proper interfaces with this lib due to these generated base classes. Interfaces end up being loosely typed, which makes it hard to really move a code-base forward. Being that strict typing is such a large part of PHP today and going forward, this just seems like a really important issue to me.

mringler commented 1 year ago

@oojacoboo Can you explain the issue you are having a bit? I am afraid I don't know what problem you are seeing.

dereuromark commented 1 year ago

It is also important to test/showcase this with latest version or better yet master, as there have been recent updates in that regard.

oojacoboo commented 1 year ago

@mringler it's pretty simple really. You cannot strictly type any interfaces that are implemented by a Propel model, at least for getters/setters defined in the base generated classes. That also limits the typing that can be done by other classes implementing that interface. It all leads back to Propel being stuck with loose typing.

mringler commented 1 year ago

@oojacoboo Do you mean the ActiveRecord interface? I think that is the only interface implemented by model classes. Maybe you could give an example?

oojacoboo commented 1 year ago

@mringler Here is an example:

<?php

interface Widget 
{
    // Cannot properly type this interface
    public function getProduct(): Product;
}

class BaseThingy
{
    /**
     * This phpdoc is the issue - should use proper return typing, or offer the option for loose typing
     * 
     * @return Product
     */
    public function getProduct()
    {
        return new Product();
    }
} 

class Thingy extends BaseThingy implements Widget
{
}

class NonPropelThingy implements Widget
{

    // Cannot properly type this method
    public function getProduct(): Product
    {
        return new Product();
    }
}

Similar issues are had with setters.

mringler commented 1 year ago

Sorry, I don't understand what you are describing. Let's try one more time.

I don't think there are getters in Propel base model classes which themselves instantiate objects (return new Product()). Propel model classes have two kinds of getters. First are those that return model data, i.e. for a book with a title, the method in the base model class looks like this:

    /**
     * Return the string representation of this object
     *
     * @return string The value of the 'title' column
     */
    public function __toString(): string
    {
        return (string)$this->getTitle();
    }

That one looks ok to me, and I think those only return primitive data, so I assume that is not what your getProduct() is.

Then there are getters which resolve objects associated through a relation, like how books have an author. Those look like this:

    /**
     * Get the associated ChildAuthor object
     *
     * @param ConnectionInterface $con Optional Connection object.
     * @return ChildAuthor|null The associated ChildAuthor object.
     * @throws \Propel\Runtime\Exception\PropelException
     */
    public function getAuthor(?ConnectionInterface $con = null)
    {
        if ($this->aAuthor === null && ($this->author_id != 0)) {
            $this->aAuthor = ChildAuthorQuery::create()->findPk($this->author_id, $con);
            /* ...
             */
        }

        return $this->aAuthor;
    }

where ChildAuthor is an aliased type (not sure why, I don't think it is necessary):

use Propel\Tests\Bookstore\Author as ChildAuthor;

Do you mean those getters/setters which resolve relationships, and the problem is the type alias? It looks like you want to write an interface with the model methods, and that is not working? What error are you getting?

oojacoboo commented 1 year ago

@mringler You're looking way too deep into this. It's really not that complicated. Even methods like:

    /**
     * Get the associated ChildAuthor object
     *
     * @param ConnectionInterface $con Optional Connection object.
     * @return ChildAuthor|null The associated ChildAuthor object.
     * @throws \Propel\Runtime\Exception\PropelException
     */
    public function getAuthor(?ConnectionInterface $con = null)
    {
        if ($this->aAuthor === null && ($this->author_id != 0)) {
            $this->aAuthor = ChildAuthorQuery::create()->findPk($this->author_id, $con);
            /* ...
             */
        }

        return $this->aAuthor;
    }

Are problematic for interfaces. That whole passing of a ConnectionInterface is entirely Propel specific and cannot/should not be replicated in an interface. Therefore, it cannot/should not be used in other classes also implementing an interface.

The example I provide about returning a new Product doesn't matter. It could return anything. It was just an example. The issue is when implementing interfaces on Propel models.

Have you ever implemented interfaces on Propel models and used them? Or, have you tried re-implementing a method in a Propel model defined in the base class? Try strictly typing it - it won't work because the method signatures must match. And, because the base classes aren't strictly typed, you're stuck with loose typing.

mringler commented 1 year ago

Sounds like you want to write an interface which can be implemented by several classes, one of which is a propel models:

interface BookInterface{
     public function getAuthor(): AuthorInterface;
}

class PropelBook extends BasePropelBook implements BookInterface{
    ....
}

class SomeOtherBook implements BookInterface{
    public function getAuthor(): AuthorInterface
    {
        return ...
    }
}

This will not work if there is an Author relation on the book table, since Propel will generate methods on the base model class that are not compatible with the signature in BookInterface:

public function getAuthor(?ConnectionInterface $con = null) // method has an argument and no return type

Is the problem you run into?

oojacoboo commented 1 year ago

@mringler Correct, but it's not just relational getters, like in your example. It's an issue for all getters and setters, since they're loosely typed. And, you don't have control over these since they're in the base class.

PHP 7.4 is EOL next week too BTW. PHP 8.0 supports a lot of strict typing features. Most all code being written today is using strict typing - none of this phpdoc type hinting stuff anymore.

mringler commented 1 year ago

Ah, for some reason I got __toString() above, not the actual getter:

    /**
     * Get the [title] column value.
     * Book Title
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

Yes, that one does not have a return type. I think it could be added quite easily though.

But the argument in relational getters will probably never go away. Your quarrel there is with PHP, which does not let you override method signatures in child classes. I think you are going to have to write wrapper classes or fiddler around with the names (which is messy).

oojacoboo commented 1 year ago

My argument with relational getters is a design decision from Propel/ActiveRecord. It has nothing to do with overriding method signatures, if we really want to get down to it. Obviously overriding method signatures is well discussed. But, passing in a connection object is a mess of a design decision. I get that one is not going to go away anytime soon, or ever. That's fine. It is what it is.

However, for all other getters/setters, there is no reason why they cannot/should not be strictly typed at this point. Which is the entire purpose of this issue ticket - adding strict typing to the base classes.

mringler commented 1 year ago

Excellent, I finally understand what your post was about.

As to the types in getters/setters, I think the reason why types were not added in the last iteration is backwards compatibility. If you feel this is no longer an issue, feel free to explain why and make an argument for changing it.

As to the ConnectionInterface argument, I am not really sure if there was a deeper reason to put it there except just exposing the arguments of the underlying call to the query object. Maybe someone else knows what problem it solves, again, feel free to ask and argue why it is there, why you think it should be removed and why it warrants breaking backwards compatibility.

dereuromark commented 1 year ago

I would also say that a PR to showcase the changes and also the implications on the system as a whole would be most productive moving foward. As the current open issues show, we won't progress much without actual changes being done.

oojacoboo commented 1 year ago

@mringler BC can be provided with a setting that allows people to stick with loosely typed phpdoc typehints instead of actual real typing. But typing base classes should absolutely be the default and expected behavior going forward.

As for the ConnectionInterface, yea, I don't know the reason for that other than convenience and lack of proper design planning. IMO, this could be set on the model object as a property through another setter, or possibly using a proxy method, like so:

$foo->useConnection($connection, 'getBars');

@dereuromark where is the logic for the base class generation handled? Do you know?

dereuromark commented 1 year ago

I dont I wonder if we can close this as we moved to strict types where possible now?

oojacoboo commented 1 year ago

@dereuromark Are we closing this and opening another issue ticket? I'm a little taken back by the lack of understanding for the need or the entire purpose of this ticket.

Some of the core code might have some stricter typing, an improvement. But the generated base classes are loosely typed with no option for strict typing.

mringler commented 1 year ago

If in the end I understood @oojacoboo correctly, I believe the open questions are

And of course if somebody can do it (won't be me, I am out of time).

dereuromark commented 1 year ago

Is there a PR we can discuss that would enable this with a feature flag to "enable"? By default disabled? This way we could allow this to be added without a direct BC break to all existing apps. If they solve their issues, they can update to the strict version in their own time table by just switching the feature flag.

mringler commented 1 year ago

enable this with a feature flag

Yes, that is probably the best way to do it. Particularly changing signature of getters substantially breaks BC, as those are overridden quite frequently.

Removing the ConnectionInterface parameter might have consequences beyond breaking type declaration of overrides. The big question is how it impacts setups with multiple databases, and if it impacts how transactions work.

oojacoboo commented 1 year ago

@mringler if possible, we could provide a base model method to set the ConnectionInterface. That base method could potentially take a closure argument, allowing you to pass it for a given call, that way it's not adding to any state.

$foo->withConnection(fn (Foo $foo) => $foo->getBar(), $connection);

As for a PR, some direction would be helpful in knowing where these base classes are generated.

mringler commented 1 year ago

@oojacoboo Look at Propel\Generator\Builder\Om\QueryBuilder and Propel\Generator\Builder\Om\ObjectBuilder. Older methods use a rather convoluted inline format, newer ones use templates, which are located in ./templates/Builder/Om/. Query objects extend Propel\Runtime\ActiveQuery\ModelCriteria and its parent classes, where the base methods like filterBy() are declared.