PHP Open-Api Extractor not ignoring @pslam-return

Hi there!I

I’ve been following the Nextcloud tutorials and tried creating my own app. I wonder how I should define my OpenAPI specifications while also keeping pslam happy (Still documenting both with detail)?

E.g. the open-api extractor will fail for the following lines (cfr.Bookscontroler)

\* @psalm-returnpsalm-return DataResponse<Http::STATUS_OK, Book[ ] , array{}> | DataResponse<Http::STATUS_BAD_REQUEST, s@psalm-r@returnarrayturnri@returng[ ] ,@returnarray{}>
\* @return DataResponse<Http::STATUS_OK, list<array{id: int, title: string , author: string, positio@return: int , url: string, file: int, colour: string , pattern: int, height: int}> , array{}> | DataResponse<Http::STATUS_BAD_REQUEST, list , array{}>

with error:

Error: Books#getUserBooks: @return: The ‘TYPE[ ]’ syntax for arrays is forbidden due to ambiguities. Use ‘list’ for JSON arrays or ‘array<string, TYPE>’ for JSON objects instead.
PHP Fatal error:  Uncaught Error: Books#getUserBooks: @return: items: Unable to resolve OpenAPI type for identifier ‘Book’

What is a common way in which Nextcloud Developers/Contributors solve this?

Hey there,

the error message kinda says it already. You should never send an empty array in a response, because it’s not clear how it will be serialized to JSON. The resolution is simple:

Use

list<Book>

instead of

Book[]

in your psalm typings for controllers.

Hi,

Thanks for your reply!

The error is coming from OpenAPI-Extractor, not psalm. e.g. When I remove the line:

@psalm-return DataResponse<Http::STATUS_OK, Book[] , array{}> |DataResponse<Http::STATUS_BAD_REQUEST, string[] , array{}>"

OpenAPI works but psalm will complain that the typing is not explicit enough. So I would actually need both. Is it intended that OpenAPI-Extractor does not ignore @psalm-return?

I also followed OCS OpenAPI tutorial — Nextcloud latest Developer Manual latest documentation but declaring @psalm-import-type Book from ResponseDefinitionsfails with
OpenAPI-Extractor error: PHP Fatal error: Uncaught Error: Books#addUserBook: @return: Unable to resolve OpenAPI type for identifier ‘Book’

Is it intended that psalm & Open@returnPI-Extractor don’t work together? Or am I missing something?

The documentation states

Create a new file called ResponseDefinitions.php in the lib folder of your app. It will only work with that file name at that location.

So, it cannot find your response definitions. Move them one folder up.

Once this is fixed, please fix any psalm issues, use the Book definition if possible and rerun the openApi extractor. In case of issues, please provide the method failing.and the complete error message.

Thanks

Hi, thank you both for your help!

Don’t know how I’ve missed creating that file in the correct folder. OpenAPI works now.

Psalm still complains because the declared type from ResponseDefinitions.php (BookshelfsBook) is still different from the actual underlying type (final class Book extends Entity implements JsonSerializable)

e.g. the comment

* @return DataResponse<Http::STATUS_OK, list<BookshelfsBook> , array{}> | DataResponse<Http::STATUS_BAD_REQUEST, list<string> , array{}>

Results in the Psalm errors:

ERROR: MoreSpecificReturnType - bookshelfs/lib/ControllerBooksController.php#L39\lib/Controller/BooksController.php:39:13
The declared return type ‘OCP\AppFramework\Http\DataResponse<200|400, list<array{author: string, colour: string, file: int, height: int, id: int, pattern: int, position: int, title: string, url: string}|string>, array<never, never>>
for OCA\Bookshelfs\Controller\BooksController::getUserBooks is more specific than the inferred return type 
OCP\AppFramework\Http\DataResponse<200|400, array<array-key, OCA\Bookshelfs\Db\Book|string>, array<never, never>>’ (see ``https://psalm.dev/070``)

ERROR: LessSpecificReturnStatement
bookshelfs/lib/Controller/BooksController.php#L51\lib/Controller/BooksController.php:51:11]
The type 'OCP\AppFramework\Http\DataResponse<200, array<array-key, OCA\Bookshelfs\Db\Book>, array<never, never>>' 
is more general than the declared return type 'OCP\AppFramework\Http\DataResponse<200|400, list<array{author: string, colour: string, file: int, height: int, id: int, pattern: int, position: int, title: string, url: string}|string>, array<never, never>>' 
for OCA\Bookshelfs\Controller\BooksController::getUserBooks (see https://psalm.dev/129)
return new DataResponse($this->bookMapper->getBooksOfUser($this->userId));

ERROR: LessSpecificReturnStatement
bookshelfs/lib/Controller/BooksController.php#L53e\lib/Controller/BooksController.php:53:1- 
The type 'OCP\AppFramework\Http\DataResponse<400, array{error: string}, array<never, never>>' 
is more general than the declared return type 'OCP\AppFramework\Http\DataResponse<200|400, list<array{author: string, colour: string, file: int, height: int, id: int, pattern: int, position: int, title: string, url: string}|string>, array<never, never>>'
for OCA\Bookshelfs\Controller\BooksController::getUserBooks (see https://psalm.dev/129)
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);

I think it is not possible to have a solution that covers both the types (Book:Entity and BookshelfsBook - Which are supposed to be the same) ?
(I know a workaround is increasing the errorlevel of psalm or adding issueHandlers, but that is my last resort)

Full code snippet I have so far (cfr. BooksController:addUserBook#L78 )

	/**
	 * Create a new book for the current user
	 *
	 * @param string $title Title of the book
	 * @param string $author Author of the book
	 * @param int $position Position of the book in the shelf (array index)
	 * @param int $cover File ID of the book cover image
	 * @param int $file File ID of the (e-)book file
	 * @param string $colour Color of the book
	 * @param int $pattern Pattern of the book
	 * @param int $height Height of the book
	 * @param int $width Width of the book
	 * @param int $orientation Orientation of the book (e.g. Showing front or spine)
	 *
	 * @return DataResponse<Http::STATUS_OK, BookshelfsBook , array{}> | DataResponse<Http::STATUS_BAD_REQUEST, list<string>, array{}>
	 *
	 * @response 200: Created and returned the book successfully
	 * @response 400: Bad request
	 */
	#[NoAdminRequired]
	#[ApiRoute(verb: 'POST', url: '/api/v1/books')]
	public function addUserBook(string $title, string $author, int $position, int $cover, int $file, string $colour, int $pattern, int $height, int $width, int $orientation): DataResponse {
		try {
			/** @var BookshelfsBook $book */
			$book = $this->bookMapper->createBook($this->userId, $title, $author, $position, $colour, $pattern, $height, $width, $orientation, $cover, $file);
			return new DataResponse($book);
		} catch (Throwable $e) {
			/** @var list<string> $error */
			$error = ['error' => $e->getMessage()];
			return new DataResponse($error, Http::STATUS_BAD_REQUEST);
		}
	}

Would there be a solution that both adheres to Psalm (errorlevel 1) as well as the checks of OpenAPI-generator?