CakePHP 2.x Achievements - php

I'm attempting to build an achievements system in my CakePHP app using CakeEvents. I've been using the following website for helping me put the Events together: http://martinbean.co.uk/blog/2013/11/22/getting-to-grips-with-cakephps-events-system/
In my app, achievements are called badges and can be awarded to users. These badges are awarded based on rules that link the badge to a Cake event.
So for example if a user creates a post, that will fire the Model.Post.add event which should check if any rules exist for that event, and if so do the parameters match up, and again if all checks out then award a badge connected to that rule to the user.
The schema comprises of the following tables:
users
id
username
password
posts
id
title
content
created
modified
user_id
badges
id
title
description
events
id
title
name
event_badges
id
event_id
badge_id
badge_users
id
badge_id
user_id
Hopefully that all makes sense. And here are the Models to show how they connect.
User.php
class User extends AppModel
{
public $name = 'User';
public $hasMany = array(
'Post', 'Badge'
);
public $belongsTo = array(
'BadgeUser'
);
}
Badge.php
class Badge extends AppModel {
public $hasMany = array(
'BadgeUser'
);
public $actsAs = array('Containable');
}
Event.php
class Event extends AppModel {
public $hasMany = array(
'EventBadge'
);
public $actsAs = array('Containable');
}
EventBadge.php
class ActionBadge extends AppModel {
public $belongsTo = array(
'Action', 'Badge'
);
public $actsAs = array('Containable');
}
BadgeUser.php
class BadgeUser extends AppModel {
public $belongsTo = array(
'Badge', 'User'
);
public $actsAs = array('Containable');
}
** Feel free to comment if you think the schema is incorrect to achieve what is being described in this question.**
So example badges might be:
Title: First Post, Description: Awarded for creating a post
Title: 10 Posts, Description: Awarded for creating 10 posts
Title: Editor, Description: Editing a post
Title: Deleter, Description: Deleting a post
And some example rules in the Events table:
Title: Add Post, Event Model.Post.add
Title: Edit Post, Event: Model.Post.edit
Title: Delete Post, Event: Model.Post.delete
Title: View Post, Event: Model.Post.view
So as you can see the Events are linked to the above Badges and the Events are called using the CakeEvents system.
Okay so when a person does something, let's say saves a new post, I have the following in the Post model:
public function afterSave($created, $options = array()) {
if ($created) {
$event = new CakeEvent('Model.Post.add', $this, array(
'id' => $this->id,
'data' => $this->data[$this->alias]
));
$this->getEventManager()->dispatch($event);
}
}
The first question is, how do I pass different events to the afterSave? As both Add and Edit methods in the controller would fire this...
And then I have a PostListener.php file in /app/Event
class PostListener implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Post.add' => 'postAdded',
'Model.Post.edit' => 'postEdited',
'Model.Post.view' => 'postViewed',
'Model.Post.delete' => 'postDeleted',
);
}
public function postAdded(CakeEvent $event) {
$this->Badge->awardBadge($badgeId, $userId);
}
public function postEdited(CakeEvent $event) {
}
public function postViewed(CakeEvent $event) {
}
public function postDeleted(CakeEvent $event) {
}
}
So the next question is how do I link the event listener back up to my Events table?
And then award the badge connected to that action? Noting that some will need to do extra checks like a user must of created 10 posts to achieve the 10 Posts badged and not just because they have created a post.
In the Badge.php model I have the following function to award badges:
public function awardBadge($badgeId, $userId) {
$controlFind = $this->BadgeUser->find(
'first',
array(
'conditions' => array(
'badge_id' => $badgeId,
'user_id' => $userId,
)
)
);
if(!$controlFind) {
$temp = array(
'BadgeUser' => array(
'badge_id' => $badgeId,
'user_id' => $userId,
'created' => date('Y-m-d H:i:s')
)
);
$collection[] = $temp;
return $this->BadgeUser->saveAll($collection, array('validate' => false));
}
}
So I need to run the code above from the listener when things match up with the DB rules. Just struggling to make it all stick together.

I think your database schema may be complicating things by having a notion of events. Personally, I’d have a badges table that stores badge title and description (and also image path if it’s needed). I’d then have an event listener that corresponds to each badge, so there will be some degree of manual labour involved.
So let’s think of a sample scenario. Let’s say a badge is awarded when a user posts a 100 times. So you’d have your Post model that fires an event when a post is saved:
<?php
App::uses('CakeEvent', 'Event');
class Post extends AppModel {
public function afterSave($created, $options = array()) {
$event = new CakeEvent('Model.User.afterSave', $this);
$this->getEventManager()->dispatch($event);
}
}
You can then create a corresponding handler:
<?php
// Event/BadgeListener.php
App::uses('CakeEventListener', 'Event');
App::uses('ClassRegistry', 'Utility');
class BadgeListener implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Post.afterSave' => 'afterSaveListener'
);
}
public function afterSaveListener(CakeEvent $event) {
// check number of posts
$this->Post = ClassRegistry::init('Post');
$count = $this->Post->find('count', array(
'Post.author_id' => $event->subject()->data[$event->subject()->alias]['author_id'];
));
// award badge
if ($count > 100) {
// TODO: check user does not already have badge
$this->BadgeUser = ClassRegistry::init('BadgeUser');
$this->BadgeUser->create();
$this->BadgeUser->set('badge_id', 'whatever_badge_id_actually_is');
$this->BadgeUser->set('user_id', $this->Auth->user('id')); // logged in user ID
$this->BadgeUser->save();
}
}
}
This is a rough example written off-the-cuff, but hopefully it should steer you in the right direction.
If there’s anything you want me to clear up, let me know and I’ll do my best.

The first question - differentiate between new post and edit
To differentiate between add and edit you need to pass along the $created parameter when you create your CakeEvent. Something like this:
public function afterSave($created, $options = array()) {
//NOTICE: it's afterSave now, since this will be called after edit or new
$event = new CakeEvent('Model.Post.afterSave', $this, array(
'created' => $created, //NOW you will know when you process the even if this was a new post or post edit
'id' => $this->id,
'data' => $this->data[$this->alias]
));
$this->getEventManager()->dispatch($event);
}
You should add to the data of your event all the data that you will need later on to process it and to match your rules. You can even add the model object itself, if you think you will need that to do some queries with it.
Second Question - how to link event processing to the Events table
In the Event listener you can use the ClassRegistry to get an instance of the Event model (and any other models you require). You can do your rules matching and once you have all the data you need you save the badge to the database.
Since you now know if it's an edit or a new post (from the event->data) you can take appropriate action.
The editor id I guess you can take it with the Auth component as being the logged in user. So when you have that you can use it to read what you need about the editor from the database.
To conclude: when you send your events, use the event data to pass all the information that you will need in the listener. Event can carry so much more information, not just their names and the fact that they were triggered. You can send and then look at the entire "context".
In the listener, make good use of the data you get from the event and of the currently logged in user ID or the post author, do the matching that you need to do and then save the badge for the appropriate user.
Hopefully this is what you were looking for :).

Related

Symfony 3 rating form on several categories displayed on one page

On some Company (entity) page I'm displaying several Categories (entity) which the Company might have. For every of the categories displayed I need to add simple form with rating value. So I've created entity Called Rating and I've generated RatingType.
Now I have the problem with display this form for each category displayed, I can display the form only once for the first category occurrence, but it in the Rating Form it can't get the name (the id) of the category which it should be connected.
The Rating entity have defined Category and Company in relation ManyToOne (#ORM).
I would appreciate for help how can I handle with that.
I suppose that the trick sits in the Controller, so below is my code:
/**
* #Route("/catalog/{id}.html", name="card_show")
*/
public function cardShowAction(Company $company, Request $request)
{
$form = null;
// #TODO: add verification if user is logged on => if ($user = $this->getUser())
$rating = new Rating();
$rating->setCompany($company);
$form = $this->createForm(RatingType::class, $rating);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted()) {
$em = $this->getDoctrine()->getManager();
$em->persist($rating);
$em->flush();
$this->addFlash('success', "Your vote has been saved!");
return $this->redirectToRoute('card_show', array('id' => $company->getId()));
}
return $this->render("default/catalog/show.html.twig", array(
'card' => $company,
'form' => is_null($form) ? $form : $form->createView()
));
}
Here is the RatingType code:
class RatingType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('comment')
->add('vote')
// ->add('userName')
// ->add('ip')
// ->add('createdAt')
->add('service')
// ->add('company')
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Rating'
));
}
}
Each Symfony Form can be rendered in view only once (at least without nasty hacks).
I propose another approach. In your template render as many simple html forms (not binded with Symfony Forms at all!), as many categories you have. Then, give each of those form another action attribute (say "/catalog/{company_id}/category/{category_id}/vote"). Then, create new action, where you will parse, validate and use data from Request object.
If you really want to do all of this using Symfony forms you need to generate as many Symfony Forms instances, as many categories you will have. Each form needs to have unique name and action (or some hidden field) to distinct category user would like to vote.

Laravel Eloquent validation insert exception?

I've created a form which adds a category of product in a Categories table (for example Sugar Products or Beer), and each user has their own category names.
The Categories table has the columns id, category_name, userId, created_At, updated_At.
I've made the validation and every thing is okay. But now I want every user to have a unique category_name. I've created this in phpMyAdmin and made a unique index on (category_name and userId).
So my question is this: when completing the form and let us say that you forgot and enter a category twice... this category exist in the database, and eloquent throws me an error. I want just like in the validation when there is error to redirect me to in my case /dash/warehouse and says dude you are trying to enter one category twice ... please consider it again ... or whatever. I am new in laravel and php, sorry for my language but is important to me to know why is this happens and how i solve this. Look at my controller if you need something more i will give it to you.
class ErpController extends Controller{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return view('pages.erp.dash');
}
public function getWarehouse()
{
$welcome = Auth::user()->fName . ' ' . Auth::user()->lName;
$groups = Group::where('userId',Auth::user()->id)->get();
return view('pages.erp.warehouse', compact('welcome','groups'));
}
public function postWarehouse(Request $request)
{
$input = \Input::all();
$rules = array(
'masterCategory' => 'required|min:3|max:80'
);
$v = \Validator::make($input, $rules);
if ($v->passes()) {
$group = new Group;
$group->group = $input['masterCategory'];
$group->userId = Auth::user()->id;
$group->save();
return redirect('dash/warehouse');
} else {
return redirect('dash/warehouse')->withInput()->withErrors($v);
}
}
}
You can make a rule like this:
$rules = array(
'category_name' => 'unique:categories,category_name'
);

CakePhp multiple table data fetch on single page

I am new in cakePHP try to create a blog site where user add blog after category select, I have one categories table : fields: category_id, name and posts table : fields : id, category_id , title, body.
I want to fetch all categories to a dropdown list. When user add new post they have to select category first then he is able to post anything..
My PostsController:
<?php
class PostsController extends AppController{
public $helpers = array('Html', 'Form', 'Session');
public $components = array('Session','Paginator');
public function index(){
$this->Paginator->setting = array(
'limit' =>'10',
'order' => array('Post.id' =>'Desc')
/*'conditions' => array(
'Post.user_id' => AuthComponent::user(id)
)*/
);
$arrPosts = $this->Paginator->paginate('Post');
$this->set('posts', $arrPosts);
}
public function view($id = null){
if(!$id){
throw new NotFoundException(__("Error Processing Request ID"));
}
$post =$this->Post->findById($id);
if(!$post){
throw new NotFoundException(__("Error Processing Request POST"));
}
$this->set('post',$post);
}
public function add(){
// HERE I want to fetch all categoryies from categories table and want to send to view
if($this->request->is('post')){
$this->Post->create();
if($this->Post->save($this->request->data)){
$this->Session->setFlash(__('Blog Posted Sucessfully'));
return $this->redirect(array('action' => 'index'));
}else{
$this->Session->setFlash(__('Unable to Post Blog '));
}
}
}
}
?>
I want to show my category in add form:
Please help me ...
In your controller action you need to use $this->set() to set a View variable. As long as you've got your associations setup correctly in your Post model you should then be able to use:-
$this->set('categories', $this->Post->Category->find('list'));
Cake's FormHelper should automatically know that the Post.category_id form field wants to be a select input with $categories as the options.
One further point, it would be better to set the view variables after processing the form as you won't need them if it saves correctly and so can reduce the number of database queries by 1.
If you use find('all') to retrieve the categories you will need to convert it into the format used by find('list') which can easily be done using Hash::combine():-
$categories = $this->Post->Category->find('all');
$this->set(
'categories',
Hash::combine($categories, '{n}.Category.id', '{n}.Category.name')
);
The only real value in doing this is if you need $categories for something else.
public function add(){
/*Here get the category list & set the view (ctp file)*/
$this->set('categories', $this->Post->Category->find('list'));
if($this->request->is('post')){
$this->Post->create();
if($this->Post->save($this->request->data)){
$this->Session->setFlash(__('Blog Posted Sucessfully'));
return $this->redirect(array('action' => 'index'));
}else{
$this->Session->setFlash(__('Unable to Post Blog '));
}
}
}
//Set The dropdown in ctp file
$this->Form->input('category_id', array('type'=>'select', 'options'=>'categories'));

Zend Framework 2: Extend ZfcUser with own fields

I am working on my first Zend Framework 2 Project. I needed a User Module and integrated ZfcUser for this. Because I have a slight difference in my User Table, I had to use my own User Entity and User Mapper. I created a new Module called ZfcUserExtension.
I then copied a lot of files from the original ZfcUSer Module like:
Entity/User.php
Entity/UserInterface.php
Factory/Entity/IndexControllerFactory.php
Factory/Mapper/UserHydratorFactory.php
Mapper/Exeption/ExceptionInterface
Mapper/Exeption/InvalidArgumentException.php
Mapper/Exeption/RuntimeException.php Mapper/HydratorInterface.php
Mapper/User.php Mapper/UserHydrator.php Mapper/UserHydrator.php
Mapper/UserInterface.php
In zfcuser.global.php I set the user_entity_class to use my own Entity.
'user_entity_class' => 'ZfcUserExtension\Entity\User',
In the module.config.php from the ZfcUserExtension I add the below to make sure that I use my own User Mapper and UserHydrator. The reason for that was that I use "id" as a Primary Key in my User table instead of "user_id", so I had to make sure that this gets overwritten as well.
<?php
return array(
'controllers' => array(
'factories' => array(
'ZfcUserExtension\Controller\Index' => function(Zend\Mvc \Controller\ControllerManager $cm) {
$sm = $cm->getServiceLocator();
return new \ZfcUserExtension\Controller\IndexController(
$sm->get("doctrine.entitymanager.orm_default")
);
}
),
),
'service_manager' => array(
'factories' => array(
'zfcuser_user_mapper' => function ($sm) {
$options = $sm->get('zfcuser_module_options');
$mapper = new \ZfcUserExtension\Mapper\User();
// No db adapter present add below line
$mapper->setDbAdapter($sm->get('zfcuser_zend_db_adapter'));
$entityClass = $options->getUserEntityClass();
// No entity prototype set add below line
$mapper->setEntityPrototype(new $entityClass);
$mapper->setHydrator($sm->get('zfcuser_user_hydrator'));
$mapper->setTableName($options->getTableName());
return $mapper;
},
// 'zfcuserextension_change_password_form' => 'ZfcUserExtension\Factory\Form\ChangePhoneFormFactory',
),
),
I finally got all this to work, till I now run into another problem. I want some additional fields for the User like Phone Number. How would I approach this? I know there are some ideas on the Internet, but I am mainly interested to know how I would actually offer the option to have a "Change Phone" Form. I have created a Form, similar to the "Change Password and "Change Email". I have then created a IndexController.php in my ZfcUSerExtension, again followed the set-up of the UserController from the ZfcUser Module
class IndexController extends AbstractActionController {
const ROUTE_LOGIN = 'zfcuser/login';
/**
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
public function __construct(\Doctrine\ORM\EntityManager $em)
{
$this->em = $em;
}
/**
* #var Form
*/
protected $changeEmailForm;
public function indexAction() {
if (!$this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute(static::ROUTE_LOGIN);
}
return new ViewModel();
}
public function changephoneAction() {
// if the user isn't logged in, we can't change phone
if (!$this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute(static::ROUTE_LOGIN);
}
$form = $this->getChangePhoneForm();
$request = $this->getRequest();
$request->getPost()->set('PrevPhone', $this->getUserService()->getAuthService()->getIdentity()->getPrevPhone());
return array(
'status' => false,
'changePhoneForm' => $form,
);
$fm = $this->flashMessenger()->setNamespace('change-phone')->getMessages();
if (isset($fm[0])) {
$status = $fm[0];
} else {
$status = null;
}
$prg = $this->prg(static::ROUTE_LOGIN);
if ($prg instanceof Response) {
return $prg;
} elseif ($prg === false) {
return array(
'status' => $status,
'changePhoneForm' => $form,
);
}
$form->setData($prg);
if (!$form->isValid()) {
return array(
'status' => false,
'changePhoneForm' => $form,
);
}
$change = $this->getUserService()->changeEmail($prg);
if (!$change) {
$this->flashMessenger()->setNamespace('change-email')->addMessage(false);
return array(
'status' => false,
'changeEmailForm' => $form,
);
}
$this->flashMessenger()->setNamespace('change-email')->addMessage(true);
return $this->redirect()->toRoute(static::ROUTE_CHANGEEMAIL);
}
public function getChangePhoneForm()
{
$sl = $this->getServiceLocator();
$this->setChangePhoneForm($sl->get('zfcuserextension_change_phone_form'));
return $this->changePhoneForm;
}
public function setChangePhoneForm($changePhoneForm)
{
$this->changePhoneForm = $changePhoneForm;
return $this;
}
I now noticed that I will face a problem with the User Service Service/User.php. The Service offers a changePassword() and changeEmail() Method. I now thought that I need to copy this file into my own Modules. Am I right that if I extend the User Service from ZfcUser then the Methods changePassword() and changeEmail() will still be available, so I would delete it from the just copied file and just add changePhone()?
And if I am right with my thoughts, the User Service currently starts like this:
class User extends EventProvider implements ServiceManagerAwareInterface
How would I have to change it that I extend the original User Service? I hope somebody can help, I am still rather confused with all this. Thanky you very much in advance.
There are two possible methods:
Build custom classes extending ZfcUser's entity, form and input filter and add your custom fields. In the ZfcUser configuration change aliases or override factories to ensure your custom classes are instantiated rather than the built in ones.
If you are OK with having the custom profile fields stored and accessed separately from the ZfcUser user entity, check out my module on GitHub: LdcUserProfile. It provides a profile system for ZfcUser but also makes it easy to add your own custom profile fieldsets linked to a user.

CakePHP Private messaging system

I am just wondering if anyone can get me started in writing a private messaging system on the CakePHP framework. I am aiming for something similar to the Facebook inbox system. Of course it doesnt have to be as complicated!
I currently have a AUTH system with users whom can log on and off.
The simplest way would be to just create a messages database table with at the very least, five columns: id, sender_id, recipient_id, subject, body. You can then also add other columns you desire, such as created.
You can then set up your controller as follows:
<?php
class MessagesController extends AppController {
public function inbox() {
$messages = $this->Message->find('all', array(
'conditions' => array(
'recipient_id' => $this->Auth->user('id')
)
));
}
public function outbox() {
$messages = $this->Message->find('all', array(
'conditions' => array(
'sender_id' => $this->Auth->user('id')
)
));
}
public function compose() {
if ($this->request->is('post')) {
$this->request->data['Message']['sender_id'] = $this->Auth->user('id');
if ($this->Message->save($this->request->data)) {
$this->Session->setFlash('Message successfully sent.');
$this->redirect(array('action' => 'outbox'));
}
}
}
}
Obviously you'll need to flesh this example out, and change anything that may not apply to your application. You'll also need to add in checks for if the user is friends with the person they're trying to message if you want that as well.

Categories