Find whether an Image is used anywhere - php

In my SilverStripe 3.4 environment I have a bunch of different models that have an attached Image, e.g.:
BlogPost has_one Image (via silverstripe/blog)
Widget has_one Image (via silverstripe/widgets)
MyWidget has_one Image (custom module)
I want to prevent an Image e.g. ID 123 from being deleted in the CMS admin if it's used in any of the above (as examples - this should be system wide).
Is there a way that I can check all models that have a related Image at once, maybe via a Image belongs_many_many lookup or something?

You'd prob need to decorate Image via a DataExtension subclass and declare a $belongs_to static array with a custom onBeforeDelete() or possibly validate().
Regardless, within either is where you'd call a routine that checks your database for the necessary conditions. Your choice of which method to use
is dictated by the scenarios under which an Image record might be deleted in your system (e.g you may have some automated, non-Human tasks where the scenario you wish to avoid is played out - so you'd avoid validate() and use onBeforeDelete())
Something like this (Totally untested!)
class MyImageExtension extends DatExtension
{
public function onBeforeDelete()
{
if (!$this->imagesExistThatShouldNotBeDeleted()) {
parent::onBeforeDelete();
}
}
/**
* #return boolean True if images exist that shouldn't be deleted, false otherwise.
*/
private function imagesExistThatShouldNotBeDeleted()
{
$owner = $this->getOwner();
$dataObjectSubClasses = ClassInfo::getValidSubClasses('DataObject');
$classesWithImageHasOne = [];
foreach ($dataObjectSubClasses as $subClass) {
if ($classHasOneImage = $subClass::create()->hasOneComponent('Image')) {
$classesWithImageHasOne[] = $classHasOneImage;
}
}
if (in_array($owner->class, $classesWithImageHasOne)) {
return true;
}
return false;
}
}

Related

Laravel Eloquent - Model extends other model

I have a question about extending my own Models eloquent.
In the project I am currently working on is table called modules and it contains list of project modules, number of elements of that module, add date etc.
For example:
id = 1; name = 'users'; count = 120; date_add = '2007-05-05';
and this entity called users corresponds to model User (Table - users) so that "count" it's number of Users
and to update count we use script running every day (I know that it's not good way but... u know).
In that script is loop and inside that loop a lot of if statement (1 per module) and inside the if a single query with count. According to example it's similar to:
foreach($modules as $module) {
if($module['name'] == 'users') {
$count = old_and_bad_method_to_count('users', "state = 'on'");
}
}
function old_and_bad_method_to_count($table, $sql_cond) {}
So its look terrible.
I need to refactor that code a little bit, because it's use a dangerous function instead of Query/Builder or Eloquent/Model and looks bad.
I came up with an idea that I will use a Models and create Interface ElementsCountable and all models that do not have an interface will use the Model::all()->count(), and those with an interface will use the interface method:
foreach ($modules as $module) {
$className = $module->getModelName();
if($className) {
$modelInterfaces = class_implements($className);
if(isset($modelInterfaces[ElementsCountable::class])) {
/** #var ElementsCountable $className */
$count = $className::countModuleElements();
} else {
/** #var Model $className */
$count = $className::all()->count();
}
}
}
in method getModelName() i use a const map array (table -> model) which I created, because a lot of models have custom table name.
But then I realize that will be a good way, but there is a few records in Modules that use the same table, for example users_off which use the same table as users, but use other condition - state = 'off'
So it complicated things a little bit, and there is a right question: There is a good way to extends User and add scope with condition on boot?
class UserOff extends User
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(function (Builder $builder) {
$builder->where('state', '=', 'off');
});
}
}
Because I have some concerns if this is a good solution. Because all method of that class NEED always that scope and how to prevent from method withoutGlobalScope() and what about other complications?
I think it's a good solution to create the UserOff model with the additional global scope for this purpose.
I also think the solution I would want to implement would allow me to do something like
$count = $modules->sum(function ($module) {
$className = $module->getModelName();
return $className::modulesCount();
}
I would create an interface ModulesCountable that mandates a modulesCount() method on each of the models. The modulesCount() method would return either the default count or whatever current implementation you have in countModuleElements().
If there are a lot of models I would probably use a trait DefaultModulesCount for the default count, and maybe the custom version too eg. ElementsModuleCount if that is consistent.

What is the right way to mutate objects to avoid side effects?

I have a next system of objects (simple example):
class Grid
{
public State $state;
// Other fields
public function __construct(State $state)
{
$this->state = $state;
}
// ...
}
class State
{
public bool $isCompleted = false;
public ?User $judge;
}
class User
{
}
Disclaimer: Grid class is a legacy ActiveRecord model which can't be covered by isolated unit tests because it writes to a database and changes some other data in the system. So I'm only interested in State class.
I need a mutator class for State. It must be easy to test. It's look like this:
class StateMutator
{
public function mutate(State $state, array $changes):?State
{
// ...
$state->isCompleted = true;
// ...
if(!$someCondition){
return null;
}
// ...
return $state;
}
}
And it's used in this way:
/** #var Grid $grid */
/** #var array $changes */
$newState = (new StateMutator())->mutate($grid->state, $changes);
if($newState !== null){
$grid->state = $newState;
}
// Some other changes in $grid
$grid->saveChanges();
It looks good. But something confuses me. If the mutator does some changes in an obtained object and returns null after this, so calling code will thinks that State hasn't changed - makes some other changes in it and save it to the database. But because PHP pass objects by reference, changes which the mutator made in the state object will be save to the database too. And it's a problem.
What should I do to avoid this problem?
I have two ways to solve this problem, but both of them have big problems.
If the mutator can't change the state object in any place inside it, it should revert changes which it has already done. But it's difficult to do or even impossible in some cases.
The mutator should clone the state, mutate it's copy and return it. But in this case the method will need much more memory (the state can have more then 1000 objects in it's property).
May be someone have any idea?
I would generally avoid that objects can get into an invalid state. Your State object should have methods that either mutate its state or return a clone with the new state. These methods validate the input and mutate/return a clone only if the resulting state is valid. No need to keep track of changes and rolling back.

Laravel moving Controller logic to a Model

I'm at a stage where I'm refactoring my code, and I've come across an interesting conundrum.
In my ArticleController I have a bog standard store method for storing an article in my articles database table.
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(StoreArticle $request)
{
$article = new Article();
$defauultPublished = "draft";
$IntranetOnly = false;
$isFeatured = false;
$isFeatured = ($request->get('featuredArticle') == "1" ? true : false);
$IntranetOnly = ($request->get('IntranetOnly') == "1" ? true : false);
$article->title = $request->get('title');
$article->slug = str_slug($request->get('title'));
$article->author = $request->get('author');
$article->category = $request->get('category');
$article->excerpt = $request->get('excerpt');
$article->content = clean($request->get('content'));
$article->featuredImage = $request->get('featuredImage');
$article->featuredVideo = $request->get('featuredVideo');
$article->readingTime = $this->calculateReadTime($request);
$article->featuredArticle = $isFeatured;
$article->IntranetOnly = $IntranetOnly;
$article->published = $defauultPublished;
$article->save();
$article->handleTags($request);
return redirect('editable/news-and-updates')->with('success', 'Article has been added');
}
I also have a function for calculating read time:
/**
* Calculate a rough reading time for an articles by counting the words present
* These words are then divided by a given reading time and rounded to the nearest whole number
* Reading time average is roughly 267 words per minute, so this also accounts for relatively slow readers
*
* #param Request $request
* #return void
*/
public function calculateReadTime(Request $request)
{
$readingSpeed = 200;
$title = str_word_count(strip_tags($request->get('title')));
$excerpt = str_word_count(strip_tags($request->get('excerpt')));
$content = str_word_count(strip_tags($request->get('content')));
$words = ($title + $excerpt + $content);
$minutes = round($words / $readingSpeed);
return $minutes . ' minute' . ($minutes == 1 ? '' : 's');
}
My question is should these methods be moved to the Article model?
Controller should be as slim as possible. Following a resourceful approach (which you seem to be doing), the store() method in your ArticleController class should strive as much as possible to look like this:
class ArticleController extends Controller
{
public function store(CreateArticleRequest $request)
{
$article = Article::create($request->validated());
// Redirect with success message
}
}
Here, your request data is validated in a form request class before it even reaches the controller method; and then an Article model instance is created from that validated data.
A couple of other notes…
Statements like ($data['featuredArticle'] == "1" ? true : false) are overly verbose. You’re doing a condition check which will evaluate to true or false; you don’t need to manually return each value in a ternary operator. So this could be slimmed down to $data['featuedArticle'] == '1'. Furthermore, if you pass a value of 0 by default, then you could just get rid of the check entirely. If in your Blade template you put a hidden input before your checkbox:
<input type="hidden" name="featuredArticle" value="0" />
<input type="checkbox" name="featuredArticle" value="1" />
Then 1 will be send if the checkbox is checked (as it overrides the hidden input’s value, or 0 sent if the checkbox isn’t checked).
Also, try to stick to Laravel conventions to make your life easier. If you use snake_case for your input names, then it just makes life easier matching them up to model attribute and table column names. So use featured_article, have an attribute in your model with the same name, which maps to a database column with the same name again. This allows you to do shorthand calls like create() (as per my controller example) and update().
Finally, methods like calculating reading time definitely belong on your model. Models represent something in your application. It therefore follows that you can do things with your models. Calculating the time to read an Article model instance therefore lends itself to having a calculateReadingTime() method on the Article model.
A bit long-winded, but hopefully there should be some helpful pointers for you in the above. I’ve been working on Laravel projects for around five years now and have found that this approach and conventions is what works best.
Your controller's store article is fine, because it fills your article instance based on request data. It could use some refactoring and you could encapsulate more logic into your Article (for example, assign slug field inside your Article model whenever title is changed and so on).
But the line $article->handleTags($request); is a suspect, because your model should never operate with requests - it will quickly polute your model code with very specialized dependencies that you don't want (what happens when you receive your tags from cache and don't have a request instance? What happens if other type of request contains tags differently? and so on). Your model shouldn't have knowledge about requests or other parts of your app. Your controller is connecting the dots between them, so make sure your handleTags takes some basic abstract types/structures as a parameter (for example, an array) and make sure your controller takes and transforms data from request accordingly before feeding it to your article.
As for your calculateReadTime dilemma, it should definitely be inside your model. Think about it this way - do you have everything you need to calculate read time of your article inside your Article model? The answer is yes, it's a property of an article object, doesn't matter if you store it in DB or calculate it off other properties. Make getReadTime method. You don't want a controller to compute something about your model because it will tie that logic to a specific place in your app which is bad (what happens when you need to calculate read time of an article in other controller? Other model? and so on).
Make sure you read about has and is concepts regarding object-oriented design, it will help you immensely.
I think you should move those assignments to a Service Class. You could also go ahead and create a repository class. This would thus become your code structure:
Controller -> Service -> Repository -> Model.
Doing this $article = new Article(); is bad. You will have a had time when writing a test for your controller store method.
I would suggest you do this:
Create a Service class, say ArticleService.php. Define a store method in it.
ArticleService.php
use Article;
class ArticleService {
protected $article;
public function __construct(Article $article){
$this->article = $article;
}
public function store(array $data){
$defauultPublished = "draft";
$IntranetOnly = false;
$isFeatured = false;
$isFeatured = ($data['featuredArticle'] == "1" ? true : false);
$IntranetOnly = ($data['IntranetOnly'] == "1" ? true : false);
$this->article->title = $data['title'];
$this->article->slug = str_slug($data['title']);
$this->article->author = $data['author'];
$this->article->category = $data['category'];
$this->article->excerpt = $data['excerpt'];
$this->article->content = clean($data['content']);
$this->article->featuredImage = $data['featuredImage'];
$this->article->featuredVideo = $data['featuredVideo'];
$this->article->readingTime = $data['reading_time'];
$this->article->featuredArticle = $isFeatured;
//Capital letter I? You should be consistent with your naming convention
$this->article->IntranetOnly = $IntranetOnly;
$this->article->published = $defauultPublished;
if($this->article->save()){
$this->article->handleTags($request);
return true;
}
return false;
}
}
And your Controller now becomes:
class ArticleController{
protected $articleService;
public function __construct(ArticleService $articleService){
$this->articleService = $articleService;
}
public function store(Request $request){
//Some Validation Logic
$readingTime = $this->calculateReadTime($request)
$data = array_merge(['reading_time' => $readTime], $request->all());
return $this->articleService->store($request->all());
}
}
I also see that you are not validating the incoming Request. You should always do that because you can/should never trust your users to always provide/input the right data. It is your duty to force them to do that. e.g I as your user might decide to enter my name in your email field. If you don't validate that data, you will end up with wrong data.
There is also the issue of individually assigning your request parameter to their corresponding Model attribute. I decided to leave it that way so as not to overload you with information.
In summary, just take a look at the following resources for more insight.
https://laravel.com/docs/5.1/quickstart-intermediate
https://laravel.com/docs/5.6/validation
In short, read up the whole Laravel documentation! Goodluck!

Design pattern for repetitive switch in getters?

I've an ORM model (PHP Active Record), say, for a blogging system. I've something that's a post model that stores the number of likes. The post could either be a picture or quote (say), and they are different tables (and hence models).
The schema is that a post holds data like number of shares, likes, description, etc. along with either a picture or a quote.
So when writing getters for the post model I'm having to write
public function getX() {
if ($this->isPicture()) {
return $this->picture->getX();
}
else if ($this->isQuote()) {
return $this->quote->getX()
}
else {
return self::DEFAULT_X
}
}
I'm currently having to write this structure for many getter. Is there something I can do to avoid that?
PS: Tagged as PHP because that's my code in.
EDIT
Changed comments to code.
This is a model (and a corresponding table in the DB) that has more data than just a picture and quote. Example, description that's part of the post and doesn't reside on either the picture or the quote.
There's tables for pictures and quotes.
Using PHP Active Record and each of the three classes extends the generic model class provided by PHP Active Record.
The picture model has it's own data. Same for quote.
To expand on the idea of the Strategy pattern mentioned in the comments:
class Post {
// get the correct 'strategy'
public function getModel() {
if ($this->isPicture()) {
return $this->picture;
}
if ($this->isQuote()) {
return $this->quote;
}
return null;
}
// using the strategy
public function getX() {
$model = $this->getModel();
if (null === $model) {
return self::DEFAULT_X;
}
return $model->getX();
}
}
Each strategy would presumably implement the same interface as the Post class for exposing those getters. Even better would be to provide a default strategy (rather than returning null) and have that return the default values. That way, the null check in each getter becomes redundant.
An alternative approach to this is a very basic form of metaprogramming. The idea is that you go a level higher than calling your methods by hand, and let the code do it for you.
(Assume that the method definitions are all part of Post)
public function getX($model = null) {
if ($model) return $model->getX();
else return self::DEFAULT_X;
}
// usage
$postModel->getX($pictureModel);
What's happening here is that, in this single instance of getX in your Post model, you're passing in the name of another class, and executing the `getX' method on that instance (if it exists and is callable).
You can extend this in other ways. For example, maybe you don't want to pass an instance in, when the method can do it anyway:
public function getX($model_name = null) {
if ($model_name && $class_exists($model_name) && is_callable(array($model_name, 'getX')) {
$model = new $model_name;
return $model->getX();
} else {
return self::DEFAULT_X;
}
}
// usage
$postModel->getX('Picture');
In this instance, you pass the model in as a string, and the method will do the rest. While this makes it quicker to get what you want, you might find that you don't want to work with fresh instances all the time (or you can't), so there's a bit of a trade-off with this 'convenient' way.
That still doesn't fully solve your problem, though, since you still have to repeat that for each getter, over and over again. Instead, you can try something like this:
public function __call($method, $args) {
$class = $args[0];
if (class_exists($class) && is_callable(array($class, $method))) {
$model = new $class;
return $model->$method();
}
}
// usage
$postModel->getX('Picture');
$postModel->getY('Quote');
$postModel->getZ('Picture');
If you call a function that doesn't exist on the Post model, that magic method will be called, and it'll fire up a new instance of the model name you supply as an argument, and call the getWhatever method on it, if it exists.
It's important to note that you must not define these getters in Post, unless you want to override the methods in the other classes.
There is still the problem of this creating new instances all the time, though, and to remedy this you can use a bit of dependency injection. This means that you let the Post class contains a list of other instances of classes that it wants to use in future, so you can add and remove them at will.
This is what I would consider the actual solution, with the other examples hopefully showing how I've got here (will edit to clarify things, of course).
public $models = array();
public function addModel($instance) {
$this->models[get_class($instance)] = $instance;
}
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
}
}
// usage
$this->addModel($pictureModel);
$this->addModel($quoteModel);
$this->getX('Picture');
$this->getY('Quote');
Here, you're passing in your existing instances of models into the Post class, which then stores them in an array, keyed by the name of the class. Then, when you use the class as described in the last example, instead of creating a new instance, it will use the instance it has already stored. The benefit of this is that you might do things to your instances that you'd want reflected in the Post model.
This means that you can add as many new models as you like that need to plug into Post, and the only thing you need to do is inject them with addModel, and implement the getters on those models.
They all require you to tell the class what models to call at some point or another. Since you have an array of dependent models, why not add a way to get everything?
public function __call($method, $args) {
$class = $args[0];
if (array_key_exists($class, $this->models)) {
$model = $this->models[$class];
if (is_callable(array($model, $method)) {
return $model->$method();
}
} elseif ($class === 'all') {
// return an array containing the results of each method call on each model
return array_map(function($model) use ($method) {
if (is_callable(array($model, $method) return $model->$method();
}, $this->models);
}
}
// usage
$postModel->getX('all');
Using this, you'll get an array containing the return values of each getX method on each model you added with addModel. You can create pretty powerful functions and classes that do all this stuff without you having to repeat tedious logic.
I have to mention that these examples are untested, but at the very least I hope the concept of what you can do has been made clear.
Note:
The same thing can be applied to __GET and __SET methods, too, which are used for accessing properties. It's also worth saying that there may be the slight risk of a library already using these magic methods, in which case you'll need to make the code a little more intelligent.

Table joins accross multiple database tables and objects in PHP and MySQL

I have 4 classes, each with their own database tables:
GlobalObject
Image
Project
Page
GlobalObject has 3 properties
global_object_id
added_by
datetime_added
Image, Project and Page have many different fields, they ALL have the following though:
id
global_object_id (Foreign key)
At the moment, my class structure has Images, Projects and Pages as subclasses of the GlobalObject class, this allows the 3 subclasses access to the variables that are required of them, datetime_added etc.
How should I set the properties using PHP and MySQL? At the moment, ALL of the fields (including those in the global_object table) are in each section's table (except global_object_id which cannot exist without the new table - and is why I need to do this), So i request the data from the Image table for example, which includes all the properties in the parent class and is set using parent::set_by_row($database_args), and $this->set_by_row($database_args);
At the moment I do the following (Using Image as an example):
class Image extends GlobalObject
{
$id;
$title;
function set_by_id($id)
{
// Select Q from Image table
$this->set_by_row($db_result);
}
function set_by_row($args)
{
parent::set_by_row($args);
// Set class properties
}
}
To reiterate: I want to be able to do the following:
$image = new Image()
$image->set_by_global_object_id(23);
And the image be set, including everything in the parent class (which is stored in a separate table)
Please ask away if any of that is unclear.
A very convenient way is offered by those "magic methods", like __get and __set. The following example provides read-only access to the data, but can easily be extended with a __set method.
abstract class GlobalObject {
protected $_data = array();
/* Magic Method: Enable read access via $this->name */
public function __get($key) {
if (isset($this->_data[$key])) {
return $this->_data[$key];
}
throw new Exception('Property "'. $key .'" not found.');
}
protected function _setFromArray(array $data) {
// .. do whatever needs to be done with the data ..
$this->_data = $data;
}
}
Now just extend this class:
class Image extends GlobalObject {
public function setByGlobalId($id) {
$result = mysql_query('SELECT i.*, g.* FROM images AS i
LEFT JOIN global_objects AS g
ON g.global_object_id = i.global_object_id
WHERE i.global_object_id = '. intval($id) .' LIMIT 1');
// #todo check if result is empty
$data = mysql_fetch_assoc($result);
$this->_setFromArray($data);
}
}
And a simple example:
$image = new Image();
$image->SetByGlobalId(42);
echo $image->added_by;

Categories