Skip to content

Commit 82ecd89

Browse files
author
Lennart
committed
Ensure snake_case soft delete compatibility
1 parent 6706645 commit 82ecd89

10 files changed

Lines changed: 221 additions & 81 deletions

File tree

application/datamapper/HasTimestamps.php

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
* class User extends DataMapper {
2525
* use HasTimestamps;
2626
*
27-
* protected $createdAtColumn = 'created'; // Customize column name
28-
* protected $updatedAtColumn = 'modified'; // Customize column name
29-
* protected $timestampFormat = 'U'; // Unix timestamp format
27+
* protected $created_at_column = 'created'; // Customize column name
28+
* protected $updated_at_column = 'modified'; // Customize column name
29+
* protected $timestamp_format = 'U'; // Unix timestamp format
3030
* }
3131
* ```
3232
*
@@ -40,20 +40,20 @@ trait HasTimestamps
4040
* The name of the "created at" column
4141
* @var string
4242
*/
43-
protected $createdAtColumn = 'created_at';
43+
protected $created_at_column = 'created_at';
4444

4545
/**
4646
* The name of the "updated at" column
4747
* @var string
4848
*/
49-
protected $updatedAtColumn = 'updated_at';
49+
protected $updated_at_column = 'updated_at';
5050

5151
/**
5252
* The format for timestamp values
5353
* Options: 'Y-m-d H:i:s' (MySQL), 'U' (Unix timestamp), 'c' (ISO 8601)
5454
* @var string
5555
*/
56-
protected $timestampFormat = 'Y-m-d H:i:s';
56+
protected $timestamp_format = 'Y-m-d H:i:s';
5757

5858
/**
5959
* Hook into DataMapper's save process to add timestamps
@@ -64,17 +64,18 @@ trait HasTimestamps
6464
protected function _timestamp_before_save()
6565
{
6666
$timestamp = $this->_fresh_timestamp();
67-
67+
68+
$created_column = $this->get_created_at_column();
69+
$updated_column = $this->get_updated_at_column();
70+
6871
// If this is a new record (no ID), set created_at
6972
if (!$this->exists()) {
70-
$created_column = $this->createdAtColumn;
7173
if (!isset($this->{$created_column}) || empty($this->{$created_column})) {
7274
$this->{$created_column} = $timestamp;
7375
}
7476
}
75-
77+
7678
// Always update updated_at on save
77-
$updated_column = $this->updatedAtColumn;
7879
$this->{$updated_column} = $timestamp;
7980
}
8081

@@ -85,9 +86,7 @@ protected function _timestamp_before_save()
8586
*/
8687
protected function _fresh_timestamp()
8788
{
88-
$format = property_exists($this, 'timestampFormat') ?
89-
$this->timestampFormat :
90-
'Y-m-d H:i:s';
89+
$format = $this->resolve_timestamp_format();
9190

9291
if ($format === 'U') {
9392
return time(); // Unix timestamp
@@ -107,7 +106,7 @@ public function touch(): bool
107106
return FALSE;
108107
}
109108

110-
$updated_column = $this->updatedAtColumn;
109+
$updated_column = $this->get_updated_at_column();
111110
$this->{$updated_column} = $this->_fresh_timestamp();
112111

113112
// Update only the timestamp column
@@ -122,23 +121,58 @@ public function touch(): bool
122121
*
123122
* @return string
124123
*/
125-
public function getCreatedAtColumn(): string
124+
public function get_created_at_column(): string
126125
{
127-
return property_exists($this, 'createdAtColumn') ?
128-
$this->createdAtColumn :
129-
'created_at';
126+
return $this->resolve_timestamp_column('created_at_column', 'createdAtColumn', 'created_at');
130127
}
131128

132129
/**
133130
* Get the name of the "updated at" column
134131
*
135132
* @return string
136133
*/
137-
public function getUpdatedAtColumn(): string
134+
public function get_updated_at_column(): string
138135
{
139-
return property_exists($this, 'updatedAtColumn') ?
140-
$this->updatedAtColumn :
141-
'updated_at';
136+
return $this->resolve_timestamp_column('updated_at_column', 'updatedAtColumn', 'updated_at');
137+
}
138+
139+
/**
140+
* Resolve timestamp column names while supporting legacy camelCase overrides.
141+
*
142+
* @param string $snake Property name expected in new snake_case style
143+
* @param string $legacy Legacy camelCase property name
144+
* @param string $default Default column value
145+
* @return string
146+
*/
147+
protected function resolve_timestamp_column($snake, $legacy, $default)
148+
{
149+
if (property_exists($this, $snake) && !empty($this->{$snake})) {
150+
return $this->{$snake};
151+
}
152+
153+
if (property_exists($this, $legacy) && !empty($this->{$legacy})) {
154+
return $this->{$legacy};
155+
}
156+
157+
return $default;
158+
}
159+
160+
/**
161+
* Resolve the timestamp format, honoring both snake_case and legacy camelCase properties.
162+
*
163+
* @return string
164+
*/
165+
protected function resolve_timestamp_format()
166+
{
167+
if (property_exists($this, 'timestamp_format') && !empty($this->timestamp_format)) {
168+
return $this->timestamp_format;
169+
}
170+
171+
if (property_exists($this, 'timestampFormat') && !empty($this->timestampFormat)) {
172+
return $this->timestampFormat;
173+
}
174+
175+
return 'Y-m-d H:i:s';
142176
}
143177
}
144178

application/datamapper/SoftDeletes.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* class User extends DataMapper {
2525
* use SoftDeletes;
2626
*
27-
* protected $deletedAtColumn = 'archived_at';
27+
* protected $deleted_at_column = 'archived_at';
2828
* }
2929
* ```
3030
*
@@ -40,15 +40,23 @@ trait SoftDeletes
4040
*
4141
* @var string
4242
*/
43-
protected $deletedAtColumn = 'deleted_at';
43+
protected $deleted_at_column = 'deleted_at';
4444
/**
4545
* Get the name of the "deleted at" column.
4646
*
4747
* @return string
4848
*/
49-
public function getDeletedAtColumn()
49+
public function get_deleted_at_column()
5050
{
51-
return $this->deletedAtColumn;
51+
if (property_exists($this, 'deleted_at_column') && !empty($this->deleted_at_column)) {
52+
return $this->deleted_at_column;
53+
}
54+
55+
if (property_exists($this, 'deletedAtColumn') && !empty($this->deletedAtColumn)) {
56+
return $this->deletedAtColumn;
57+
}
58+
59+
return 'deleted_at';
5260
}
5361

5462
/**

application/datamapper/querybuilder.php

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ public function value($field, $default = NULL)
815815
*
816816
* @return DataMapper|DMZ_Collection|NULL
817817
*/
818-
public function getSmart() {
818+
public function get_smart() {
819819
// If limit is 1, return single model
820820
if ($this->_limit === 1) {
821821
return $this->first();
@@ -1570,7 +1570,7 @@ protected function _apply_eager_constraints_to_db($db, $relation, $table_prefix
15701570
protected function _apply_soft_delete_scope_to_db($db, $model, $table_prefix = '', $wrapper = NULL) {
15711571
// Check if user explicitly set soft delete scope in constraint callback
15721572
if ($wrapper !== NULL) {
1573-
$scope = $wrapper->getSoftDeleteScope();
1573+
$scope = $wrapper->get_soft_delete_scope();
15741574

15751575
// If user called with_softdeleted(), don't apply any deleted_at filter
15761576
if ($scope === 'with_softdeleted' || $scope === 'with_deleted') {
@@ -1640,6 +1640,8 @@ protected function _get_deleted_at_column($model) {
16401640
// Get custom column name if specified
16411641
if (property_exists($model, 'deleted_at_column') && !empty($model->deleted_at_column)) {
16421642
$deleted_col = $model->deleted_at_column;
1643+
} elseif (method_exists($model, 'get_deleted_at_column')) {
1644+
$deleted_col = $model->get_deleted_at_column();
16431645
} elseif (property_exists($model, 'deletedAtColumn') && !empty($model->deletedAtColumn)) {
16441646
$deleted_col = $model->deletedAtColumn;
16451647
} elseif (method_exists($model, 'getDeletedAtColumn')) {
@@ -1706,6 +1708,12 @@ protected function _load_nested_relation($results, $relation) {
17061708
* @return mixed
17071709
*/
17081710
public function __call($method, $args) {
1711+
$snake_case = $this->camel_to_snake($method);
1712+
1713+
if ($snake_case !== $method && method_exists($this, $snake_case)) {
1714+
return call_user_func_array(array($this, $snake_case), $args);
1715+
}
1716+
17091717
$result = call_user_func_array(array($this->model, $method), $args);
17101718

17111719
// If result is the model, return this for chaining
@@ -1715,6 +1723,20 @@ public function __call($method, $args) {
17151723

17161724
return $result;
17171725
}
1726+
1727+
/**
1728+
* Convert camelCase method names to snake_case for internal delegation.
1729+
*
1730+
* @param string $method
1731+
* @return string
1732+
*/
1733+
protected function camel_to_snake($method)
1734+
{
1735+
$snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $method));
1736+
1737+
// Historical methods used "softdeleted" without an underscore
1738+
return str_replace('soft_deleted', 'softdeleted', $snake);
1739+
}
17181740
}
17191741

17201742
/**
@@ -2084,7 +2106,7 @@ public function result_count() {
20842106
* @param string $key Key to index by
20852107
* @return array
20862108
*/
2087-
public function keyBy($key) {
2109+
public function key_by($key) {
20882110
$result = array();
20892111
foreach ($this->items as $item) {
20902112
$key_value = is_object($item) ? $item->{$key} : $item[$key];
@@ -2099,7 +2121,7 @@ public function keyBy($key) {
20992121
* @param string $key Key to group by
21002122
* @return array
21012123
*/
2102-
public function groupBy($key) {
2124+
public function group_by($key) {
21032125
$result = array();
21042126
foreach ($this->items as $item) {
21052127
$key_value = is_object($item) ? $item->{$key} : $item[$key];
@@ -2128,6 +2150,12 @@ public function groupBy($key) {
21282150
* @throws BadMethodCallException
21292151
*/
21302152
public function __call($method, $args) {
2153+
$snake_case = $this->camel_to_snake($method);
2154+
2155+
if ($snake_case !== $method && method_exists($this, $snake_case)) {
2156+
return call_user_func_array(array($this, $snake_case), $args);
2157+
}
2158+
21312159
// If collection has exactly one item, proxy to it
21322160
if ($this->count() === 1) {
21332161
$item = $this->first();
@@ -2152,6 +2180,19 @@ public function __call($method, $args) {
21522180
"Method '{$method}' does not exist on DMZ_Collection. {$suggestion}"
21532181
);
21542182
}
2183+
2184+
/**
2185+
* Convert camelCase method names to snake_case for internal delegation.
2186+
*
2187+
* @param string $method
2188+
* @return string
2189+
*/
2190+
protected function camel_to_snake($method)
2191+
{
2192+
$snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $method));
2193+
2194+
return str_replace('soft_deleted', 'softdeleted', $snake);
2195+
}
21552196

21562197
/**
21572198
* Magic method to proxy property access to the first item
@@ -2550,9 +2591,9 @@ public function only_softdeleted() {
25502591
*
25512592
* @return string 'active'|'with_softdeleted'|'only_softdeleted'
25522593
*/
2553-
public function getSoftDeleteScope() {
2554-
return $this->soft_delete_scope;
2555-
}
2594+
public function get_soft_delete_scope() {
2595+
return $this->soft_delete_scope;
2596+
}
25562597

25572598
/**
25582599
* Magic method to proxy other methods to the DB instance

application/libraries/datamapper.php

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8998,13 +8998,11 @@ protected function _handle_timestamps()
89988998
}
89998999

90009000
// Get column names from trait properties or fall back to config
9001-
$created_col = property_exists($this, 'createdAtColumn') && !empty($this->createdAtColumn) ?
9002-
$this->createdAtColumn :
9003-
(isset(DataMapper::$config['created_at_column']) ? DataMapper::$config['created_at_column'] : 'created_at');
9004-
9005-
$updated_col = property_exists($this, 'updatedAtColumn') && !empty($this->updatedAtColumn) ?
9006-
$this->updatedAtColumn :
9007-
(isset(DataMapper::$config['updated_at_column']) ? DataMapper::$config['updated_at_column'] : 'updated_at');
9001+
$created_col = $this->_resolve_model_property(array('created_at_column', 'createdAtColumn'),
9002+
isset(DataMapper::$config['created_at_column']) ? DataMapper::$config['created_at_column'] : 'created_at');
9003+
9004+
$updated_col = $this->_resolve_model_property(array('updated_at_column', 'updatedAtColumn'),
9005+
isset(DataMapper::$config['updated_at_column']) ? DataMapper::$config['updated_at_column'] : 'updated_at');
90089006

90099007
// Generate fresh timestamp
90109008
$timestamp = $this->_fresh_timestamp();
@@ -9052,6 +9050,11 @@ protected function _fresh_timestamp()
90529050
*/
90539051
protected function _soft_delete_is_enabled()
90549052
{
9053+
if (property_exists($this, 'soft_delete') && $this->soft_delete !== NULL)
9054+
{
9055+
return (bool) $this->soft_delete;
9056+
}
9057+
90559058
if (property_exists($this, 'softDelete') && $this->softDelete !== NULL)
90569059
{
90579060
return (bool) $this->softDelete;
@@ -9079,10 +9082,23 @@ protected function _timestamps_is_enabled()
90799082
*/
90809083
protected function _get_deleted_at_column()
90819084
{
9085+
// Allow the trait to resolve column precedence first (snake_case preferred)
9086+
if (method_exists($this, 'get_deleted_at_column'))
9087+
{
9088+
return $this->get_deleted_at_column();
9089+
}
9090+
9091+
if (method_exists($this, 'getDeletedAtColumn'))
9092+
{
9093+
return $this->getDeletedAtColumn();
9094+
}
9095+
90829096
// If model uses SoftDeletes trait, get column from trait property
9083-
if (property_exists($this, 'deletedAtColumn') && !empty($this->deletedAtColumn))
9097+
$column = $this->_resolve_model_property(array('deleted_at_column', 'deletedAtColumn'), NULL);
9098+
9099+
if ($column !== NULL)
90849100
{
9085-
return $this->deletedAtColumn;
9101+
return $column;
90869102
}
90879103

90889104
// Fall back to global config
@@ -9097,10 +9113,30 @@ protected function _get_deleted_at_column()
90979113
*/
90989114
protected function _soft_delete_settings()
90999115
{
9100-
$explicit = property_exists($this, 'softDelete') && $this->softDelete !== NULL;
9116+
$explicit = $this->_resolve_model_property(array('soft_delete', 'softDelete')) !== NULL;
91019117
$enabled = $this->_soft_delete_is_enabled();
91029118
return array($enabled, $explicit);
91039119
}
9120+
9121+
/**
9122+
* Resolve a model property from a list of candidates, favouring the first non-empty value.
9123+
*
9124+
* @param array<int, string> $properties Ordered list of property names to probe
9125+
* @param mixed $default Default value when no property is found
9126+
* @return mixed
9127+
*/
9128+
protected function _resolve_model_property(array $properties, $default = NULL)
9129+
{
9130+
foreach ($properties as $property)
9131+
{
9132+
if (property_exists($this, $property) && isset($this->{$property}) && $this->{$property} !== '')
9133+
{
9134+
return $this->{$property};
9135+
}
9136+
}
9137+
9138+
return $default;
9139+
}
91049140

91059141
// --------------------------------------------------------------------
91069142

0 commit comments

Comments
 (0)