Single Responsibility Principle. How can I design this code properly?

Hi guys;

I have been struggling with this. I really appreciate some help. I don’t know how to learn this one.

I understand that a class should be responsible for doing only 1 thing. That is a simple concept. But practising that is not that simple.

For example I have a class that sends newsletters out and it is a big big big big mess.

I tried to break it down to different components but I dont know what is the right approach. I appreciate if someone puts me in the right direction.

I know model should be separate from view and controller but how to apply it to this one I am not sure.

Maybe place 1 or 2 of the methods in a another class? Or another location? I am not sure. Here is the code anyways.

Thanks for your help.

     <?php
        class SendCampaign extends BaseController {
            public function __construct(){
                $this->beforeFilter('login_check');
            }

            public function send($id){
                //Set data
                $data['name'] = $name = ucfirst(Input::get('name'));
                $data['email'] = $email = Input::get('email');
                $data['receiver'] = Input::get('email');
                $time_sent = time();
                //Validate posted data
                $validator = Validator::make(   array('email' =>$email ),   array('email' => array('required', 'email')));

                if ($validator->fails()){
                    Session::put('campaign_sent', '<p class="text-error">Invalid email address</p>');
                    return Redirect::to('email');
                }

                //Load campaign
                $results = Campaign::find($id);
                $view_file = $results->view_file;
                //Attach deals

                if(is_array(Input::get('deals'))){
                    $in = "(";
                    foreach(Input::get('deals') as $val){
                        $in .= $val.",";
                    }

                    $in = substr($in, 0, -1);
                    $in .= ")";
                    echo $in;
                    $data['deals']="";
                    $deals = DB::select('SELECT  gomSite_deals.id AS DID,
                        gomSite_deals.title AS DT,
                        gomSite_deals.status AS DSTAT,
                        gomSite_deals.original_value AS OV,
                        gomSite_deals.end_date AS end_date,
                        gomSite_deals.deal_value AS DV
                        FROM gomSite_deals INNER JOIN
                        gomSite_business ON gomSite_business.id = gomSite_deals.business_id
                        WHERE gomSite_deals.id IN '.$in.' GROUP BY gomSite_deals.id
                                                ');
                    $data['deals'] .= "<table><tr>";
                    foreach($deals as $val =>$row){
                        $seconds_left = strtotime($row->end_date) - time();
                        $days_left = floor($seconds_left/(3600*24));

                        if($days_left<0){
                            $days_left = 0;
                        }

                        $discount = round(($row->DV*100)/$row->OV);
                        $discount = 100 - $discount;
                        $data['deals'] .= "<td>
                                    <table style = \\"float:left; width:350px;\\">
                                        <tr><td colspan = \\"2\\"><a href = \\"http://www.gomSite.com.au/details/show/".$row->DID."\\"><img src = 'http://gomSite.com.au/uploads/".$row->DID.".jpg' width=\\"250px\\" height = \\"250px\\" style = \\"-moz-border-radius: 10px; border-radius: 10px;\\"></a></td></tr>
                                        <tr><td colspan = \\"2\\"><a style = \\"; text-decoration:none;\\" href = \\"http://www.gomSite.com.au/details/show/".$row->DID."\\"><strong style = \\"font-family:Arial; font-size:16px; text-decoration:none;\\">".$row->DT."</strong></a></td></tr>
                                        <tr>
                                            <td colspan = \\"2\\" style=\\"border-bottom:1px solid #cccccc\\"><br /><strong style = \\"color: #ED2930; margin:0; padding:0;
                                                font-family: Arial,Helvetica,sans-serif;
                                                font-size: 16px;
                                                font-weight: bold;\\">$".$row->DV."
                                                </strong>&nbsp;&nbsp;&nbsp;
                                                <strong style = \\"color: #202020; margin:0; padding:0; text-decoration:line-through;
                                                font-family: Arial,Helvetica,sans-serif;
                                                font-size: 16px;
                                                font-weight: bold;\\">$".$row->OV."
                                                </strong><br /><br />
                                            </td>
                                        </tr>
                                        <tr>
                                            <td><br />
                                                <span style = \\"color: #444444; margin:0; padding:0;
                                                font-family: Arial,Helvetica,sans-serif;
                                                font-size: 14px;\\"><img src = \\"http://gomSite.com.au/images/clock-image.png\\">&nbsp;".$days_left." days Left
                                                </span>&nbsp;&nbsp;&nbsp;
                                            </td>
                                            <td align = \\"right;\\">
                                                <span style = \\"color: #444444;
                                                font-family: Arial,Helvetica,sans-serif;
                                                font-size: 14px;
                                                \\"><img src = \\"http://gomSite.com.au/images/voucher-image.png\\">&nbsp;".$discount."% discount
                                                </span>
                                            </td>
                                        </tr>
                                            </table>
                                            </td>";
                    }

                    $data['deals'] .= "</tr></table>";
                    //exit();
                } else {
                    $data['deals'] = "";
                }

                //echo $data['deals'];exit();
                //Load sende'r signature details signature
                $sender_id = Session::get('id');
                $sender_details = Admin::find($sender_id);
                $data['sender_name'] = ucwords($sender_details->name);
                $data['sender_email'] = $sender_details->email;
                $data['position'] = $sender_details->position;
                $data['mobile'] = $sender_details->mobile;
                $data['phone'] = $sender_details->phone;
                //Check if view exists

                if (!View::exists($view_file)){
                    Session::put('campaign_sent', '<p class="text-error">The email template you are trying to send does not exists!</p>');
                    return Redirect::to('email');
                }

                //Send email
                Mail::send($view_file, $data,
                function($message){
                    $message->to(Input::get('email'), Input::get('name'));
                    $message->from('support@gomSite.com', 'gomSite Support');
                    $message->cc('gomSiteAdmin@yahoo.com', 'Behnam');
                    $message->subject(Input::get('email_title'));
                }

                );
                //Save report   gomSite_campaign_sent table
                $CampaignReport = new CampaignReport;
                $CampaignReport->name = $name;
                $CampaignReport->email = $email;
                $CampaignReport->campaign_id = $id;
                $CampaignReport->time_sent = $time_sent;
                $CampaignReport->save();
                //If member already not in this mailing list
                $results = DB::select('select * from gomSite_individual_campaign_signup where email = ?', array($email));

                if(count($results)==0){
                    //Save new member
                    $CampaignSignup = new CampaignSignup;
                    $CampaignSignup->name = $name;
                    $CampaignSignup->email = $email;
                    $CampaignSignup->campaign_id = $id;
                    $CampaignSignup->time_sent = $time_sent;
                    $CampaignSignup->save();
                }

                //If member already not exists
                $results = DB::select('select * from gomSite_mailinglist_member where email = ? AND mailing_list_id = ?', array(Input::get('email'), Input::get('mailinglist_id')));

                if(count($results)==0){
                    //Sign this user to gomSite_IC_MLG table //
                    $ICSML = new ICSML;
                    $ICSML->mailing_list_id = Input::get('mailinglist_id');
                    $ICSML->email = Input::get('email');
                    $ICSML->save();
                }

                //Redirect
                Session::put('campaign_sent', '<p class="text-error">Campaign sent successfully</p>');
                return Redirect::to('email');
            }

        }



Thanks

Quick disclaimer. I’m not as experienced with Laravel, so some syntax may be off, but you should be able to follow the principles.

With that out of the way, absolutely the first thing you should do is get the HTML out of your controller and into a template. For example,

<!-- Stored in app/views/campaign/deals.blade.php -->

<table><tr>

@foreach ($deals as $deal)
    <td>
    <table style = "float:left; width:350px;">
        <tr><td colspan = "2"><a href = "http://www.gomSite.com.au/details/show/{{ $deal->DID }}"><img src = 'http://gomSite.com.au/uploads/{{ $deal->DID }}.jpg' width="250px" height = "250px" style = "-moz-border-radius: 10px; border-radius: 10px;"></a></td></tr>
        <tr><td colspan = "2"><a style = "; text-decoration:none;" href = "http://www.gomSite.com.au/details/show/{{ $deal->DID }}"><strong style = "font-family:Arial; font-size:16px; text-decoration:none;">{{ $deal->DT }}</strong></a></td></tr>
        <tr>
            <td colspan = "2" style="border-bottom:1px solid #cccccc"><br /><strong style = "color: #ED2930; margin:0; padding:0;
                font-family: Arial,Helvetica,sans-serif;
                font-size: 16px;
                font-weight: bold;">${{ $deal->DV }}
                </strong>&nbsp;&nbsp;&nbsp;
                <strong style = "color: #202020; margin:0; padding:0; text-decoration:line-through;
                font-family: Arial,Helvetica,sans-serif;
                font-size: 16px;
                font-weight: bold;">${{ $deal->OV }}
                </strong><br /><br />
            </td>
        </tr>
        <tr>
            <td><br />
                <span style = "color: #444444; margin:0; padding:0;
                font-family: Arial,Helvetica,sans-serif;
                font-size: 14px;"><img src = "http://gomSite.com.au/images/clock-image.png">&nbsp;{{ $days_left }} days Left
                </span>&nbsp;&nbsp;&nbsp;
            </td>
            <td align = "right;">
                <span style = "color: #444444;
                font-family: Arial,Helvetica,sans-serif;
                font-size: 14px;
                "><img src = "http://gomSite.com.au/images/voucher-image.png">&nbsp;{{ $discount }}% discount
                </span>
            </td>
        </tr>
            </table>
            </td>
@endforeach

</tr></table>
$data['deals'] = View::make('campaign.deals', array('deals' => $deals));

Second is you should get the DB queries out of your controller and into a DB table class (whatever Laravel’s notion of that would be).

Those are two very significant improvements you could do, and we haven’t even touched the sending of the newsletters yet.

In addition, if you create an Eloquent ‘Deal’ model, you can move the logic that calculates the remaining days and the discount into methods of that model, which keeps it out of your controller.

Also, I noticed from browsing the Laravel docs that Eloquent has a firstOrCreate method which you could use to tidy up the creation of the ICSML entry:

$icsml = ICSML::firstOrCreate(array('email' => $email, 'mailing_list_id' => Input::get('mailinglist_id')));

I realise the best way is to understand Dependency Injection first :slight_smile:

Can anyone tell me if this is the right approach?


$payment_method = new credit_card_payment();
$object = new process_payment($payment_method);
echo $object->execute(2);


#Classes, interfaces and Jesus

interface payment_service
	{
		public function payment_option($details);
	}

class credit_card_payment implements payment_service
	{
		public function payment_option($details)
			{
				echo "CC method";
			}
	}

class paypal_payment implements payment_service
	{
		public function payment_option($details)
			{
				echo "Paypal method";
			}
	}	

class process_payment
	{
		private $payment_method;

		public function __construct(payment_service $payment_service)
			{
				$this->payment_method = $payment_service;
			}

		public function execute($details)
			{
				return $this->payment_method->payment_option($details);
			}	
	}

I don’t like the fact that I have to write this $payment_method = new credit_card_payment();

I am reading on PHP-DI container now.

It seems like a very simple concept BUT IT’S NOT.

Thanks

I’ve given some more thought as to how you might refactor that controller action, and I’m going to share my take on it. I’m still in the process of learning Laravel, so apologies if I’ve made any mistakes with the framework specific stuff, as it’s mostly based on browsing the docs.

Deals

Following on from previous comments, I started by moving some of those DB queries out of the controller. In the case of the deal query, it seems sensible to create a model class for that:


class Deal extends Eloquent {

    protected $table = 'gomSite_deals';

    public function getDaysLeftAttribute()
    {
        $seconds_left = strtotime($this->end_date) - time();
    	return floor($seconds_left / (3600*24));
    }

    public function getDiscountAttribute()
    {
    	$discount = round(($this->deal_value * 100) / $this->original_value);
    	return 100 - $discount;
    }

}

As your table names don’t follow the Laravel convention, I’ve added the $table property so it knows where to find the data. I’ve also added two accessor methods to encapsulate the logic for calculating the days left and the discount. Those can now be called directly in the deal view like {{ $deal->discount }}.

The original code that turned an array of deal ids into a string for the DB query is now unnecessary, as Eloquent allows us to return all the relevant deals just by passing in the array:

$deals = Deal::find(Input::get('deals'));

Setting the view data

A lot of code is taken up setting values in the $data array, to pass to the view. We could avoid setting all the sender values by pass in the whole sender object:


// Before
$sender_id = Session::get('id');
$sender_details = Admin::find($sender_id);

$data['sender_name'] = ucwords($sender_details->name);
$data['sender_email'] = $sender_details->email;
$data['position'] = $sender_details->position;
$data['mobile'] = $sender_details->mobile;
$data['phone'] = $sender_details->phone;

// After
$sender = Admin::find(Session::get('id'));
$data['sender'] = $sender;

Presentational formatting, like uppercasing the first character of the name, belongs in the view itself and can be applied with a view helper.

A Campaign Service

The send action currently contains a lot of business logic relating to sending a campaign. Creating a campaign service seems like a good next step, to allow us to move all that logic out of the controller.


class CamapaignService
{

	public function send(Campaign $campaign, Admin $sender, Recipient $recipient, $deals = array())
	{
        //Load campaign
        $view_file = $campaign->view_file;

		$data = array(
			'sender'    => $sender,
			'recipient' => $recipient,
			'deals'	 => View::make('campaign.deals', array('deals' => $deals));
		);
		$time_sent = time();

		//Send email
        Mail::send($view_file, $data, function($message) use ($recipient) {
            $message->to($recipient->email, $recipient->name);
            $message->from('support@gomSite.com', 'gomSite Support');
            $message->cc('gomSiteAdmin@yahoo.com', 'Behnam');
            $message->subject(Input::get('email_title'));
        });

        $this->createReport($recipient, $campaign, $time_sent);

        //If member already not in this mailing list
		if (! CampaignSignup::where('email', '=', $email)->first()) {
            $this->createSignup($recipient, $campaign, $time_sent);
        }

        //If member already not exists
        $icsml = ICSML::firstOrCreate(array('email' => $email, 'mailing_list_id' => Input::get('mailinglist_id')));
	}

	public function createReport(Recipient $recipient, Campaign $campaign, $time_sent)
	{
		$name  		 = $recipient->name;
		$email 		 = $recipient->email;
		$campaign_id = $campaign->id;

		return CampaignReport::create(compact($name, $email, $campaign_id, $time_sent));
	}

	public function createSignup(Recipient $recipient, Campaign $campaign, $time_sent)
	{
		$name  		 = $recipient->name;
		$email 		 = $recipient->email;
		$campaign_id = $campaign->id;

		return CampaignSignup::create(compact($name, $email, $campaign_id, $time_sent));
	}

}

I decided to create a simple Recipient class to allow the name and email of the receiver to be grouped together, and it allows us to type-hint on the send method to make sure it’s being passed the correct dependencies. It also simplifies the view $data array even more, as we can just pass in the Recipient object.

The Controller Action

Now the controller action is only responsible for collecting the input and passing it off to the service. It doesn’t do any of the sending itself. Having the logic in a service also means it’s very simple to call that same process from an API controller, or a command line script.


public function send($id)
{
    $name = Input::get('name');
    $email = Input::get('email');

    $validator = Validator::make( array('email' => $email), array('email' => array('required', 'email')) );

    if ($validator->fails()) {
        return Redirect::to('email')->with('campaign_sent', '<p class="text-error">Invalid email address</p>');
    }

    $sender    = Admin::find(Session::get('id'));
    $campaign  = Campaign::find($id);
    $recipient = new Recipient($name, $email);
    $deals     = Deal::find(Input::get('deals'));

    $campaignService = new CampaignService();
    $campaignService->send($campaign, $sender, $recipient, $deals);

    return Redirect::to('email')->with('campaign_sent', '<p class="text-error">Campaign sent successfully</p>');
}

It is simple, especially with Laravel, don’t rush into it & don’t panic, slow & steady wins the race. :slight_smile:

I’m assuming you allow selection of form field to determine payment type (credit card or paypal). So you would create a Service Provider first:


use Illuminate\\Support\\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider {

    public function register() {
        if ( Input::has( 'payment_service' ) && Input::get( 'payment_service' ) == 'card' ) {
            $this->app->bind( 'payment_service', 'credit_card_payment' );
        } else {
            $this->app->bind( 'payment_service', 'paypal_payment' );
        }
    }

}

and register this Service Provider in your config/app.php config file. If you’re not getting the payment service type field in form and use any other way of determining that then feel free to update the logic in the Service Provider.

Now that you have a Service Provider which dynamically binds a payment service class to your ‘payment_service’ interface, you can inject the interface easily and let Laravel do the heavy lifting of initializing the correct class.


#Classes, interfaces and Jesus

interface payment_service {
    public function payment_option( $details );
}

class credit_card_payment implements payment_service {

    public function payment_option( $details ) {
        echo "CC method";
    }

}

class paypal_payment implements payment_service {

    public function payment_option( $details ) {
        echo "Paypal method";
    }

}

class process_payment {

    private $_payment_method;

    public function __construct( payment_service $payment_service ) {
        $this->_payment_method = $payment_service;
    }

    public function execute( $details ) {
        return $this->_payment_method->payment_option( $details );
    }

}

Now all that remains is how to instantiate process_payment class. There are 2 ways: you can either inject it into whichever class you want to use it in and Laravel will handle the instantiation or you can create the object yourself by resolving it out of IoC container.

If you want to resolve it out of IoC container yourself then you just need to do this:


$object = App::make( 'process_payment' );
echo $object->execute(2);

Or if you want to inject it into a class where you’re going to use it then just typecast a parameter in constructor as ‘process_payment’ - just the same way you’ve done in process_payment constructor.