Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [],
"deny": []
}
"permissions": {
"defaultMode": "acceptEdits",
"allow": [],
"deny": []
},
"enabledMcpjsonServers": [
"playwright"
],
"enableAllProjectMcpServers": true
}
12 changes: 12 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": [
"@playwright/mcp@latest"
],
"env": {}
}
}
}
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,86 @@ $bundles = [
Make sure you add it before `SyliusGridBundle`, otherwise you'll get
`You have requested a non-existent parameter "setono_sylius_review.model.review_request.class".` exception.

### Extend the ProductReview entity

This plugin extends Sylius's ProductReview entity with additional fields. You need to create your own ProductReview entity that implements the plugin's interface and uses its trait.

Create `src/Entity/ProductReview.php`:

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Setono\SyliusReviewPlugin\Model\ProductReviewInterface;
use Setono\SyliusReviewPlugin\Model\ProductReviewTrait;
use Setono\SyliusReviewPlugin\Model\ReviewInterface;
use Sylius\Component\Core\Model\ProductReview as BaseProductReview;
use Sylius\Component\Order\Model\OrderInterface;

#[ORM\Entity]
#[ORM\Table(name: 'sylius_product_review')]
class ProductReview extends BaseProductReview implements ProductReviewInterface
{
use ProductReviewTrait;

#[ORM\ManyToOne(targetEntity: OrderInterface::class)]
#[ORM\JoinColumn(name: 'order_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
protected ?OrderInterface $order = null;

#[ORM\ManyToOne(targetEntity: ReviewInterface::class, inversedBy: 'productReviews')]
#[ORM\JoinColumn(name: 'review_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
protected ?ReviewInterface $review = null;

public function __construct()
{
parent::__construct();

$this->status = self::STATUS_PENDING;
}
}
```

### Extend the ProductReview repository

You also need to create a custom repository that implements the plugin's interface:

Create `src/Repository/ProductReviewRepository.php`:

```php
<?php

declare(strict_types=1);

namespace App\Repository;

use Setono\SyliusReviewPlugin\Repository\ProductReviewRepositoryInterface;
use Setono\SyliusReviewPlugin\Repository\ProductReviewRepositoryTrait;
use Sylius\Bundle\CoreBundle\Doctrine\ORM\ProductReviewRepository as BaseProductReviewRepository;

class ProductReviewRepository extends BaseProductReviewRepository implements ProductReviewRepositoryInterface
{
use ProductReviewRepositoryTrait;
}
```

### Configure Sylius resources

Configure Sylius to use your custom entity and repository in `config/packages/sylius_review.yaml`:

```yaml
sylius_review:
resources:
product:
review:
classes:
model: App\Entity\ProductReview
repository: App\Repository\ProductReviewRepository
```

### Update your database

```bash
Expand Down
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
],
"require": {
"php": ">=8.1",
"doctrine/collections": "^1.8",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/orm": "^2.0",
"doctrine/persistence": "^2.0 || ^3.0",
"ocramius/doctrine-batch-utils": "^2.4",
Expand All @@ -21,11 +23,16 @@
"sylius/mailer-bundle": "^1.8 || ^2.0",
"sylius/order": "^1.0",
"sylius/resource-bundle": "^1.6",
"sylius/review": "^1.0",
"symfony/config": "^5.4 || ^6.4 || ^7.0",
"symfony/console": "^5.4 || ^6.4 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
"symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
"symfony/form": "^5.4 || ^6.4 || ^7.0",
"symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0",
"symfony/http-foundation": "^5.4 || ^6.4 || ^7.0",
"symfony/options-resolver": "^5.4 || ^6.4 || ^7.0",
"symfony/validator": "^5.4 || ^6.4 || ^7.0",
"symfony/workflow": "^5.4 || ^6.4 || ^7.0",
"webmozart/assert": "^1.11"
},
Expand Down
6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ parameters:
-
identifier: missingType.generics
path: src/Form
-
identifier: trait.unused
path: src/Model
-
identifier: trait.unused
path: src/Repository
30 changes: 30 additions & 0 deletions src/Checker/ReviewAutoApprovalChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker;

use Setono\SyliusReviewPlugin\Model\StoreReviewInterface;
use Sylius\Component\Review\Model\ReviewInterface;

final class ReviewAutoApprovalChecker implements ReviewAutoApprovalCheckerInterface
Copy link
Member Author

Choose a reason for hiding this comment

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

Rename to AutoApprovalCheckerInterface and make it use the composite pattern used elsewhere in this plugin to allow plugin users to define their own 'auto approval checker' functionality

{
public function __construct(
private readonly int $minimumRatingForAutoApproval = 4,
) {
}

public function shouldAutoApprove(StoreReviewInterface $review): bool
{
$rating = $review->getRating();

return null !== $rating && $rating >= $this->minimumRatingForAutoApproval;
}

public function shouldAutoApproveProductReview(ReviewInterface $review): bool
{
$rating = $review->getRating();

return null !== $rating && $rating >= $this->minimumRatingForAutoApproval;
}
}
21 changes: 21 additions & 0 deletions src/Checker/ReviewAutoApprovalCheckerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker;

use Setono\SyliusReviewPlugin\Model\StoreReviewInterface;
use Sylius\Component\Review\Model\ReviewInterface;

interface ReviewAutoApprovalCheckerInterface
{
/**
* Returns true if the store review should be auto-approved
*/
public function shouldAutoApprove(StoreReviewInterface $review): bool;

/**
* Returns true if the product review should be auto-approved
*/
public function shouldAutoApproveProductReview(ReviewInterface $review): bool;
}
26 changes: 26 additions & 0 deletions src/Checker/ReviewableOrder/CompositeReviewableOrderChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker\ReviewableOrder;

use Setono\CompositeCompilerPass\CompositeService;
use Sylius\Component\Core\Model\OrderInterface;

/**
* @extends CompositeService<ReviewableOrderCheckerInterface>
*/
final class CompositeReviewableOrderChecker extends CompositeService implements ReviewableOrderCheckerInterface
{
public function check(OrderInterface $order): ReviewableOrderCheck
{
foreach ($this->services as $service) {
$check = $service->check($order);
if (!$check->reviewable) {
return $check;
}
}

return ReviewableOrderCheck::reviewable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker\ReviewableOrder;

use Sylius\Component\Core\Model\OrderInterface;

final class OrderFulfilledReviewableOrderChecker implements ReviewableOrderCheckerInterface
{
public function check(OrderInterface $order): ReviewableOrderCheck
{
if ($order->getState() === OrderInterface::STATE_FULFILLED) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Let's add an array of 'reviewable states' in the contructor and make those configurable in the plugin configuration

return ReviewableOrderCheck::reviewable();
}

return ReviewableOrderCheck::notReviewable('setono_sylius_review.ui.order_not_fulfilled');
}
}
24 changes: 24 additions & 0 deletions src/Checker/ReviewableOrder/ReviewableOrderCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker\ReviewableOrder;

final class ReviewableOrderCheck
{
private function __construct(
public readonly bool $reviewable,
public readonly ?string $reason = null,
) {
}

public static function reviewable(): self
{
return new self(true);
}

public static function notReviewable(string $reason): self
{
return new self(false, $reason);
}
}
12 changes: 12 additions & 0 deletions src/Checker/ReviewableOrder/ReviewableOrderCheckerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker\ReviewableOrder;

use Sylius\Component\Core\Model\OrderInterface;

interface ReviewableOrderCheckerInterface
{
public function check(OrderInterface $order): ReviewableOrderCheck;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusReviewPlugin\Checker\ReviewableOrder;

use Setono\SyliusReviewPlugin\Repository\StoreReviewRepositoryInterface;
use Sylius\Component\Core\Model\OrderInterface;

final class StoreReviewEditableReviewableOrderChecker implements ReviewableOrderCheckerInterface
{
public function __construct(
private readonly StoreReviewRepositoryInterface $storeReviewRepository,
private readonly string $editablePeriod = '+24 hours',
Copy link
Member Author

Choose a reason for hiding this comment

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

Make this configurable in the plugin configuration. Also make it possible to disable this functionality because some store owners might want to allow users to be able to edit forever

) {
}

public function check(OrderInterface $order): ReviewableOrderCheck
{
$existingReview = $this->storeReviewRepository->findOneByOrder($order);

if (null === $existingReview) {
return ReviewableOrderCheck::reviewable();
}

$createdAt = $existingReview->getCreatedAt();
if (null === $createdAt) {
return ReviewableOrderCheck::reviewable();
}

$editableUntil = \DateTimeImmutable::createFromInterface($createdAt)->modify($this->editablePeriod);

if (new \DateTimeImmutable() < $editableUntil) {
return ReviewableOrderCheck::reviewable();
}

return ReviewableOrderCheck::notReviewable('setono_sylius_review.ui.review_period_expired');
}
}
Loading
Loading