Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120835263
EntitleableTrait.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
EntitleableTrait.php
View Options
<?php
namespace
App\Traits
;
use
App\Entitlement
;
use
App\Package
;
use
App\Sku
;
use
App\Transaction
;
use
App\User
;
use
App\Wallet
;
use
Carbon\Carbon
;
use
Illuminate\Database\Eloquent\Casts\Attribute
;
use
Illuminate\Database\Eloquent\Relations\HasMany
;
use
Illuminate\Database\Query\JoinClause
;
use
Illuminate\Support\Str
;
/**
* @property ?User $account Account (wallet) owner
*/
trait
EntitleableTrait
{
/**
* Get the account (wallet) owner. Mutated walletOwner() result.
*/
protected
function
account
():
Attribute
{
return
Attribute
::
make
(
get
:
fn
()
=>
$this
->
walletOwner
(),
);
}
/**
* Assign a package to an entitleable object. It should not have any existing entitlements.
*
* @param Package $package The package
* @param Wallet $wallet The wallet
*
* @return $this
*/
public
function
assignPackageAndWallet
(
Package
$package
,
Wallet
$wallet
)
{
// TODO: There should be some sanity checks here. E.g. not package can be
// assigned to any entitleable, but we don't really have package types.
foreach
(
$package
->
skus
as
$sku
)
{
for
(
$i
=
$sku
->
pivot
->
qty
;
$i
>
0
;
$i
--)
{
Entitlement
::
create
([
'wallet_id'
=>
$wallet
->
id
,
'sku_id'
=>
$sku
->
id
,
'cost'
=>
$sku
->
pivot
->
cost
(),
'fee'
=>
$sku
->
pivot
->
fee
(),
'entitleable_id'
=>
$this
->
id
,
'entitleable_type'
=>
self
::
class
,
]);
}
}
return
$this
;
}
/**
* Assign a SKU to an entitleable object.
*
* @param Sku $sku the sku to assign
* @param int $count Count of entitlements to add
* @param ?Wallet $wallet The wallet to use when objects's wallet is unknown
*
* @return $this
*
* @throws \Exception
*/
public
function
assignSku
(
Sku
$sku
,
int
$count
=
1
,
$wallet
=
null
)
{
if
(!
$wallet
)
{
$wallet
=
$this
->
wallet
();
}
if
(!
$wallet
)
{
throw
new
\Exception
(
"No wallet specified for the new entitlement"
);
}
$exists
=
$this
->
entitlements
()->
where
(
'sku_id'
,
$sku
->
id
)->
count
();
while
(
$count
>
0
)
{
Entitlement
::
create
([
'wallet_id'
=>
$wallet
->
id
,
'sku_id'
=>
$sku
->
id
,
'cost'
=>
$exists
>=
$sku
->
units_free
?
$sku
->
cost
:
0
,
'fee'
=>
$exists
>=
$sku
->
units_free
?
$sku
->
fee
:
0
,
'entitleable_id'
=>
$this
->
id
,
'entitleable_type'
=>
self
::
class
,
]);
$exists
++;
$count
--;
}
return
$this
;
}
/**
* Assign the object to a wallet.
*
* @param Wallet $wallet The wallet
* @param ?string $title Optional SKU title
*
* @return $this
*
* @throws \Exception
*/
public
function
assignToWallet
(
Wallet
$wallet
,
$title
=
null
)
{
if
(
empty
(
$this
->
id
))
{
throw
new
\Exception
(
"Object not yet exists"
);
}
if
(
$this
->
entitlements
()->
count
())
{
throw
new
\Exception
(
"Object already assigned to a wallet"
);
}
// Find the SKU title, e.g. \App\SharedFolder -> shared-folder
// Note: it does not work with User/Domain model (yet)
if
(!
$title
)
{
$title
=
Str
::
kebab
(
\class_basename
(
self
::
class
));
}
$sku
=
$this
->
skuByTitle
(
$title
);
$exists
=
$wallet
->
entitlements
()->
where
(
'sku_id'
,
$sku
->
id
)->
count
();
Entitlement
::
create
([
'wallet_id'
=>
$wallet
->
id
,
'sku_id'
=>
$sku
->
id
,
'cost'
=>
$exists
>=
$sku
->
units_free
?
$sku
->
cost
:
0
,
'fee'
=>
$exists
>=
$sku
->
units_free
?
$sku
->
fee
:
0
,
'entitleable_id'
=>
$this
->
id
,
'entitleable_type'
=>
self
::
class
,
]);
return
$this
;
}
/**
* Boot function from Laravel.
*/
protected
static
function
bootEntitleableTrait
()
{
// Soft-delete and force-delete object's entitlements on object's delete
static
::
deleting
(
static
function
(
$model
)
{
$force
=
$model
->
isForceDeleting
();
$entitlements
=
$model
->
entitlements
();
if
(
$force
)
{
$entitlements
=
$entitlements
->
withTrashed
();
}
$list
=
$entitlements
->
get
()
->
map
(
static
function
(
$entitlement
)
use
(
$force
)
{
if
(
$force
)
{
$entitlement
->
forceDelete
();
}
else
{
$entitlement
->
delete
();
}
return
$entitlement
->
id
;
})
->
all
();
// Remove transactions, they have no foreign key constraint
if
(
$force
&&
!
empty
(
$list
))
{
Transaction
::
where
(
'object_type'
,
Entitlement
::
class
)
->
whereIn
(
'object_id'
,
$list
)
->
delete
();
}
});
// Restore object's entitlements on restore
static
::
restored
(
static
function
(
$model
)
{
$model
->
restoreEntitlements
();
});
}
/**
* Count entitlements for the specified SKU.
*
* @param string $title The SKU title
*
* @return int Numer of entitlements
*/
public
function
countEntitlementsBySku
(
string
$title
):
int
{
$sku
=
$this
->
skuByTitle
(
$title
);
if
(!
$sku
)
{
return
0
;
}
return
$this
->
entitlements
()->
where
(
'sku_id'
,
$sku
->
id
)->
count
();
}
/**
* Entitlements for this object.
*
* @return HasMany<Entitlement, $this>
*/
public
function
entitlements
()
{
return
$this
->
hasMany
(
Entitlement
::
class
,
'entitleable_id'
,
'id'
)
->
where
(
'entitleable_type'
,
self
::
class
);
}
/**
* Check if an entitlement for the specified SKU exists.
*
* @param string $title The SKU title
*
* @return bool True if specified SKU entitlement exists
*/
public
function
hasSku
(
string
$title
):
bool
{
return
$this
->
countEntitlementsBySku
(
$title
)
>
0
;
}
/**
* Remove a number of entitlements for the SKU.
*
* @param Sku $sku The SKU
* @param int $count The number of entitlements to remove
*
* @return $this
*/
public
function
removeSku
(
Sku
$sku
,
int
$count
=
1
)
{
$entitlements
=
$this
->
entitlements
()
->
where
(
'sku_id'
,
$sku
->
id
)
->
orderBy
(
'cost'
,
'desc'
)
->
orderBy
(
'created_at'
)
->
get
();
$entitlements_count
=
count
(
$entitlements
);
foreach
(
$entitlements
as
$entitlement
)
{
if
(
$entitlements_count
<=
$sku
->
units_free
)
{
continue
;
}
if
(
$count
>
0
)
{
$entitlement
->
delete
();
$entitlements_count
--;
$count
--;
}
}
return
$this
;
}
/**
* Restore object entitlements.
*/
public
function
restoreEntitlements
():
void
{
// We'll restore only these that were deleted last. So, first we get
// the maximum deleted_at timestamp and then use it to select
// entitlements for restore
$deleted_at
=
$this
->
entitlements
()->
withTrashed
()->
max
(
'deleted_at'
);
if
(
$deleted_at
)
{
$threshold
=
(
new
Carbon
(
$deleted_at
))->
subMinute
();
// Restore object entitlements
$this
->
entitlements
()->
withTrashed
()
->
where
(
'deleted_at'
,
'>='
,
$threshold
)
->
update
([
'updated_at'
=>
now
(),
'deleted_at'
=>
null
]);
// Note: We're assuming that cost of entitlements was correct
// on deletion, so we don't have to re-calculate it again.
// TODO: We should probably re-calculate the cost
}
}
/**
* Find the SKU object by title. Use current object's tenant context.
*
* @param string $title SKU title
*
* @return ?Sku A SKU object
*/
protected
function
skuByTitle
(
string
$title
):
?
Sku
{
return
Sku
::
withObjectTenantContext
(
$this
)->
where
(
'title'
,
$title
)->
first
();
}
/**
* Get all SKU titles for this object.
*
* @return array<string>
*/
public
function
skuTitles
():
array
{
return
$this
->
entitlements
()->
distinct
()
->
join
(
'skus'
,
'skus.id'
,
'='
,
'entitlements.sku_id'
)
->
pluck
(
'title'
)
->
sort
()
->
values
()
->
all
();
}
/**
* Returns entitleable object title (e.g. email or domain name).
*
* @return string|null An object title/name
*/
public
function
toString
():
?
string
{
// This method should be overloaded by the model class
// if the object has not email attribute
return
$this
->
email
;
}
/**
* Returns the wallet by which the object is controlled
*
* @return ?Wallet A wallet object
*/
public
function
wallet
():
?
Wallet
{
// Note: Using $this->entitlements() here results in more complicated query
$entitlement
=
Entitlement
::
withTrashed
()
->
where
(
'entitleable_id'
,
$this
->
id
)
->
where
(
'entitleable_type'
,
self
::
class
)
->
orderByDesc
(
'created_at'
)
->
limit
(
1
);
// Note: We use joinSub() because whereIn() does not allow LIMIT in the subquery
// @phpstan-ignore-next-line
$wallet
=
Wallet
::
select
(
'wallets.*'
)
->
joinSub
(
$entitlement
,
'e'
,
static
function
(
JoinClause
$join
)
{
$join
->
on
(
'wallets.id'
,
'='
,
'e.wallet_id'
);
})
->
first
();
// A new user account does not have entitlements. Without this fallback to
// the user wallet e.g. assignSku() will fail. It is not a problem for
// assignPackage() so typical account creation works w/o this. Some tests fail, though.
// TODO: In future we should throw an exception here instead.
if
(!
$wallet
&&
$this
instanceof
User
)
{
$wallet
=
$this
->
wallets
()->
first
();
}
return
$wallet
;
}
/**
* Return the owner of the wallet (account) this entitleable is assigned to
*
* @return ?User Account owner
*/
public
function
walletOwner
():
?
User
{
$wallet
=
$this
->
wallet
();
if
(
$wallet
)
{
if
(
$this
instanceof
User
&&
$wallet
->
user_id
==
$this
->
id
)
{
return
$this
;
}
return
$wallet
->
owner
;
}
return
null
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Apr 24, 1:16 PM (4 d, 20 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18740793
Default Alt Text
EntitleableTrait.php (10 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline