PHP-MySQL Tagging - php

I have a comics website which currently allows users to choose which comics they view by category: Charts, Office, Life, Misc, etc.
I'd like to implement a tagging system, similar to what we have here on StackOverflow, which will describe more of the content of each comic rather than its category. Ex: In Charts category, I have several business related...
My simple solution would be to handle it just how I've handled my categorization-
Create a "Tags" table with tagid, tagname, tagdescription
Add a tagid_ForeignKey field in comics table, and add a tag to each post.
When a user clicks on a tag, it will show only those posts with that tag... or if there is also a category specified, it will show that specific category with that specific tag.
This approach, however, seems to limit me to one tag per category. What if I have a comic that is business and relationships related... so It'd need both of those tags.
How would I be able to attach multiple tags per comic?
EDIT:
A few more questions:
1) What do I insert into my new relational table... anything?
2) Also, for while ($row = $tag->fetch_assoc()) {, how can I loop through a table if there is a join? Isn't that an associative array?
3) The issue is that I am echoing out the tag choices as such, so once a user clicks on a link, how would you be able to allow them to then click on another link to assign a 2nd tag?
function getTags() {
include 'dbconnect.php';
global $site;
$tag = $mysqli->query("SELECT tagid, tagname FROM tags");
//$tag = $mysqli->query("SELECT * FROM comics c INNER JOIN comictags ct ON (c.comicID = ct.comicID) WHERE ct.tag_id IN (1, 2, 3) GROUP BY c.comic_id");
mysqli_close($mysqli);
while ($row = $tag->fetch_assoc()) {
echo "<a href='?action=homepage&site=" . $site . "&tag=" . $row['tagid'] . "&tagname=" . $row['tagname'] . "'/>" . $row['tagname'] . "</a><br />";
}
}

Just add another table. Then you have three: One for Tags, one for Comics, and one for the relationship. You have to have this indirection table to properly store a many-to-many relationship. This allows each comic to have zero or more tags (and vice versa).

You can accomplish this with a many-to-many relationship. A many-to-many relationship uses a relational join table that would look like this:
+---------------+---------------+
| comic_id | tag_id |
+---------------+---------------+
| 1 | 2 |
+---------------+---------------+
| 1 | 3 |
+---------------+---------------+
| 1 | 4 |
+---------------+---------------+
Now, in your query:
SELECT * FROM comics c INNER JOIN comic_tags ct ON (c.comic_id = ct.comic_id) WHERE ct.tag_id IN (1, 2, 3) GROUP BY c.comic_id
Where 1, 2, 3 are the tags the user selected that they would like to see.

Related

Store and fetch CSV columns from DB using implode and magic __get()

I have PHP code to select categories from tbl_categorie, Now I have a multiple select in a form where an article can have more than one categorie. While inserting the values in database, I want to store multiple categories values in one column/attribute. Where ID_CAT attribute would only store categories ids of the tbl_categorie separated by comma(,).
I have two tables in one DB,
tbl_blog:
ID_BLOG ID_CAT TITLE ARTICL DATE
1 1,3 title1 article1 2013-03-04
2 4,10 title2 article2 2013-03-04
3 3,6 title3 article3 2013-03-04
tbl_categorie:
ID_CAT NOM_CAT
1 HTML
2 CSS
3 DESIGN
4 PHP
5 ..
I have problem in first place to add tow ID_CAT for one article although I used the implode() statment but she works when i change the type of ID_CAT from int to varchar , and this is the process to add article :
Article::creatArticle(0,$_POST['title'],implode(', ', $_POST['id_categorie']),$_POST['article']);
and this is the function to add article from class_article :
/**
* function créeArticle
*/
public static function creatArticle($id_article,$title,$id_categorie,$article)
{
global $db;
$req = $db->prepare("INSERT INTO blog (ID_BLOG,TITLE,ID_CAT, ARTICLE,DATE) VALUES ('',:title,:id_categorie,:article,'".date('Y-m-d')."')");
$ok = $req->execute(Array('title' =>$title,'id_categorie' => $id_categorie,'article' => $article));
return $db->lastInsertId();
//$erreur = $req->errorInfo();
}
now i have probleme to fetch all categories for which in each article shall have , and this is how i fetch in my back-office the table of articles using the magic method get() which she returne only the first value in th column :
<?php
foreach(Article::getAllArticle()as $blog ){
$article= new Article($blog->ID_BLOG);
$categorie = new Categorie($article->getIDCategorie());
echo'
<tr>';
echo '<td><input type="checkbox" /></td>';
echo '<td>'.$article->getTitlearticle().'</td>';
echo '<td>'.$categorie->getNomCategorie().'</td>';
echo '<td>'.$article->getDatearticle().'</td>';
echo '<td>35</td>';
echo ' <td class="actions">';
echo '<img src="img/icons/actions/edit.png" alt="" />';
echo ' <img src="img/icons/actions/delete.png" alt="" /></td>';
echo '</tr>';
} ..
i know that i have to use the explode() statment or making a loop but i can't figure it out how :( ,and i have a doubt about the type of ID_CAT should be varchar make the problem ? thanx !
It is difficult to understand what you are doing and what you want to do. I'm not sure if you are getting the results from a CSV file or from a database?
If it is from a database, from what I can see you need to normalise your database tables. Create a linking table between the blog and categories, this would make it a lot easier to bring back the results. A simple join sql will get all articles in an category or the get all categories that the article is associated with.
tbl_blog:
ID_BLOG TITLE ARTICL DATE
1 title1 article1 2013-03-04
2 title2 article2 2013-03-04
3 title3 article3 2013-03-04
tbl_categorie:
ID_CAT NOM_CAT
1 HTML
tbl_blogcat
ID_BLOG ID_CAT
1 1
1 3
2 4
2 10
It might be a little extra work now but it will be worth it in the future. Storing your categories as a comma delimited string makes it difficult to search.
This problem can be solve easy way, You can create a new table which will store the information of tbl_blog and tbl_catagory's primary id. The benefit is when you choose one more categories of a blog it will store your information of every category of a blog. the table is look like.....
tbl_combine:
ID_COMB ID_Blog ID_Cat
1 1 2
2 1 1
3 1 3
4 2 3
5 2 2
6 3 1
7 3 2
8 3 3
Above we just let 3 blog and 3 categories. the tbl_combine can store each blog category's primary id of its chosen blog in each row. So we can insert unlimited categories of a blog. there's no need explode or implode function.
Then we can retrieve any blog categories. The query look like.
SELECT
(SELECT category_name FROM tbl_catagory
WHERE id_cat=A.id_cat)
AS catagory_name
FROM tbl_combine
AS A
WHERE id_blog=$blog_id
Note we just store id of tbl_blog and tbl_catagory tables is tbl_combine it will not slow

Tree Traversal recursive calculation

We have users, questions and unlimited levels of categories. The users can get some points from questions. Questions can have multiple categories.
What I want to do is to calculate the top users per category: It's simply total points taken from the questions under that category AND it's sub-categories too.
So, I have these tables:
questions
--------------
id
title
question
categories
--------------
id
parent_id
category
lft
rgt
question_categories
--------------
question_id
category_id
users
--------------
id
username
user_points
--------------
id
user_id
question_id
point_type
points
user_category
--------------
user_id
category_id
points
What I want to do is to calculate user_category.points value.
Summing up the points for each category is easy but including the sub-categories is getting complicated.
What might be the best way to do this?
Example calculation:
Let's say the categories are:
Programming
PHP
Zend Framework
Symfony
Java
Ruby on Rails
Assume that the user got 3 points from Zend Framework, 2 points from PHP, 5 points from java and 1 point from Rails. The points for this user per categories will be:
Programming 11 (5+5+1)
PHP 5 (2+3)
Zend Framework 3
Symfony
Java 5
Ruby on Rails 1
Perhaps it would be best to use tags instead of a hierarchy. For instance, anything with a "Zend Framework" will also have "PHP" and "Programming" tags. This also helps when some categories can appear in multiple places. For instance, I can use ajax in jQuery and also Javascript. Then, add 1 to each tag listed in the category for the user.
I would create a user_categories table in which I would store 3 values: user_id, category_id and user_score. It's easy to maintain (need only to INSERT or UPDATE) and it's also easy to query for top-users of every category.
If you're only going to sum per top-level category, then you should add a field to your categories table called root_id (holding the id of the transitive parent of the category).
Then your sum would be calculated as:
select up.user_id, ctg.root_id, sum(up.points)
from user_points up
join question_categories qc on up.question_id = qc.question_id
join categories ctg on qc.category_id = ctg.id
group by up.user_id, ctg.root_id
This php and SQL should get you the top 3 users for each category including sub categories:
$query = "SELECT id, parent_id FROM categories";
$parent = array();
...fetch mysql data loop depending on what connection you use, mysqli or pdo...
{
$parent[$result['id']] = $result['parent_id'];
}
$childs = array();
foreach($parent as $id => $parrent_id)
{
$childs[$parrent_id][$id] = $id;
$next_parrent_id = $parrent_id;
while($next_parrent_id = $parent[$next_parrent_id])
{
$childs[$next_parrent_id][$id] = $id;
}
}
foreach($parent as $id => $parrent_id)
{
$current_categories = array($id => $id) + $childs[$id];
$query = "SELECT user_id, username, SUM(points) AS total_points
FROM user_points
LEFT JOIN users ON (user_id = users.id)
LEFT JOIN question_categories USING (question_id)
WHERE category_id IN (" . implode(', ', $current_categories). ")
ORDER BY total_points DESC
LIMIT 3";
...fetch mysql data loop...
}

Need help with MySQL query & PHP

I have the following 3 tables:
(PK = Primary Key, FK = Foreign Key)
Files Table
File ID (PK) File Name ...
------------ ---------
1 a.jpg ...
2 b.png ...
3 c.jpg ...
. .
. .
. .
Tags Table
Tag ID (PK) Tag Name ...
----------- ----------
1 Melbourne ...
2 April ...
3 2010 ...
. .
. .
. .
Files_Tags Table
File ID (FK) Tag ID (FK)
------------ -----------
1 1
1 5
1 7
2 2
2 4
3 3
. .
. .
. .
In PHP, I want to get a list of all tags along with the number of times the tag appears (i.e. the number of files that have this tag).
Is that possible to do with one MySQL query ?
Try GROUP BY on your tag id. Use a LEFT JOIN to include tags that exist in the tags table but aren't ever used.
SELECT
Tag_Name,
COUNT(Files_Tags.Tag_ID) AS cnt
FROM Tags
LEFT JOIN Files_Tags
ON Tags.Tag_ID = Files_Tags.Tag_ID
GROUP BY Tags.Tag_ID
Result:
Melbourne 1
April 1
2010 1
... ...
You may also want to add an ORDER BY Tag_Name or an ORDER BY COUNT(*) if you want the rows returned in sorted order.
Daniel Vassello also submitted an answer but deleted it. However his answer is quite easy to adapt to meet your new requirements. Here is his solution, modified to use a LEFT JOIN instead of an INNER JOIN:
SELECT t.tag_id,
t.tag_name,
IFNULL(d.tag_count, 0) AS tag_count
FROM tags t
LEFT JOIN
(
SELECT tag_id, COUNT(*) tag_count
FROM files_tags
GROUP BY tag_id
) d ON d.tag_id = t.tag_id;
You shouldn't use too much of GROUP BY, ORDER BY and * JOIN as those query are very heavy and it's not something you should base your code on.
If I was you, I would do multiple simple SELECT query and group the stuff together using PHP algorithms. This way, you're DB won't be hit by very slow query.
So basically, in your specific question I would have more than 1 query.
I would start by doing a
SELECT * FROM "tags_table".
In php, I would created a foreach loop that would count appearance of every tag in your "files_tags" table:
SELECT FILE_ID COUNT(*) FROM TAGS_TABLE WHERE TAG_ID = 'tag_uid'
It's mostly pseudo-code so I wouldn't expect those query to work but you get the general idea.

MySQL Result - "Group By" removing incorrect duplicates

Will do my best to describe the problem Im having :)
Each thread/topic in my forum represents one disc. Registered members of the forum use a series of checkboxes (one displayed next to each disc) to tick each disc that they have in their collection. When the form is $_POST'ed it stores the information in a table like so:
| user_id - disc_id |
+--------------------+
| 2 - 571 |
| 2 - 603 |
| 2 - 4532 |
When the user next views the forum I have the checkboxes ticked and disabled on discs that the user owns. This is done using:
$sql = 'SELECT id, poster, subject, posted, last_post, last_post_id,
last_poster, num_views, num_replies, closed, sticky, moved_to, topicimage,
c.user_id, c.disc_id FROM topics LEFT JOIN collections AS c ON c.disc_id=id
WHERE forum_id='.$id.' ORDER BY sticky DESC;
The above grabs all of the discs, which I then display using the following (stripped down) code:
$result = $db->query($sql) or error('Unable to fetch topic list '.$sql.'', __FILE__, __LINE__, $db->error());
// If there are topics in this forum
if ($db->num_rows($result))
{
while ($cur_topic = $db->fetch_assoc($result))
{
// If logged in users ID matches the current discs user_id (i.e if this user owns this disc)
if ($cur_topic['user_id']==$pun_user['id']) {
$read = ' I own this!';
} else {
$read = ' I own this!';
}
}
}
This works great, until a second user adds the same disc ID to his collection, eg:
| user_id - disc_id |
+--------------------+
| 2 - 571 |
| 2 - 603 |
| 6 - 571 |
This causes a duplicate thread to appear in the forum. One is correctly ticked (because I own it), the other is not ticked, though it shares all of the same information such as topic id and image.
My first thought was to try adding GROUP BY c.disc_id to the SQL, which does successfully remove the duplicate topic - However, it is removing the wrong one. The disc that I have ticked is no longer shown, leaving only the unticked version.
Hope that makes sense. Can anyone offer any insight or ideas?
Many Thanks.
This is a guess, since I don't know your schema, but I don't see you specifying the user's ID in your WHERE clause.
What about something like the following?
SELECT t.id, t.poster, t.subject, t.posted, t.last_post, t.last_post_id,
t.last_poster, t.num_views, t.num_replies, t.closed, t.sticky,
t.moved_to, t.topicimage, c.user_id, c.disc_id
FROM topics AS t LEFT JOIN collections AS c ON c.disc_id = t.id
WHERE forum_id = '.$id.'
AND c.user_id = '.$user_id.'
ORDER BY t.sticky DESC;
Also, you're joining on Topic ID = Disc ID. Is that intentional?
I can see two easy way for solving this:
first:
with two query, you group query and a second to fetch all the disc_id owned by the user
second:
with your first query:
if ($db->num_rows($result)) {
$array = Array();
while ($cur_topic = $db->fetch_assoc($result)) {
$id = $cur_topic['disc_id'];
if (!array_key_exists ($id, $array)) { // allow only result per disc_id
$array[$id] = $cur_topic;
$array[$id]['owned'] = false;
}
// If logged in users ID matches the current discs user_id (i.e if this user owns this disc)
if ($cur_topic['user_id']==$pun_user['id']) // check if one is owned by the user
$array['owned'] = true;
}
foreach ($array as $cur_topic) {
if ($cur_topic['owned']) {
$read = '<br /><input type="checkbox" disabled="disabled" checked="checked" /> <span style="color:#999">I own this!</span>';
} else {
$read = '<br /><input type="checkbox" name="discs[]" value="'.$cur_topic['id'].'" /> I own this!';
}
}
}

where id = multiple artists

Any time there is an update within my music community (song comment, artist update, new song added, yadda yadda yadda), a new row is inserted in my "updates" table. The row houses the artist id involved along with other information (what type of change, time and date, etc).
My users have a "favorite artists" section where they can do just that -- mark artists as their favorites. As such, I'd like to create a new feature that shows the user the changes made to their various favorite artists.
How should I be doing this efficiently?
SELECT *
FROM table_updates
WHERE artist_id = 1
OR artist_id = 500
OR artist_id = 60032
Keep in mind, a user could have 43,000 of our artists marked as a favorite.
Thoughts?
This depends on how your database is setup. If I had my way, I'd set it up with a table like so:
Table: user_favourite_artist
user_id | artist_id
---------------------
1 | 2
1 | 8
1 | 13
2 | 2
3 | 6
6 | 20
6 | 1
6 | 3
user_id and artist_id together would be a composite primary key. Each row specifies a user, by id, and an artist they have as a favourite, by id. A query like so:
SELECT artist_id FROM user_favourite_artist WHERE user_id = 1
Would give you the artist_id's 2, 8, and 13. This is a very simple query that will scale to your expectations.
On the reverse, when an artist is updated, you'd run this query:
SELECT user_id FROM user_favourite_artist WHERE artist_id = 2
And you would get the user_id's 1 and 2. This will tell you which users to notify. This query is also simple and will scale.
Maybe you can try this:
SELECT *
FROM table_updates
WHERE artist_id IN(1, 500, 60032)
If you have the marked artists in a secondary table, I would recomend rather using a join.
Something like
SELECT *
FORM table_updates tu INNER JOIN
table_marked_by_user tmbu ON tu.artist_id = tmbu.artist_id
WHERE tmbu.user_id = $user_id
If you're on SQL Server, you can use a nested select statement:
select * from table_updates where artist_id in
(select artist_id from favorites_table where user_id = 10)
If you don't mind doing dirty reads, you can speed it up with (nolock).
select * from table_updates (nolock) where artist_id in
(select artist_id from favorites_table (nolock) where user_id = 10)

Categories