Skip to content

Conversation

@norkunas
Copy link
Collaborator

@norkunas norkunas commented Nov 20, 2025

Pull Request

Related issue

Fixes #744

What does this PR do?

  • Returns stats object instead of untyped array;

PR checklist

Please check if your PR fulfills the following requirements:

  • Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)?
  • Have you read the contributing guidelines?
  • Have you made sure that the title is accurate and descriptive of the changes?

Summary by CodeRabbit

  • New Features

    • Statistics API now returns structured objects with detailed database and index metrics (document counts, storage sizes, avg document size, indexing status, embedding counts, field distributions, and last update timestamps).
  • Bug Fixes

    • Improved validation and clearer error handling when statistics payloads are malformed or unexpected.
  • Tests

    • Added unit and integration tests for constructing and parsing statistics objects and for end-to-end stats assertions.

✏️ Tip: You can customize this high-level summary in your review settings.

@norkunas norkunas added enhancement New feature or request breaking-change The related changes are breaking for the users labels Nov 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

Adds two final readonly value objects, IndexStats and Stats, with factories and getters, and updates HandlesSystem::stats() to return a Stats contract/object instead of an array. Adds tests for the new contracts and adjusts integration tests to use the getters and new behavior.

Changes

Cohort / File(s) Summary
New Contracts
src/Contracts/IndexStats.php, src/Contracts/Stats.php
Add final readonly value objects representing per-index and aggregate stats. Each has typed readonly properties, getters, and a static fromArray(array $data): self factory (including DateTimeImmutable parsing and index -> IndexStats conversion).
Updated System Handler
src/Endpoints/Delegates/HandlesSystem.php
Change stats() return type from array to a Stats/StatsContract object; validate that backend response is an array, convert via Stats::fromArray() (or contract factory), and throw LogicException when response is not an array.
Contract Tests
tests/Contracts/StatsTest.php
Add PHPUnit tests for constructing Stats and nested IndexStats via constructor and fromArray(), asserting all getters and nested field distributions.
Integration Tests
tests/Endpoints/ClientTest.php
Update testStats() to assert via getDatabaseSize(), getUsedDatabaseSize(), and per-index getNumberOfDocuments() / getFieldDistribution(); reorder exception expectations in testBadClientUrl() and testHeaderWithoutApiKey() to target the stats() call.

Sequence Diagram(s)

sequenceDiagram
    participant App as Client/Application
    participant Handler as HandlesSystem::stats()
    participant API as Meilisearch API
    participant Stats as Stats (fromArray)
    participant Index as IndexStats (fromArray)

    App->>Handler: stats()
    activate Handler
    Handler->>API: GET /stats
    activate API
    API-->>Handler: raw array
    deactivate API

    alt response is array
        Handler->>Stats: Stats::fromArray(array)
        activate Stats
        Stats->>Index: IndexStats::fromArray(...) for each index
        activate Index
        Index-->>Stats: IndexStats instances
        deactivate Index
        Stats-->>Handler: Stats instance
        deactivate Stats
        Handler-->>App: Stats object
    else response invalid
        Handler-->>App: throws LogicException
    end
    deactivate Handler
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review Stats::fromArray() and IndexStats::fromArray() for correct type coercion and DateTimeImmutable parsing.
  • Verify HandlesSystem::stats() signature change and imports (Stats/StatsContract, LogicException) and callers updated.
  • Check tests for proper indexing wait/teardown and assertions matching new getters.

"I hopped through arrays to make them neat,
New stats in paw, both tidy and sweet.
From raw to object I bound each part,
Field counts sparkling — a rabbit's art. 🐇"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.52% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly describes the main change: adding a typed Stats object to replace untyped arrays in the API.
Linked Issues check ✅ Passed The PR fully satisfies issue #744 by adding a return typehint to HandlesSystem::stats that returns a typed Stats object.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing typed Stats object support. No out-of-scope modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1832c65 and b781ae9.

📒 Files selected for processing (5)
  • src/Contracts/IndexStats.php (1 hunks)
  • src/Contracts/Stats.php (1 hunks)
  • src/Endpoints/Delegates/HandlesSystem.php (2 hunks)
  • tests/Contracts/StatsTest.php (1 hunks)
  • tests/Endpoints/ClientTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/Endpoints/ClientTest.php
  • tests/Contracts/StatsTest.php
  • src/Endpoints/Delegates/HandlesSystem.php
🧰 Additional context used
🧬 Code graph analysis (2)
src/Contracts/IndexStats.php (1)
src/Contracts/Stats.php (1)
  • fromArray (59-67)
src/Contracts/Stats.php (1)
src/Contracts/IndexStats.php (3)
  • __construct (17-26)
  • fromArray (92-103)
  • IndexStats (7-104)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Upload coverage to Codecov
🔇 Additional comments (6)
src/Contracts/Stats.php (3)

7-20: LGTM! Clean value object implementation.

The readonly properties and constructor property promotion are used correctly. The type declarations align with the docblock annotations.


22-49: LGTM! Getters correctly expose readonly properties.

All getter methods properly return their corresponding properties with correct type declarations.


59-67: Well-implemented factory method.

The fromArray method correctly constructs the Stats object from API response data. The null-handling for lastUpdate and the transformation of nested indexes via IndexStats::fromArray are both implemented correctly.

Note: The method assumes all required keys exist in the input array (fail-fast on contract violations), which is a reasonable design choice for API response objects.

src/Contracts/IndexStats.php (3)

7-26: LGTM! Well-structured value object.

The constructor correctly uses readonly property promotion with appropriate types. The seven properties comprehensively represent index statistics.


28-79: LGTM! Complete getter coverage with correct types.

All seven getters properly expose the readonly properties. The isIndexing() method follows the conventional naming pattern for boolean getters.


92-103: Factory method correctly implemented.

The fromArray method properly constructs an IndexStats instance by mapping all seven fields from the input array. The implementation is consistent with the Stats::fromArray approach, using a fail-fast pattern for invalid input.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 75.00000% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.97%. Comparing base (0078a8c) to head (b781ae9).
⚠️ Report is 85 commits behind head on main.

Files with missing lines Patch % Lines
src/Contracts/IndexStats.php 58.33% 10 Missing ⚠️
src/Endpoints/Delegates/HandlesSystem.php 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #823      +/-   ##
==========================================
- Coverage   89.78%   87.97%   -1.82%     
==========================================
  Files          59       80      +21     
  Lines        1449     1721     +272     
==========================================
+ Hits         1301     1514     +213     
- Misses        148      207      +59     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d90618 and 2c9178f.

📒 Files selected for processing (5)
  • src/Contracts/IndexStats.php (1 hunks)
  • src/Contracts/Stats.php (1 hunks)
  • src/Endpoints/Delegates/HandlesSystem.php (2 hunks)
  • tests/Contracts/StatsTest.php (1 hunks)
  • tests/Endpoints/ClientTest.php (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-17T06:57:43.914Z
Learnt from: norkunas
Repo: meilisearch/meilisearch-php PR: 735
File: src/Contracts/SimilarDocumentsQuery.php:10-12
Timestamp: 2025-11-17T06:57:43.914Z
Learning: In the meilisearch-php project, docblock type annotations (such as `non-empty-string`, `positive-int`, etc.) are treated as contracts for static analysis tools rather than requiring runtime validation. If users violate these documented constraints, it's their responsibility.

Applied to files:

  • src/Contracts/IndexStats.php
🧬 Code graph analysis (5)
src/Endpoints/Delegates/HandlesSystem.php (3)
src/Contracts/Stats.php (2)
  • Stats (7-68)
  • fromArray (59-67)
src/Exceptions/LogicException.php (1)
  • LogicException (7-9)
src/Contracts/Endpoint.php (1)
  • show (19-22)
tests/Endpoints/ClientTest.php (7)
tests/TestCase.php (2)
  • createEmptyIndex (131-136)
  • safeIndexName (138-141)
src/Endpoints/Delegates/HandlesDocuments.php (1)
  • addDocuments (46-49)
src/Endpoints/Delegates/HandlesSystem.php (2)
  • stats (43-52)
  • health (22-25)
src/Contracts/Stats.php (3)
  • getDatabaseSize (25-28)
  • getUsedDatabaseSize (33-36)
  • getIndexes (46-49)
src/Contracts/IndexStats.php (2)
  • getNumberOfDocuments (31-34)
  • getFieldDistribution (76-79)
src/Client.php (1)
  • Client (34-72)
src/Exceptions/ApiException.php (1)
  • ApiException (9-95)
tests/Contracts/StatsTest.php (2)
src/Contracts/IndexStats.php (3)
  • IndexStats (7-104)
  • isIndexing (52-55)
  • fromArray (92-103)
src/Contracts/Stats.php (6)
  • Stats (7-68)
  • getDatabaseSize (25-28)
  • getUsedDatabaseSize (33-36)
  • getLastUpdate (38-41)
  • getIndexes (46-49)
  • fromArray (59-67)
src/Contracts/Stats.php (1)
src/Contracts/IndexStats.php (3)
  • __construct (17-26)
  • fromArray (92-103)
  • IndexStats (7-104)
src/Contracts/IndexStats.php (1)
src/Contracts/Stats.php (2)
  • __construct (14-20)
  • fromArray (59-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Seer Code Review
🔇 Additional comments (6)
src/Contracts/IndexStats.php (1)

7-104: LGTM!

The IndexStats value object is well-structured with proper readonly properties, getters, and a factory method. The implementation follows PHP best practices and provides type safety through constructor property promotion.

src/Endpoints/Delegates/HandlesSystem.php (1)

43-52: LGTM - Validation ensures type safety.

The validation correctly guards against both null and non-array responses from the API. This is stricter than other methods in the trait (e.g., health() returns ?array directly), but aligns with the breaking-change nature of this PR to enforce typed returns.

tests/Contracts/StatsTest.php (1)

13-71: LGTM!

The tests provide good coverage for both direct construction and the fromArray factory method, including validation of nested IndexStats objects.

tests/Endpoints/ClientTest.php (2)

211-235: LGTM!

The test correctly validates the new typed Stats API by creating an index, adding documents, and asserting on the structured Stats object properties. The dynamic lookup for the index by prefix is appropriate given the randomized index names.


237-256: LGTM!

The test reorganization improves clarity by establishing the exception expectation after client creation in testBadClientUrl, and by adding a health check before the stats call in testHeaderWithoutApiKey.

src/Contracts/Stats.php (1)

7-68: LGTM!

The Stats value object is well-implemented with proper readonly properties, getters, and a factory method. The fromArray method correctly handles nullable lastUpdate by converting it to DateTimeImmutable when present, and properly maps nested index data through IndexStats::fromArray.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/Contracts/Stats.php (2)

7-20: LGTM!

The class structure follows best practices for value objects with readonly properties and clear type constraints in PHPDoc for static analysis.

Consider adding runtime validation in the constructor to enforce non-negative constraints on databaseSize and usedDatabaseSize:

 public function __construct(
     private readonly int $databaseSize,
     private readonly int $usedDatabaseSize,
     private readonly ?\DateTimeImmutable $lastUpdate,
     private readonly array $indexes,
 ) {
+    if ($databaseSize < 0 || $usedDatabaseSize < 0) {
+        throw new \InvalidArgumentException('Database sizes must be non-negative');
+    }
 }

This would catch invalid data at construction time rather than relying solely on PHPDoc annotations. However, this is optional and may not be necessary if the API always returns valid data.


59-67: LGTM!

The factory method correctly converts API response data into typed objects. The DateTimeImmutable conversion and IndexStats mapping logic is sound.

Consider adding defensive validation to handle malformed API responses more gracefully:

 public static function fromArray(array $data): self
 {
+    if (!isset($data['databaseSize'], $data['usedDatabaseSize'], $data['indexes'])) {
+        throw new \InvalidArgumentException('Missing required keys in stats data');
+    }
+    
+    if (!is_array($data['indexes'])) {
+        throw new \InvalidArgumentException('Indexes must be an array');
+    }
+
     return new self(
         $data['databaseSize'],
         $data['usedDatabaseSize'],
         null !== $data['lastUpdate'] ? new \DateTimeImmutable($data['lastUpdate']) : null,
         array_map(static fn (array $v) => IndexStats::fromArray($v), $data['indexes']),
     );
 }

This would provide clearer error messages if the API returns unexpected data. However, this is optional and depends on whether you want to fail fast with clear errors or trust the API contract.

src/Contracts/IndexStats.php (1)

92-103: LGTM!

The factory method correctly maps array data to constructor parameters. The implementation is straightforward and consistent with Stats::fromArray().

Consider adding defensive validation to match the optional suggestion for Stats::fromArray():

 public static function fromArray(array $data): self
 {
+    $required = ['numberOfDocuments', 'rawDocumentDbSize', 'avgDocumentSize', 
+                 'isIndexing', 'numberOfEmbeddings', 'numberOfEmbeddedDocuments', 
+                 'fieldDistribution'];
+    
+    foreach ($required as $key) {
+        if (!array_key_exists($key, $data)) {
+            throw new \InvalidArgumentException("Missing required key: {$key}");
+        }
+    }
+
     return new self(
         $data['numberOfDocuments'],
         $data['rawDocumentDbSize'],
         $data['avgDocumentSize'],
         $data['isIndexing'],
         $data['numberOfEmbeddings'],
         $data['numberOfEmbeddedDocuments'],
         $data['fieldDistribution'],
     );
 }

This would provide clearer error messages for malformed data. However, this is optional and depends on your error handling strategy.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c9178f and 1832c65.

📒 Files selected for processing (5)
  • src/Contracts/IndexStats.php (1 hunks)
  • src/Contracts/Stats.php (1 hunks)
  • src/Endpoints/Delegates/HandlesSystem.php (2 hunks)
  • tests/Contracts/StatsTest.php (1 hunks)
  • tests/Endpoints/ClientTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Contracts/StatsTest.php
🧰 Additional context used
🧬 Code graph analysis (4)
src/Endpoints/Delegates/HandlesSystem.php (2)
src/Contracts/Stats.php (2)
  • Stats (7-68)
  • fromArray (59-67)
src/Exceptions/LogicException.php (1)
  • LogicException (7-9)
tests/Endpoints/ClientTest.php (3)
src/Endpoints/Indexes.php (1)
  • stats (240-243)
src/Contracts/Stats.php (3)
  • getDatabaseSize (25-28)
  • getUsedDatabaseSize (33-36)
  • getIndexes (46-49)
src/Contracts/IndexStats.php (2)
  • getNumberOfDocuments (31-34)
  • getFieldDistribution (76-79)
src/Contracts/IndexStats.php (1)
src/Contracts/Stats.php (2)
  • __construct (14-20)
  • fromArray (59-67)
src/Contracts/Stats.php (1)
src/Contracts/IndexStats.php (3)
  • __construct (17-26)
  • fromArray (92-103)
  • IndexStats (7-104)
🔇 Additional comments (8)
src/Endpoints/Delegates/HandlesSystem.php (2)

7-7: LGTM!

The new imports are necessary to support the typed Stats return value and validation error handling.

Also applies to: 13-13


43-52: LGTM!

The implementation correctly converts the raw stats array into a strongly-typed StatsContract object. The defensive validation at line 47 is good practice for runtime safety.

tests/Endpoints/ClientTest.php (3)

211-235: LGTM!

The test properly validates the new typed Stats API by:

  1. Creating test data with known field distribution
  2. Verifying database size getters work correctly
  3. Confirming index stats are accessible via the typed interface
  4. Asserting the correct document count and field distribution

237-244: LGTM!

The reordering improves test clarity by setting up the client before declaring exception expectations.


246-256: LGTM!

The reordering improves test flow by clearly separating the successful health check from the expected stats() failure.

src/Contracts/Stats.php (1)

22-49: LGTM!

The getter methods correctly expose readonly properties with proper type annotations for static analysis.

src/Contracts/IndexStats.php (2)

7-26: LGTM!

The class structure follows the same value object pattern as Stats, with readonly properties and comprehensive type constraints for static analysis.


28-79: LGTM!

The getter methods are correctly implemented with consistent naming conventions (including the conventional isIndexing() for the boolean property) and proper type annotations.

Copy link
Collaborator

@Strift Strift left a comment

Choose a reason for hiding this comment

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

Great 🙌

@Strift Strift added this pull request to the merge queue Nov 24, 2025
Merged via the queue into meilisearch:main with commit dfac2d4 Nov 24, 2025
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change The related changes are breaking for the users enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typehint HandlesSystem::stats return type

2 participants