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
2 changes: 2 additions & 0 deletions doc/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,8 @@ Optional Attributes:

- **hardDelete** - Flag indicating the object supports hard deletes, defaults to `true`

- **nonDeletedColumnValue** - The value that is seen as not deleted, default: null

Examples:

```php
Expand Down
23 changes: 23 additions & 0 deletions doc/soft-deleteable.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,26 @@ assert($found !== null); // Found because deletion time is in the future
By default, the soft deleteable extension allows soft deleted records to be "hard deleted" (fully removed from the database)
by deleting them a second time. However, by setting the `hardDelete` parameter in the configuration to `false`, you can
prevent soft deleted records from being deleted at all.

## Setting the non-deleted value

By default a record set to null will be seen as not (yet) soft-deleted.
This can be overwritten by setting `nonDeletedColumnValue` on the specified entity

```php
#[ORM\Entity]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', hardDelete: false, nonDeletedColumnValue: '1970-01-01 00:00:00')]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
public ?int $id = null;

#[ORM\Column(type: Types::STRING)]
public ?string $title = null;

#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
public ?\DateTimeImmutable $deletedAt = null;
}
```
6 changes: 5 additions & 1 deletion src/Mapping/Annotation/SoftDeleteable.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ final class SoftDeleteable implements GedmoAnnotation

public bool $hardDelete = true;

public ?string $nonDeletedColumnValue = null;

/**
* @param array<string, mixed> $data
*/
public function __construct(array $data = [], string $fieldName = 'deletedAt', bool $timeAware = false, bool $hardDelete = true)
public function __construct(array $data = [], string $fieldName = 'deletedAt', bool $timeAware = false, bool $hardDelete = true, ?string $nonDeletedColumnValue = null)
{
if ([] !== $data) {
Deprecation::trigger(
Expand All @@ -53,12 +55,14 @@ public function __construct(array $data = [], string $fieldName = 'deletedAt', b
$this->fieldName = $this->getAttributeValue($data, 'fieldName', $args, 1, $fieldName);
$this->timeAware = $this->getAttributeValue($data, 'timeAware', $args, 2, $timeAware);
$this->hardDelete = $this->getAttributeValue($data, 'hardDelete', $args, 3, $hardDelete);
$this->nonDeletedColumnValue = $this->getAttributeValue($data, 'nonDeletedColumnValue', $args, 4, $nonDeletedColumnValue);

return;
}

$this->fieldName = $fieldName;
$this->timeAware = $timeAware;
$this->hardDelete = $hardDelete;
$this->nonDeletedColumnValue = $nonDeletedColumnValue;
}
}
5 changes: 3 additions & 2 deletions src/SoftDeleteable/Filter/ODM/SoftDeleteableFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,19 @@ public function addFilterCriteria(ClassMetadata $targetEntity): array
}

$column = $targetEntity->getFieldMapping($config['fieldName']);
$nonDeletedValue = $config['nonDeletedColumnValue'] ?? null;

if (isset($config['timeAware']) && $config['timeAware']) {
return [
'$or' => [
[$column['fieldName'] => null],
[$column['fieldName'] => $nonDeletedValue],
[$column['fieldName'] => ['$gt' => new \DateTime()]],
],
];
}

return [
$column['fieldName'] => null,
$column['fieldName'] => $nonDeletedValue,
];
}

Expand Down
3 changes: 2 additions & 1 deletion src/SoftDeleteable/Filter/SoftDeleteableFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAli

$column = $quoteStrategy->getColumnName($config['fieldName'], $targetEntity, $platform);

$addCondSql = $targetTableAlias.'.'.$column.' IS NULL';
$nonDeletedSql = isset($config['nonDeletedColumnValue']) ? '= "'.$config['nonDeletedColumnValue'].'"': 'IS NULL';
$addCondSql = $targetTableAlias.'.'.$column.' '.$nonDeletedSql;
if (isset($config['timeAware']) && $config['timeAware']) {
$addCondSql = "({$addCondSql} OR {$targetTableAlias}.{$column} > {$platform->getCurrentTimestampSQL()})";
}
Expand Down
8 changes: 8 additions & 0 deletions src/SoftDeleteable/Mapping/Driver/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ public function readExtendedMetadata($meta, array &$config)

$config['hardDelete'] = $annot->hardDelete;
}

if (isset($annot->nonDeletedColumnValue)) {
if (!is_string($annot->nonDeletedColumnValue)) {
throw new InvalidMappingException('nonDeletedColumnValue must be string. '.gettype($annot->nonDeletedColumnValue).' provided.');
}

$config['nonDeletedColumnValue'] = $annot->nonDeletedColumnValue;
}
}

$this->validateFullMetadata($meta, $config);
Expand Down
5 changes: 5 additions & 0 deletions src/SoftDeleteable/Mapping/Driver/Xml.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public function readExtendedMetadata($meta, array &$config)
if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'hard-delete')) {
$config['hardDelete'] = $this->_getBooleanAttribute($xml->{'soft-deleteable'}, 'hard-delete');
}

$config['nonDeletedColumnValue'] = null;
if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'non-deleted-column-value')) {
$config['nonDeletedColumnValue'] = $this->_getAttribute($xml->{'soft-deleteable'}, 'non-deleted-column-value');
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/SoftDeleteable/Mapping/Driver/Yaml.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public function readExtendedMetadata($meta, array &$config)
}
$config['hardDelete'] = $classMapping['soft_deleteable']['hard_delete'];
}

$config['nonDeletedColumnValue'] = null;
if (isset($classMapping['soft_deleteable']['non_deleted_column_value'])) {
if (!is_string($classMapping['soft_deleteable']['non_deleted_column_value'])) {
throw new InvalidMappingException('nonDeletedColumnValue must be string. '.gettype($classMapping['soft_deleteable']['non_deleted_column_value']).' provided.');
}
$config['nonDeletedColumnValue'] = $classMapping['soft_deleteable']['non_deleted_column_value'];
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Gedmo\Tests\Mapping\Fixture\Xml\SoftDeleteableNonDeletedColumnValue" table="soft_deleteables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="deletedAt" type="datetime" nullable="true"/>
<gedmo:soft-deleteable field-name="deletedAt" time-aware="false" hard-delete="false" non-deleted-column-value="1970-01-01 00:00:00"/>
</entity>
</doctrine-mapping>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
Gedmo\Tests\Mapping\Fixture\Yaml\SoftDeleteableNonDeletedColumnValue:
type: entity
table: soft_deleteables
gedmo:
soft_deleteable:
field_name: deletedAt
hard_delete: false
non_deleted_column_value: '1970-01-01 00:00:00'
id:
id:
type: integer
generator:
strategy: AUTO
fields:
deletedAt:
type: datetime
nullable: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tests\Mapping\Fixture;

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

/**
* @ORM\Entity
*
* @Gedmo\SoftDeleteable(fieldName="deletedAt", hardDelete: false, nonDeletedColumnValue: '1970-01-01 00:00:00')
*/
#[ORM\Entity]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', hardDelete: false, nonDeletedColumnValue: '1970-01-01 00:00:00')]
class SoftDeleteableNonDeletedColumnValue
{
/**
* @var int|null
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private $id;

private ?string $title = null;

private ?string $code = null;

/**
* @var string|null
*/
private $slug;

/**
* @var \DateTime|null
*
* @ORM\Column(name="deleted_at", type="datetime", nullable=true)
*/
#[ORM\Column(name: 'deleted_at', type: Types::DATETIME_MUTABLE, nullable: true)]
private $deletedAt;

public function getId(): ?int
{
return $this->id;
}

public function setTitle(?string $title): void
{
$this->title = $title;
}

public function getTitle(): ?string
{
return $this->title;
}

public function setCode(?string $code): void
{
$this->code = $code;
}

public function getCode(): ?string
{
return $this->code;
}

public function getSlug(): ?string
{
return $this->slug;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tests\Mapping\Fixture\Xml;

class SoftDeleteableNonDeletedColumnValue
{
/**
* @var int
*/
private $id;

/**
* @var \DateTime|null
*/
private $deletedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tests\Mapping\Fixture\Yaml;

class SoftDeleteableNonDeletedColumnValue
{
/**
* @var int
*/
private $id;

/**
* @var \DateTime|null
*/
private $deletedAt;
}
77 changes: 77 additions & 0 deletions tests/Gedmo/Mapping/SoftDeleteableMappingNonDeletedValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tests\Mapping;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\YamlDriver;
use Gedmo\Mapping\ExtensionMetadataFactory;
use Gedmo\SoftDeleteable\SoftDeleteableListener;
use Gedmo\Tests\Mapping\Fixture\SoftDeleteableNonDeletedColumnValue as AnnotatedSoftDeleteable;
use Gedmo\Tests\Mapping\Fixture\Xml\SoftDeleteableNonDeletedColumnValue as XmlSoftDeleteable;
use Gedmo\Tests\Mapping\Fixture\Yaml\SoftDeleteableNonDeletedColumnValue as YamlSoftDeleteable;

/**
* These are mapping tests for SoftDeleteable extension
*
* @author Gustavo Falco <[email protected]>
* @author Gediminas Morkevicius <[email protected]>
*/
final class SoftDeleteableMappingNonDeletedValueTest extends ORMMappingTestCase
{
private EntityManager $em;

protected function setUp(): void
{
ORMMappingTestCase::setUp();

$listener = new SoftDeleteableListener();
$listener->setCacheItemPool($this->cache);

$this->em = $this->getBasicEntityManager();
$this->em->getEventManager()->addEventSubscriber($listener);
}

/**
* @return \Generator<string, array{class-string}>
*/
public static function dataSoftDeleteableObject(): \Generator
{
yield 'Model with XML mapping' => [XmlSoftDeleteable::class];

if (PHP_VERSION_ID >= 80000) {
yield 'Model with attributes' => [AnnotatedSoftDeleteable::class];
} elseif (class_exists(AnnotationDriver::class)) {
yield 'Model with annotations' => [AnnotatedSoftDeleteable::class];
}

if (class_exists(YamlDriver::class)) {
yield 'Model with YAML mapping' => [YamlSoftDeleteable::class];
}
}

/**
* @param class-string $className
*
* @dataProvider dataSoftDeleteableObject
*/
public function testSoftDeleteableMapping(string $className): void
{
// Force metadata class loading.
$this->em->getClassMetadata($className);
$cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\SoftDeleteable');
$config = $this->cache->getItem($cacheId)->get();

static::assertArrayHasKey('nonDeletedColumnValue', $config);
static::assertSame('1970-01-01 00:00:00', $config['nonDeletedColumnValue']);
}
}
Loading