Skip to content

Swagger on inherited entities generates strange results #2671

Description

@zalesak

I have Entity user:

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
use Doctrine\ORM\Mapping\InheritanceType;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * User entity.
 *
 * @ApiResource(
 *     attributes={
 *         "order"={"lastname"="ASC"}
 *     },
 *     collectionOperations={
 *     },
 *     itemOperations={
 *         "get"={
 *              "normalization_context"={"groups"={"user_read"}}
 *         }
 *     }
 *  )
 *
 * @ORM\Entity
 * @ORM\Table(name="user")
 *
 * @InheritanceType("JOINED")
 * @DiscriminatorColumn(name="type", type="integer")
 * @DiscriminatorMap({0 = "User", 1 = "Doctor"})
 */
class User
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned"=true})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     *
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="firstname", type="string", length=255, nullable=false)
     *
     * @Groups({"user_read"})
     */
    private $firstname;

    /**
     * @var string
     *
     * @ORM\Column(name="lastname", type="string", length=255, nullable=false)
     *
     * @Groups({"user_read"})
     */
    private $lastname;

    /**
     * @var string
     *
     * @ORM\Column(name="phone", type="string", length=255, nullable=false)
     *
     * @Groups({"user_read"})
     */
    private $phone;

    /**
     * @var string|null
     *
     * @ORM\Column(name="email", type="string", length=255, nullable=false)
     *
     * @Groups({"user_read"})
     */
    private $email;

     /**
     * @var string
     *
     * @ORM\Column(name="password", type="string", length=255, nullable=false)
     */
    private $password;

    /**
     * @var bool
     *
     * @ORM\Column(name="active", type="boolean", nullable=false)
     *
     * @Groups({"user_read"})
     */
    private $active = true;

// getters and setters...
}

And inherited entity Doctor:

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * Doctor entity.
 *
 * @ApiResource(
 *     attributes={
 *         "order"={"lastname"="ASC"}
 *     },
 *     collectionOperations={},
 *     itemOperations={
 *         "get"={
 *              "normalization_context"={"groups"={"doctor_read"}}
 *         }
 *     }
 *  )
 *
 * @ORM\Entity
 * @ORM\Table(name="doctor")
 */
class Doctor extends User
{
    /**
     * @var string|null
     *
     * @ORM\Column(name="title_before", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $titleBefore;

    /**
     * @var string|null
     *
     * @ORM\Column(name="title_after", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $titleAfter;

    /**
     * @var string|null
     *
     * @ORM\Column(name="work_name", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $workName;

    /**
     * @var string|null
     *
     * @ORM\Column(name="work_street", type="string", length=255, nullable=true)
     *
     * @Groups({"read", "write"})
     *
     * @Groups({"doctor_read"})
     */
    private $workStreet;

    /**
     * @var string|null
     *
     * @ORM\Column(name="work_zip", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $workZip;

    /**
     * @var string|null
     *
     * @ORM\Column(name="work_city", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $workCity;

    /**
     * @var string|null
     *
     * @ORM\Column(name="work_phone", type="string", length=255, nullable=true)
     *
     * @Groups({"doctor_read"})
     */
    private $workPhone;

    /**
     * @var bool
     *
     * @ORM\Column(name="signature", type="boolean", nullable=false)
     */
    private $signature = false;

// getters and setters...
}

There is api:swagger:export results:

swagger: '2.0'
basePath: /
info:
  title: ''
  version: 1.0.0
paths:
  '/api/doctors/{id}':
    get:
      tags:
        - Doctor
      operationId: getDoctorItem
      produces:
        - application/ld+json
        - application/json
        - text/html
      summary: 'Retrieves a Doctor resource.'
      parameters:
        -
          name: id
          in: path
          required: true
          type: string
      responses:
        200:
          description: 'Doctor resource response'
          schema:
            $ref: '#/definitions/Doctor-doctor_read'
        404:
          description: 'Resource not found'
  '/api/users/{id}':
    get:
      tags:
        - User
      operationId: getUserItem
      produces:
        - application/ld+json
        - application/json
        - text/html
      summary: 'Retrieves a User resource.'
      parameters:
        -
          name: id
          in: path
          required: true
          type: string
      responses:
        200:
          description: 'User resource response'
          schema:
            $ref: '#/definitions/User-user_read'
        404:
          description: 'Resource not found'
securityDefinitions:
  apiKey:
    type: apiKey
    in: header
    description: 'Value for the Authorization: Bearer header'
    name: 'Authorization: Bearer'
security:
  -
    apiKey: []
definitions:
  Doctor-doctor_read:
    type: object
    description: 'Doctor entity.'
    properties:
      titleBefore:
        type: string
      titleAfter:
        type: string
      workName:
        type: string
      workStreet:
        type: string
      workZip:
        type: string
      workCity:
        type: string
      workPhone:
        type: string
  User-user_read:
    type: object
    description: 'User entity.'
    properties:
      firstname:
        type: string
      lastname:
        type: string
      phone:
        type: string
      email:
        type: string
      active:
        type: boolean
      titleBefore:
        type: string
      titleAfter:
        type: string
      workName:
        type: string
      workStreet:
        type: string
      workZip:
        type: string
      workCity:
        type: string
      workPhone:
        type: string
      signature:
        type: boolean
      id:
        readOnly: true
        type: integer
      password:
        type: string

As you can see, User-user_read object has all properties from Doctor-doctor_read, even signature property, which is not in scope of get operation on Doctor entity (not in group doctor_read).

I think, problem is in ApiPlatform\Core\Metadata\Property\Factory\InheritedPropertyNameCollectionFactory line 54:

            if (is_subclass_of($knownResourceClass, $resourceClass)) {
                foreach ($this->create($knownResourceClass) as $propertyName) {
                    $propertyNames[$propertyName] = $propertyName;
                }
            }

First of all, its completely ignore groups, so there is first correction:

                foreach ($this->create($knownResourceClass, $options) as $propertyName) {

And I am not sure why there is this part of code at all.

If I have resource User, I don`t expect data from resource Doctor , even when I want to get all users and in collection will be some doctors as well. Am I wrong?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions