Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
NeilPeyssard committed Dec 7, 2022
0 parents commit 48c56e8
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 0 deletions.
50 changes: 50 additions & 0 deletions Controller/SortableController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Sherlockode\SonataSortableBundle\Controller;

use Sherlockode\SonataSortableBundle\Manager\SortableManager;
use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;

class SortableController extends CRUDController
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly SortableManager $sortableManager
) {
}

/**
* @param string $direction
*
* @return Response
*/
public function moveAction(string $direction): Response
{
if (!$this->admin->isGranted('EDIT')) {
$this->addFlash('danger', $this->translator->trans(
'You are not authorized to perform this action',
[],
'sherlockode_sonata_sortable'
));

return new RedirectResponse($this->admin->generateUrl(
'list',
['filter' => $this->admin->getFilterParameters()]
));
}

$object = $this->admin->getSubject();
$newPosition = $this->sortableManager->getPosition($object, 'position', $direction);
$object->setPosition($newPosition);

$this->admin->update($object);

return new RedirectResponse($this->admin->generateUrl(
'list',
['filter' => $this->admin->getFilterParameters()]
));
}
}
20 changes: 20 additions & 0 deletions DependencyInjection/SherlockodeSonataSortableExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Sherlockode\SonataSortableBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class SherlockodeSonataSortableExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');
}
}
89 changes: 89 additions & 0 deletions Manager/SortableManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Sherlockode\SonataSortableBundle\Manager;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Symfony\Component\PropertyAccess\PropertyAccess;

class SortableManager
{
public function __construct(private readonly EntityManagerInterface $em)
{
}

/**
* @param object $object
* @param string $positionProperty
* @param string $direction
*
* @return int
*/
public function getPosition(object $object, string $positionProperty, string $direction): int
{
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$currentPosition = $propertyAccessor->getValue($object, $positionProperty);
$lastPosition = $this->getLastPosition(get_class($object), $positionProperty);

if ('top' === $direction) {
return 0;
}

if ('bottom' === $direction && $currentPosition < $lastPosition) {
return $lastPosition;
}

if ('up' === $direction && $currentPosition > 0) {
return $currentPosition - 1;
}

if ('down' === $direction && $currentPosition < $lastPosition) {
return $currentPosition + 1;
}

return $currentPosition;
}

/**
* @param string $className
* @param string $positionProperty
*
* @return int
*/
public function getFirstPosition(string $className, string $positionProperty): int
{
try {
$position = $this->em->createQueryBuilder()
->select(sprintf('MIN(o.%s)', $positionProperty))
->from($className, 'o')
->getQuery()
->getSingleScalarResult();
} catch (NoResultException | NonUniqueResultException $e) {
return 0;
}

return $position ?? 0;
}

/**
* @param string $className
* @param string $positionProperty
*
* @return int
*/
public function getLastPosition(string $className, string $positionProperty): int
{
try {
$position = $this->em->createQueryBuilder()
->select(sprintf('MAX(o.%s)', $positionProperty))
->from($className, 'o')
->getQuery()
->getSingleScalarResult();
} catch (NoResultException | NonUniqueResultException $e) {
return 0;
}

return $position ?? 0;
}
}
171 changes: 171 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
Installation
============

Make sure Composer is installed globally, as explained in the
[installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.

Applications that use Symfony Flex
----------------------------------

Open a command console, enter your project directory and execute:

```console
$ composer require sherlockode/sonata-sortable-bundle
```

Applications that don't use Symfony Flex
----------------------------------------

### Step 1: Download the Bundle

Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:

```console
$ composer require sherlockode/sonata-sortable-bundle
```

### Step 2: Enable the Bundle

Then, enable the bundle by adding it to the list of registered bundles
in the `config/bundles.php` file of your project:

```php
// config/bundles.php

return [
// ...
Sherlockode\SonataSortableBundle\SherlockodeSonataSortableBundle::class => ['all' => true],
];
```

Sortable behavior in admin listing
==================================

Pre-requisites
--------------

You need to have a working Sonata admin and to have installed and configured `gedmo/doctrine-extensions` (check `stof/doctrine-extensions-bundle` for easy integration).

The recipe
----------

First of all, add a position property in your entity

```php
// src/Entity/Category.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
* @ORM\Table(name="category")
* @ORM\Entity
*/
class Category
{
/**
* @var int
*
* @Gedmo\SortablePosition
* @ORM\Column(name="position", type="integer")
*/
private $position;
}
```

To change this position with Sonata, we need to add a new route in our admin:

```php
// src/Admin/CategoryAdmin.php

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Route\RouteCollectionInterface;

class CategoryAdmin extends AbstractAdmin
{
// ...

/**
* @param RouteCollectionInterface $collection
*
* @return void
*/
protected function configureRoutes(RouteCollectionInterface $collection): void
{
$collection->add('move', $this->getRouterIdParameter().'/move/{direction}');
}
}
```

Update the admin configuration to use our custom controller

```yaml
services:
admin.category:
class: 'App\Admin\CategoryAdmin'
arguments: [ ~, App\Entity\Category, 'Sherlockode\SonataSortableBundle\Controller\SortableController' ]
tags:
- { name: sonata.admin, manager_type: orm, label: Categories }
```
Then, add default sort by position in the admin:
```php
// src/Admin/CategoryAdmin.php

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridInterface;

class CategoryAdmin extends AbstractAdmin
{
// ...

protected function configureDefaultSortValues(array &$sortValues): void
{
$sortValues[DatagridInterface::PAGE] = 1;
$sortValues[DatagridInterface::SORT_ORDER] = 'ASC';
$sortValues[DatagridInterface::SORT_BY] = 'position';
}
}
```

Add controls to allow the user to change the position of items:

```php
// src/Admin/CategoryAdmin.php

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;

class CategoryAdmin extends AbstractAdmin
{
// ...

/**
* @inheritDoc
*/
protected function configureListFields(ListMapper $list): void
{
$list
// your other fields
->add(ListMapper::NAME_ACTIONS, null, [
'actions' => [
'move' => [
'template' => '@SherlockodeSonataSortable/list__action_move.html.twig',
],
],
])
;
}
}
```
7 changes: 7 additions & 0 deletions Resources/config/routing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
sherlockode_sonata_sortable_move_item:
path: /{id}/move/{direction}
controller: Sherlockode\SonataSortableBundle\Controller\SortableController::move
methods: GET
requirements:
id: '\d+'
direction: up|down|top|bottom
23 changes: 23 additions & 0 deletions Resources/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
services:
_defaults:
autowire: true
autoconfigure: true

# Controller
Sherlockode\SonataSortableBundle\Controller\SortableController:
tags: ['controller.service_arguments']
arguments:
$sortableManager: '@sherlockode.sonata_sortable.sortable_manager'

# Manager
sherlockode.sonata_sortable.sortable_manager:
class: Sherlockode\SonataSortableBundle\Manager\SortableManager
arguments:
$em: '@doctrine.orm.entity_manager'

# Twig extension
Sherlockode\SonataSortableBundle\Twig\SortableExtension:
tags: ['twig.extension']
Sherlockode\SonataSortableBundle\Twig\SortableRuntime:
arguments:
$sortableManager: '@sherlockode.sonata_sortable.sortable_manager'
27 changes: 27 additions & 0 deletions Resources/translations/sherlockode_sonata_sortable.en.xlf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="action_not_allowed">
<source>You are not authorized to perform this action</source>
<target>You are not authorized to perform this action</target>
</trans-unit>
<trans-unit id="move_down">
<source>Move down</source>
<target>Move down</target>
</trans-unit>
<trans-unit id="move_to_first_position">
<source>Move to first position</source>
<target>Move to first position</target>
</trans-unit>
<trans-unit id="move_to_last_position">
<source>Move to last position</source>
<target>Move to last position</target>
</trans-unit>
<trans-unit id="move_up">
<source>Move up</source>
<target>Move up</target>
</trans-unit>
</body>
</file>
</xliff>
Loading

0 comments on commit 48c56e8

Please sign in to comment.