Trouble initializing Facebook component

Hi,

I have trouble initializing the Facebook Component. Here’s the code




#Facebook component

class FBCore extends CApplicationComponent 

{

    public $appId;

    /**

     * @var string OAuth consumer secret.

     */

    public $appSecret;

    

	public $fb;

	

	public function __construct($appId, $appSecret)

	{

		$this->appId		=	$appId;

		$this->appSecret	=	$appSecret;

		

		$this->fb	=	new Facebook(array(

				'appId'		=>	$this->appId,

				'secret'	=>	$this->appSecret,

			));			

	}

	

	public function init()

	{

		parent::init();

	}

	

	public function getInstance()

	{

		return $this->fb;

	}

}

In my main configuration file, I have this snippet,


'components' => array(	

				'fbcore' => array(

					 'class'		=> 'common.components.FBCore',

					 'appId'		=>	$appId,

					 'appSecret'	=>	$appSecret,

				),

        ),

I try to access my component like


Yii::app()->fbcore;	

I receive this error:Notice: Undefined property: CWebApplication::$fbcore in …

How to

  1. properly define a component

  2. pass arguments to component (should I have to use constructor or init() method to initialize)

  3. inherit 2 classes. I need to inherit CApplicationComponent and Facebook’s official Facebook Class

  4. access the component created

Enjoy this :




<?php

#Facebook component

class FBCore extends CApplicationComponent {


	//we want to use setters and getter for the public vars.

	protected $_appId;

	protected $_appSecret;


	//this won't be set via config properties!

	protected $_facebook;


	public function init()

	{

		parent::init();

	}


	// this can be called the Facebook singleton <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/smile.gif' class='bbc_emoticon' alt=':)' />

	// it will instantiate the facebook original lib and return a reference to it.

	public function getFacebook()

	{

		if($this->_facebook!==null)

			return $this->_facebook;


		Yii::import('path.to.the.PHPSDK.Facebook');

		$this->_facebook=new Facebook(array(

			'appId'=>$this->getAppId(),

			'secret'=>$this->getAppSecret(),

		));


		return $this->_facebook;

	}


	// this will be called right at component initialization, 

	// thus making $this->_appId available in the component.

	public function setAppId($str)

	{

		$this->_appId=$str;

	}


	public function getAppId()

	{

		return $this->_appId;

	}


	public function setAppSecret($str)

	{

		$this->_appSecret=$str;

	}


	public function getAppSecret()

	{

		return $this->_appSecret;

	}


	// now, as far as i know the original facebook library has some nice methods, so why write them here again ?

	// instead, let's make use of PHP's __call function and call those methods 

	// basically, this is like a class inheritance(class FBCore extends Facebook)


	// note, __call() will be called only if a requested method doesn't exists in this component class.

	public function __call($method, $args)

	{

		$facebook=$this->getFacebook();

		// if the method exists in the fb class, call it, otherwise let PHP handle the error.

		if(method_exists($facebook, $method))

		{

			return call_user_func_array(array($facebook, $method), $args);

		}

	}


}



L.E:

With the above implementation, you can use FB like:




Yii::app()->fbcore->getUser();// in this case, __call will be called because your component does not have a method called getUser() but facebook class does.

Yii::app()->fbcore->facebook->getUser();// in this case you access the fb lib directly, thus __call won't be called.



[size=2][color="#006400"]/* moved to extensions */[/color][/size]

Wow!!! works like a magic! But the CApplication document says that we have to use the init() method to instantiate the component from configuration? How come this works without making use of init() method? How setAppId/setAppSecret called automagically? We didn’t specify anywhere that such methods should be called automatically. right? Even in the configuration, I have just passed the property values that shouldn’t trigger any methods?

Your trick on __call to implement Class Inheritance is great! But, do you think this is the optimal solution? Because, I didn’t see any such tricks used in Yii’s core components (db, for eg.)

You made my day so easier, Thanks a lot!

In case you’re interested, here’s my implementation:

FacebookConnect.php





<?php

/**

 * FacebookConnect class file.

 * @author Christoffer Niska <ChristofferNiska@gmail.com>

 * @copyright Copyright &copy; Christoffer Niska 2011-

 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License

 */


/**

 * Facebook connection application component.

 */

require(dirname(__FILE__) . '/../../vendors/facebook/facebook.php'); // Yii::import() will not work in this case

class FacebookConnect extends CApplicationComponent

{

	/**

	 * @property string Facebook application id.

	 */

	public $appId;

	/**

	 * @property string Facebook application secret.

	 */

	public $appSecret;

	/**

	 * @property string the application namespace.

	 */

	public $appNamespace;

	/**

	 * @property boolean whether file uploads are enabled.

	 */

	public $fileUpload;


	protected $_userId;

	protected $_facebook;


	/**

	 * Initializes this component.

	 */

	public function init()

	{

		$config = array(

			'appId' => $this->appId,

			'secret' => $this->appSecret

		);


		if ($this->fileUpload !== null)

			$config['fileUpload'] = $this->fileUpload;


		$this->_facebook = new Facebook($config);


		parent::init();

	}


	/**

	 * Logs in the current user using facebook.

	 * @return boolean whether the user was logged in successfully

	 */

	public function login()

	{

		$profile = $this->api('me');

		$user = User::model()->findByAttributes(array('email'=>$profile['email']));


		if (!empty($profile))

		{

			if ($user === null)

			{

				$user = new User();

				$user->name = $profile['username'];

				$user->email = $profile['email'];

				$user->fbuid = $profile['id'];

				$user->status = User::STATUS_ACTIVATED;

				$user->activated = new CDbExpression('NOW()');

			}


			$user->save(false);

		}


		if ($user !== null)

		{

			// NOTE: Facebook users are not authenticated using a password

			// so we can simply generate a random one to prevent misuse.

			$identity = new UserIdentity($user->name, $user->password);

			$identity->authenticate(UserIdentity::AUTH_TYPE_FACEBOOK);


			if ((int) $identity->errorCode === UserIdentity::ERROR_NONE)

			{

				$duration = 3600 * 24 * 30; // 30 days

				Yii::app()->user->login($identity, $duration);

				return true;

			}

		}


		return false;

	}


	/**

	 * Registers an Open Graph action with Facebook.

	 * @param string $action the action to register.

	 * @param array $params the query parameters.

	 */

	public function registerAction($action, $params=array())

	{

		if (!isset($params['access_token']))

			$params['access_token'] = $this->facebook->getAccessToken();


		$this->api('me/'.$this->appNamespace.':'.$action, $params);

	}

	

	/**

	 * Returns the model for the currently logged in Facebook user.

	 * @return User the user model.

	 */

	public function loadUser()

	{

		$fbuid = $this->getUserId();

		return $fbuid > 0 ? User::model()->findByAttributes(array('fbuid'=>$fbuid)) : null;

	}


	/**

	 * @return integer the Facebook user id.

	 */

	public function getUserId()

	{

		if ($this->_userId !== null)

			return $this->_userId;

		else

		{

			$userId = 0;


			try

			{

				$userId = $this->_facebook->getUser();

			}

			catch (FacebookApiException $e)

			{

			}


			return $this->_userId = $userId;

		}

	}


	/**

	 * Calls the Facebook API.

	 * @param string $query the query to send.

	 * @param array $params the query paramters.

	 * @return array the response.

	 */

	public function api($query, $params=array())

	{

		$data = array();


		if (!empty($params))

			$query .= '?'.http_build_query($params);


		try

		{

			$data = $this->_facebook->api('/'.$query);

		}

		catch (FacebookApiException $e)

		{

		}


		return $data;

	}


	/**

	 * @return Facebook the Facebook application instance.

	 */

	public function getFacebook()

	{

		return $this->_facebook;

	}

}



UserIdentity.php (used by FacebookConnect::login)




/**

 * UserIdentity class file.

 * @author Christoffer Niska <ChristofferNiska@gmail.com>

 * @copyright Copyright &copy; Christoffer Niska 2011-

 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License

 */


class UserIdentity extends CUserIdentity

{

	const AUTH_TYPE_DATABASE = 1;

	const AUTH_TYPE_FACEBOOK = 2;


	const ERROR_STATUS_INVALID = 10;


	private $_id;


	/**

	 * Authenticates a user.

	 * @param integer $type the authentication type, defaults to database.

	 * @return boolean whether authentication succeeds.

	 */

	public function authenticate($type = self::AUTH_TYPE_DATABASE)

	{

		switch ($type)

		{

			// Facebook authentication

			case self::AUTH_TYPE_FACEBOOK:

				$user = Yii::app()->fb->loadUser();


				if ($user === null)

					$this->errorCode = self::ERROR_USERNAME_INVALID;

				else if ((int) $user->status !== User::STATUS_ACTIVATED)

					$this->errorCode = self::ERROR_STATUS_INVALID;

				else

				{

					$this->_id = $user->id;

					$this->username = $user->name;

					$this->setState('fbuid',$user->fbuid);

					$this->setState('isAdmin', $user->admin);

					$this->errorCode = self::ERROR_NONE;

				}


				break;


			// Default authentication (name, status, password)

			case self::AUTH_TYPE_DATABASE:

			default:

				/** @var User $user */

				$user = User::model()->find('LOWER(name)=?', array(strtolower($this->username)));


				if ($user === null)

					$this->errorCode = self::ERROR_USERNAME_INVALID;

				else if (!$user->validatePassword($this->password))

					$this->errorCode = self::ERROR_PASSWORD_INVALID;

				else if ((int) $user->status !== User::STATUS_ACTIVATED)

					$this->errorCode = self::ERROR_STATUS_INVALID;

				else

				{

					$this->_id = $user->id;

					$this->username = $user->name;

					$this->setState('isAdmin', $user->admin);

					$this->errorCode = self::ERROR_NONE;

				}


				break;

		}


		return $this->errorCode == self::ERROR_NONE;

	}


	/**

	 * @return integer the ID of the user record

	 */

	public function getId()

	{

		return $this->_id;

	}

}



It works fine but I haven’t had time to release it as an extension yet. There might be some security issues so use at your own risk.

Well, the init method is there because it is called when the application component is instantiated, so you might place additional code to be executed there.

In case i don’t use it, i place the init() method anyway, maybe because i miss the contructor, dunno, but i consider it a good practice.

The application component makes use of the php __get/__set magic methods, and the component inherits this when it extends CApplicationComponent, really, it’s nothing special here, but the fact the components pattern is so well designed.

For more info you should read about __get/__set and also take another read on how components are being created in Yii, maybe toy a bit with a test component and see how it works.

__call is another magic method, just like __get/__set, and yes, it is a optimal method in your case.

Usually, you’d use behaviors for this kind of inheritance, but since your facebook SDK class is a standalone class and not a class derivated from IBehavior, then it made more sense to use __call().

You’re welcome :)

After reading some chapters on Components & Behaviors, I believe it could be even better define a Facebook Component that is attached to FacebookBehavior which in turn inherits Facebook SDK class. Because I thought of writing Behaviors for FQL, Graph API so the calls to Facebook will more in Yii way so changes to Facebook’s SDK will be abstracted. This makes more sense, because we can raise events on Facebook component such as onLike, onAuthorize, onDeauthorize, etc

FacebookQueryLanguage extends CModelBehavior {}

FacebookGraphApi extends CModelBehavior {}

FacebookOpenGraph extends CModelBehavior {}

and these behaviors will be attached to Facebook component as and when required.

I’m sorry, I’m not convinced with __call technique to implement mixin pattern since Yii provides behaviors for that. Moreover, Yii::app()->fb->api() fails because it is implemented as polymorphic that does not fit with __call (whereas Yii::app()->fb->getUser() works correctly). For the api() method failure, currently, I’m returning the fb instance, then calling the api() method on that instance.

I have few things in my mind, but I couldn’t express it clearly. Are you able to understand what I’m coming to say?

If you use behaviors as you intend, it basically means you will have to modify the original FB class and then keep track of the changes when a new version is released so that you can implement them in your class.This is a thing i always avoid, mainly because it is a waste of time when you are like me and you use many 3rd party classes/libraries.

If the api method won’t work as it should with __call then just inherit it in your component class and adjust it as needed. It’s just a class method to keep track of, not an entire class.

In fact, I’m going to modify the class since Facebooks apps are the only kind of apps that I develop using Yii so I don’t mind spending time maintaining the Facebook class (In fact, Facebook encourages maintaining our own version of Class). Could you advice me how I can do that one? I read the doc & other threads, they all say behaviors can be attached to models, but I haven’t seen a single example how behaviors are attached to Components. I tried like this

  1. Inherited the Facebook’s class into my Facebook class to further inherited by CModelBehavior

  2. Tried to attach that behavior into My Facebook Component, but I didn’t know where to write that code.

Hi, your approach seems to be good, but I feel more comfortable with twisted1919 approach since it encourages loosely coupling and can be extended well in future. Instead of UserIdentify, I use FacebookUserIdentify (so I will also have GPlusUserIdentity in future) and I don’t have trouble using Yii::import for including Facebooks class, etc. Anyhow, thanks for the snippet.

How about: http://www.yiiframework.com/doc/api/1.1/CComponent#attachBehavior-detail

i haven’t really tried but i think it might be like:




class mySuperComponent extends CAplicationComponent{

   

   public function init()

   {

      parent::init();

      $this->attachBehavior('facebook',array('class'=>'path.to.BehaviorClass','property1'=>'value1','property2'=>'value2'));

   }


}



Then, having the behavior attached, accessing $this->facebook from inside your component will get you access to your behavior methods.